Rebooting

This commit is contained in:
Matthew Dillon 2015-03-20 15:52:29 -08:00
parent 41ee2857ee
commit 6030310caa
149 changed files with 1489 additions and 9755 deletions

View file

@ -1,7 +0,0 @@
FROM golang:1.4.0
ADD . /go/src/github.com/thermokarst/bactdb
RUN go get -d -v github.com/thermokarst/bactdb/cmd/bactdb
RUN go install github.com/thermokarst/bactdb/cmd/bactdb
CMD /go/bin/bactdb serve --keys=/bactdb/keys/
EXPOSE 8901

28
Godeps/Godeps.json generated
View file

@ -1,24 +1,20 @@
{ {
"ImportPath": "github.com/thermokarst/bactdb", "ImportPath": "github.com/thermokarst/bactdb",
"GoVersion": "go1.4.1", "GoVersion": "go1.4.2",
"Deps": [ "Deps": [
{ {
"ImportPath": "github.com/DavidHuie/gomigrate", "ImportPath": "github.com/DavidHuie/gomigrate",
"Rev": "7883dd76040debc099811feadd58425b1495103d" "Rev": "cfb6d067cc41d95acfa2e5f496dd236346a8175e"
}, },
{ {
"ImportPath": "github.com/codegangsta/cli", "ImportPath": "github.com/codegangsta/cli",
"Comment": "1.2.0-62-gbf4a526", "Comment": "1.2.0-93-g2bcd11f",
"Rev": "bf4a526f48af7badd25d2cb02d587e1b01be3b50" "Rev": "2bcd11f863d540a1b190dc03b6c4f634c6ae91d4"
}, },
{ {
"ImportPath": "github.com/dgrijalva/jwt-go", "ImportPath": "github.com/dgrijalva/jwt-go",
"Comment": "v2.2.0-11-g7c18dce", "Comment": "v2.2.0-15-g61124b6",
"Rev": "7c18dce7b8ff32db34d4b7054d488db656d75cc3" "Rev": "61124b62ad244d655f87d944aefaa2ae5a0d2f16"
},
{
"ImportPath": "github.com/google/go-querystring/query",
"Rev": "d8840cbb2baa915f4836edda4750050a2c0b7aea"
}, },
{ {
"ImportPath": "github.com/gorilla/context", "ImportPath": "github.com/gorilla/context",
@ -26,11 +22,11 @@
}, },
{ {
"ImportPath": "github.com/gorilla/mux", "ImportPath": "github.com/gorilla/mux",
"Rev": "e444e69cbd2e2e3e0749a2f3c717cec491552bbf" "Rev": "8a875a034c69b940914d83ea03d3f1299b4d094b"
}, },
{ {
"ImportPath": "github.com/gorilla/schema", "ImportPath": "github.com/gorilla/schema",
"Rev": "ad8849d14e5cdf1f4423b1f0fc53827ebbdd7477" "Rev": "c8422571edf3131506bab7df27e18980fe2598d5"
}, },
{ {
"ImportPath": "github.com/jmoiron/modl", "ImportPath": "github.com/jmoiron/modl",
@ -43,16 +39,16 @@
}, },
{ {
"ImportPath": "github.com/lib/pq", "ImportPath": "github.com/lib/pq",
"Comment": "go1.0-cutoff-13-g19eeca3", "Comment": "go1.0-cutoff-29-g30ed220",
"Rev": "19eeca3e30d2577b1761db471ec130810e67f532" "Rev": "30ed2200d7ec99cf17272292f1d4b7b0bd7165db"
}, },
{ {
"ImportPath": "golang.org/x/crypto/bcrypt", "ImportPath": "golang.org/x/crypto/bcrypt",
"Rev": "632d287f9f3f54b09809eebbd3cacbcb00b9f2fc" "Rev": "1351f936d976c60a0a48d728281922cf63eafb8d"
}, },
{ {
"ImportPath": "golang.org/x/crypto/blowfish", "ImportPath": "golang.org/x/crypto/blowfish",
"Rev": "632d287f9f3f54b09809eebbd3cacbcb00b9f2fc" "Rev": "1351f936d976c60a0a48d728281922cf63eafb8d"
} }
] ]
} }

View file

@ -51,6 +51,8 @@ For a given migration, the `id` and `name` fields must be the same.
The id field is an integer that corresponds to the order in which The id field is an integer that corresponds to the order in which
the migration should run relative to the other migrations. the migration should run relative to the other migrations.
`id` should not be `0` as that value is used for internal validations.
### Example ### Example
If I'm trying to add a "users" table to the database, I would create If I'm trying to add a "users" table to the database, I would create

View file

@ -3,7 +3,6 @@
package gomigrate package gomigrate
import ( import (
"bytes"
"database/sql" "database/sql"
"errors" "errors"
"io/ioutil" "io/ioutil"
@ -219,19 +218,8 @@ func (m *Migrator) ApplyMigration(migration *Migration, mType migrationType) err
return err return err
} }
for n, subMigration := range splitMigrationString(string(sql)) {
if allWhitespace.Match([]byte(subMigration)) {
continue
}
log.Printf("Applying submigration: %v", n+1)
for _, line := range bytes.Split([]byte(subMigration), []byte("\n")) {
log.Printf("MIGRATION: %s", line)
}
// Perform the migration. // Perform the migration.
result, err := transaction.Exec(string(subMigration)) result, err := transaction.Exec(string(sql))
if err != nil { if err != nil {
log.Printf("Error executing migration: %v", err) log.Printf("Error executing migration: %v", err)
if rollbackErr := transaction.Rollback(); rollbackErr != nil { if rollbackErr := transaction.Rollback(); rollbackErr != nil {
@ -250,7 +238,6 @@ func (m *Migrator) ApplyMigration(migration *Migration, mType migrationType) err
} else { } else {
log.Printf("Rows affected: %v", rowsAffected) log.Printf("Rows affected: %v", rowsAffected)
} }
}
// Log the event. // Log the event.
if mType == upMigration { if mType == upMigration {

View file

@ -16,7 +16,7 @@ var (
adapter Migratable adapter Migratable
) )
func GetMigrator(test string) *Migrator { func GetMigrator(test string) (*Migrator, string) {
var suffix string var suffix string
if os.Getenv("DB") == "pg" { if os.Getenv("DB") == "pg" {
suffix = "pg" suffix = "pg"
@ -28,12 +28,16 @@ func GetMigrator(test string) *Migrator {
if err != nil { if err != nil {
panic(err) panic(err)
} }
return m return m, suffix
} }
func TestNewMigrator(t *testing.T) { func TestNewMigrator(t *testing.T) {
m := GetMigrator("test1") m, d := GetMigrator("test1")
if len(m.migrations) != 1 { if d == "pg" && len(m.migrations) != 4 {
t.Errorf("Invalid number of migrations detected")
}
if d == "mysql" && len(m.migrations) != 1 {
t.Errorf("Invalid number of migrations detected") t.Errorf("Invalid number of migrations detected")
} }
@ -43,10 +47,10 @@ func TestNewMigrator(t *testing.T) {
t.Errorf("Invalid migration name detected: %s", migration.Name) t.Errorf("Invalid migration name detected: %s", migration.Name)
} }
if migration.Id != 1 { if migration.Id != 1 {
t.Errorf("Invalid migration num detected: %s", migration.Id) t.Errorf("Invalid migration num detected: %d", migration.Id)
} }
if migration.Status != Inactive { if migration.Status != Inactive {
t.Errorf("Invalid migration num detected: %s", migration.Status) t.Errorf("Invalid migration num detected: %d", migration.Status)
} }
cleanup() cleanup()
@ -79,7 +83,7 @@ func TestCreatingMigratorWhenTableExists(t *testing.T) {
} }
func TestMigrationAndRollback(t *testing.T) { func TestMigrationAndRollback(t *testing.T) {
m := GetMigrator("test1") m, d := GetMigrator("test1")
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
t.Error(err) t.Error(err)
@ -110,9 +114,17 @@ func TestMigrationAndRollback(t *testing.T) {
t.Error("Invalid status for migration") t.Error("Invalid status for migration")
} }
if d == "pg" {
if err := m.RollbackN(4); err != nil {
t.Error(err)
}
}
if d == "mysql" {
if err := m.Rollback(); err != nil { if err := m.Rollback(); err != nil {
t.Error(err) t.Error(err)
} }
}
// Ensure that the down migration ran. // Ensure that the down migration ran.
row = db.QueryRow( row = db.QueryRow(
@ -120,8 +132,8 @@ func TestMigrationAndRollback(t *testing.T) {
"test", "test",
) )
err := row.Scan(&tableName) err := row.Scan(&tableName)
if err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
t.Errorf("Migration table should be deleted") t.Errorf("Migration table should be deleted: %v", err)
} }
// Ensure that the migration log is missing. // Ensure that the migration log is missing.
@ -129,11 +141,11 @@ func TestMigrationAndRollback(t *testing.T) {
adapter.GetMigrationSql(), adapter.GetMigrationSql(),
1, 1,
) )
if err := row.Scan(&status); err != sql.ErrNoRows { if err := row.Scan(&status); err != nil && err != sql.ErrNoRows {
t.Error(err) t.Error(err)
} }
if m.migrations[1].Status != Inactive { if m.migrations[1].Status != Inactive {
t.Errorf("Invalid status for migration, %v", m.migrations[1].Status) t.Errorf("Invalid status for migration, expected: %d, got: %v", Inactive, m.migrations[1].Status)
} }
cleanup() cleanup()

View file

@ -1 +1 @@
drop table test; drop table if exists test;

View file

@ -1 +1 @@
create table test(); create table if not exists test();

View file

@ -0,0 +1 @@
drop function if exists create_index_if_not_exists (t_name text, i_name text, index_sql text);

View file

@ -0,0 +1,23 @@
-- this function allows us to create indexes if they don't exist
create or replace function create_index_if_not_exists (t_name text, i_name text, index_sql text) returns void as $$
declare
full_index_name varchar;
schema_name varchar;
begin
full_index_name = t_name || '_' || i_name;
schema_name = 'public';
if not exists (
select 1
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.relname = full_index_name
and n.nspname = schema_name
) then
execute 'create index ' || full_index_name || ' on ' || schema_name || '.' || t_name || ' ' || index_sql;
end if;
end
$$
language plpgsql volatile;

View file

@ -0,0 +1 @@
drop table tt;

View file

@ -0,0 +1,5 @@
CREATE TABLE tt (
c text NOT NULL
);
insert into tt values ('a');
insert into tt values ('x');

View file

@ -0,0 +1 @@
select 1;

View file

@ -0,0 +1 @@
select 1;

View file

@ -7,8 +7,8 @@ import (
) )
var ( var (
upMigrationFile = regexp.MustCompile(`(\d+)_(\w+)_up\.sql`) upMigrationFile = regexp.MustCompile(`(\d+)_([\w-]+)_up\.sql`)
downMigrationFile = regexp.MustCompile(`(\d+)_(\w+)_down\.sql`) downMigrationFile = regexp.MustCompile(`(\d+)_([\w-]+)_down\.sql`)
subMigrationSplit = regexp.MustCompile(`;\s*`) subMigrationSplit = regexp.MustCompile(`;\s*`)
allWhitespace = regexp.MustCompile(`^\s*$`) allWhitespace = regexp.MustCompile(`^\s*$`)
) )
@ -40,11 +40,6 @@ func parseMatches(matches [][][]byte, mType migrationType) (uint64, migrationTyp
return parsedNum, mType, string(name), nil return parsedNum, mType, string(name), nil
} }
// Splits migration sql into different strings separated by a semi-colon.
func splitMigrationString(sql string) []string {
return subMigrationSplit.Split(sql, -1)
}
// This type is used to sort migration ids. // This type is used to sort migration ids.
type uint64slice []uint64 type uint64slice []uint64

View file

@ -210,7 +210,7 @@ Subcommands can be defined for a more git-like command line app.
app.Commands = []cli.Command{ app.Commands = []cli.Command{
{ {
Name: "add", Name: "add",
ShortName: "a", Names: []string{"a"},
Usage: "add a task to the list", Usage: "add a task to the list",
Action: func(c *cli.Context) { Action: func(c *cli.Context) {
println("added task: ", c.Args().First()) println("added task: ", c.Args().First())
@ -218,7 +218,7 @@ app.Commands = []cli.Command{
}, },
{ {
Name: "complete", Name: "complete",
ShortName: "c", Names: []string{"c"},
Usage: "complete a task on the list", Usage: "complete a task on the list",
Action: func(c *cli.Context) { Action: func(c *cli.Context) {
println("completed task: ", c.Args().First()) println("completed task: ", c.Args().First())
@ -226,7 +226,7 @@ app.Commands = []cli.Command{
}, },
{ {
Name: "template", Name: "template",
ShortName: "r", Names: []string{"r"},
Usage: "options for task templates", Usage: "options for task templates",
Subcommands: []cli.Command{ Subcommands: []cli.Command{
{ {
@ -263,7 +263,7 @@ app.EnableBashCompletion = true
app.Commands = []cli.Command{ app.Commands = []cli.Command{
{ {
Name: "complete", Name: "complete",
ShortName: "c", Names: []string{"c"},
Usage: "complete a task on the list", Usage: "complete a task on the list",
Action: func(c *cli.Context) { Action: func(c *cli.Context) {
println("completed task: ", c.Args().First()) println("completed task: ", c.Args().First())
@ -293,6 +293,6 @@ setting the `PROG` variable to the name of your program:
## Contribution Guidelines ## Contribution Guidelines
Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch. Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch.
If you are have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together. If you have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together.
If you feel like you have contributed to the project but have not yet been added as a collaborator, I probably forgot to add you. Hit @codegangsta up over email and we will get it figured out. If you feel like you have contributed to the project but have not yet been added as a collaborator, I probably forgot to add you. Hit @codegangsta up over email and we will get it figured out.

View file

@ -5,6 +5,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"strings"
"text/tabwriter" "text/tabwriter"
"text/template" "text/template"
"time" "time"
@ -34,15 +35,20 @@ type App struct {
// An action to execute before any subcommands are run, but after the context is ready // An action to execute before any subcommands are run, but after the context is ready
// If a non-nil error is returned, no subcommands are run // If a non-nil error is returned, no subcommands are run
Before func(context *Context) error Before func(context *Context) error
// An action to execute after any subcommands are run, but after the subcommand has finished
// It is run even if Action() panics
After func(context *Context) error
// The action to execute when no subcommands are specified // The action to execute when no subcommands are specified
Action func(context *Context) Action func(context *Context)
// Execute this function if the proper command cannot be found // Execute this function if the proper command cannot be found
CommandNotFound func(context *Context, command string) CommandNotFound func(context *Context, command string)
// Compilation date // Compilation date
Compiled time.Time Compiled time.Time
// Author // List of all authors who contributed
Authors []Author
// Name of Author (Note: Use App.Authors, this is deprecated)
Author string Author string
// Author e-mail // Email of Author (Note: Use App.Authors, this is deprecated)
Email string Email string
// Writer writer to write output to // Writer writer to write output to
Writer io.Writer Writer io.Writer
@ -67,22 +73,28 @@ func NewApp() *App {
BashComplete: DefaultAppComplete, BashComplete: DefaultAppComplete,
Action: helpCommand.Action, Action: helpCommand.Action,
Compiled: compileTime(), Compiled: compileTime(),
Author: "Author",
Email: "unknown@email",
Writer: os.Stdout, Writer: os.Stdout,
} }
} }
// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination // Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination
func (a *App) Run(arguments []string) error { func (a *App) Run(arguments []string) (err error) {
if a.Author != "" || a.Email != "" {
a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email})
}
if HelpPrinter == nil { if HelpPrinter == nil {
defer func() { defer func() {
HelpPrinter = nil HelpPrinter = nil
}() }()
HelpPrinter = func(templ string, data interface{}) { HelpPrinter = func(templ string, data interface{}) {
funcMap := template.FuncMap{
"join": strings.Join,
}
w := tabwriter.NewWriter(a.Writer, 0, 8, 1, '\t', 0) w := tabwriter.NewWriter(a.Writer, 0, 8, 1, '\t', 0)
t := template.Must(template.New("help").Parse(templ)) t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
err := t.Execute(w, data) err := t.Execute(w, data)
if err != nil { if err != nil {
panic(err) panic(err)
@ -111,7 +123,7 @@ func (a *App) Run(arguments []string) error {
// parse flags // parse flags
set := flagSet(a.Name, a.Flags) set := flagSet(a.Name, a.Flags)
set.SetOutput(ioutil.Discard) set.SetOutput(ioutil.Discard)
err := set.Parse(arguments[1:]) err = set.Parse(arguments[1:])
nerr := normalizeFlags(a.Flags, set) nerr := normalizeFlags(a.Flags, set)
if nerr != nil { if nerr != nil {
fmt.Fprintln(a.Writer, nerr) fmt.Fprintln(a.Writer, nerr)
@ -141,6 +153,15 @@ func (a *App) Run(arguments []string) error {
return nil return nil
} }
if a.After != nil {
defer func() {
// err is always nil here.
// There is a check to see if it is non-nil
// just few lines before.
err = a.After(context)
}()
}
if a.Before != nil { if a.Before != nil {
err := a.Before(context) err := a.Before(context)
if err != nil { if err != nil {
@ -171,7 +192,7 @@ func (a *App) RunAndExitOnError() {
} }
// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags // Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags
func (a *App) RunAsSubcommand(ctx *Context) error { func (a *App) RunAsSubcommand(ctx *Context) (err error) {
// append help to commands // append help to commands
if len(a.Commands) > 0 { if len(a.Commands) > 0 {
if a.Command(helpCommand.Name) == nil && !a.HideHelp { if a.Command(helpCommand.Name) == nil && !a.HideHelp {
@ -190,7 +211,7 @@ func (a *App) RunAsSubcommand(ctx *Context) error {
// parse flags // parse flags
set := flagSet(a.Name, a.Flags) set := flagSet(a.Name, a.Flags)
set.SetOutput(ioutil.Discard) set.SetOutput(ioutil.Discard)
err := set.Parse(ctx.Args().Tail()) err = set.Parse(ctx.Args().Tail())
nerr := normalizeFlags(a.Flags, set) nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, ctx.globalSet) context := NewContext(a, set, ctx.globalSet)
@ -225,6 +246,15 @@ func (a *App) RunAsSubcommand(ctx *Context) error {
} }
} }
if a.After != nil {
defer func() {
// err is always nil here.
// There is a check to see if it is non-nil
// just few lines before.
err = a.After(context)
}()
}
if a.Before != nil { if a.Before != nil {
err := a.Before(context) err := a.Before(context)
if err != nil { if err != nil {
@ -242,11 +272,7 @@ func (a *App) RunAsSubcommand(ctx *Context) error {
} }
// Run default Action // Run default Action
if len(a.Commands) > 0 {
a.Action(context) a.Action(context)
} else {
a.Action(ctx)
}
return nil return nil
} }
@ -277,3 +303,19 @@ func (a *App) appendFlag(flag Flag) {
a.Flags = append(a.Flags, flag) a.Flags = append(a.Flags, flag)
} }
} }
// Author represents someone who has contributed to a cli project.
type Author struct {
Name string // The Authors name
Email string // The Authors email
}
// String makes Author comply to the Stringer interface, to allow an easy print in the templating process
func (a Author) String() string {
e := ""
if a.Email != "" {
e = "<" + a.Email + "> "
}
return fmt.Sprintf("%v %v", a.Name, e)
}

View file

@ -21,6 +21,9 @@ func ExampleApp() {
app.Action = func(c *cli.Context) { app.Action = func(c *cli.Context) {
fmt.Printf("Hello %v\n", c.String("name")) fmt.Printf("Hello %v\n", c.String("name"))
} }
app.Author = "Harrison"
app.Email = "harrison@lolwut.com"
app.Authors = []cli.Author{cli.Author{Name: "Oliver Allen", Email: "oliver@toyshop.com"}}
app.Run(os.Args) app.Run(os.Args)
// Output: // Output:
// Hello Jeremy // Hello Jeremy
@ -34,13 +37,13 @@ func ExampleAppSubcommand() {
app.Commands = []cli.Command{ app.Commands = []cli.Command{
{ {
Name: "hello", Name: "hello",
ShortName: "hi", Aliases: []string{"hi"},
Usage: "use it to see a description", Usage: "use it to see a description",
Description: "This is how we describe hello the function", Description: "This is how we describe hello the function",
Subcommands: []cli.Command{ Subcommands: []cli.Command{
{ {
Name: "english", Name: "english",
ShortName: "en", Aliases: []string{"en"},
Usage: "sends a greeting in english", Usage: "sends a greeting in english",
Description: "greets someone in english", Description: "greets someone in english",
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -75,7 +78,7 @@ func ExampleAppHelp() {
app.Commands = []cli.Command{ app.Commands = []cli.Command{
{ {
Name: "describeit", Name: "describeit",
ShortName: "d", Aliases: []string{"d"},
Usage: "use it to see a description", Usage: "use it to see a description",
Description: "This is how we describe describeit the function", Description: "This is how we describe describeit the function",
Action: func(c *cli.Context) { Action: func(c *cli.Context) {
@ -105,7 +108,7 @@ func ExampleAppBashComplete() {
app.Commands = []cli.Command{ app.Commands = []cli.Command{
{ {
Name: "describeit", Name: "describeit",
ShortName: "d", Aliases: []string{"d"},
Usage: "use it to see a description", Usage: "use it to see a description",
Description: "This is how we describe describeit the function", Description: "This is how we describe describeit the function",
Action: func(c *cli.Context) { Action: func(c *cli.Context) {
@ -159,8 +162,8 @@ var commandAppTests = []struct {
func TestApp_Command(t *testing.T) { func TestApp_Command(t *testing.T) {
app := cli.NewApp() app := cli.NewApp()
fooCommand := cli.Command{Name: "foobar", ShortName: "f"} fooCommand := cli.Command{Name: "foobar", Aliases: []string{"f"}}
batCommand := cli.Command{Name: "batbaz", ShortName: "b"} batCommand := cli.Command{Name: "batbaz", Aliases: []string{"b"}}
app.Commands = []cli.Command{ app.Commands = []cli.Command{
fooCommand, fooCommand,
batCommand, batCommand,
@ -193,6 +196,32 @@ func TestApp_CommandWithArgBeforeFlags(t *testing.T) {
expect(t, firstArg, "my-arg") expect(t, firstArg, "my-arg")
} }
func TestApp_RunAsSubcommandParseFlags(t *testing.T) {
var context *cli.Context
a := cli.NewApp()
a.Commands = []cli.Command{
{
Name: "foo",
Action: func(c *cli.Context) {
context = c
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "lang",
Value: "english",
Usage: "language for the greeting",
},
},
Before: func(_ *cli.Context) error { return nil },
},
}
a.Run([]string{"", "foo", "--lang", "spanish", "abcd"})
expect(t, context.Args().Get(0), "abcd")
expect(t, context.String("lang"), "spanish")
}
func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) {
var parsedOption string var parsedOption string
var args []string var args []string
@ -420,6 +449,71 @@ func TestApp_BeforeFunc(t *testing.T) {
} }
func TestApp_AfterFunc(t *testing.T) {
afterRun, subcommandRun := false, false
afterError := fmt.Errorf("fail")
var err error
app := cli.NewApp()
app.After = func(c *cli.Context) error {
afterRun = true
s := c.String("opt")
if s == "fail" {
return afterError
}
return nil
}
app.Commands = []cli.Command{
cli.Command{
Name: "sub",
Action: func(c *cli.Context) {
subcommandRun = true
},
},
}
app.Flags = []cli.Flag{
cli.StringFlag{Name: "opt"},
}
// run with the After() func succeeding
err = app.Run([]string{"command", "--opt", "succeed", "sub"})
if err != nil {
t.Fatalf("Run error: %s", err)
}
if afterRun == false {
t.Errorf("After() not executed when expected")
}
if subcommandRun == false {
t.Errorf("Subcommand not executed when expected")
}
// reset
afterRun, subcommandRun = false, false
// run with the Before() func failing
err = app.Run([]string{"command", "--opt", "fail", "sub"})
// should be the same error produced by the Before func
if err != afterError {
t.Errorf("Run error expected, but not received")
}
if afterRun == false {
t.Errorf("After() not executed when expected")
}
if subcommandRun == false {
t.Errorf("Subcommand not executed when expected")
}
}
func TestAppNoHelpFlag(t *testing.T) { func TestAppNoHelpFlag(t *testing.T) {
oldFlag := cli.HelpFlag oldFlag := cli.HelpFlag
defer func() { defer func() {

View file

@ -13,7 +13,7 @@ func Example() {
app.Commands = []cli.Command{ app.Commands = []cli.Command{
{ {
Name: "add", Name: "add",
ShortName: "a", Aliases: []string{"a"},
Usage: "add a task to the list", Usage: "add a task to the list",
Action: func(c *cli.Context) { Action: func(c *cli.Context) {
println("added task: ", c.Args().First()) println("added task: ", c.Args().First())
@ -21,7 +21,7 @@ func Example() {
}, },
{ {
Name: "complete", Name: "complete",
ShortName: "c", Aliases: []string{"c"},
Usage: "complete a task on the list", Usage: "complete a task on the list",
Action: func(c *cli.Context) { Action: func(c *cli.Context) {
println("completed task: ", c.Args().First()) println("completed task: ", c.Args().First())
@ -38,13 +38,13 @@ func ExampleSubcommand() {
app.Commands = []cli.Command{ app.Commands = []cli.Command{
{ {
Name: "hello", Name: "hello",
ShortName: "hi", Aliases: []string{"hi"},
Usage: "use it to see a description", Usage: "use it to see a description",
Description: "This is how we describe hello the function", Description: "This is how we describe hello the function",
Subcommands: []cli.Command{ Subcommands: []cli.Command{
{ {
Name: "english", Name: "english",
ShortName: "en", Aliases: []string{"en"},
Usage: "sends a greeting in english", Usage: "sends a greeting in english",
Description: "greets someone in english", Description: "greets someone in english",
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -59,7 +59,7 @@ func ExampleSubcommand() {
}, },
}, { }, {
Name: "spanish", Name: "spanish",
ShortName: "sp", Aliases: []string{"sp"},
Usage: "sends a greeting in spanish", Usage: "sends a greeting in spanish",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{ cli.StringFlag{
@ -73,7 +73,7 @@ func ExampleSubcommand() {
}, },
}, { }, {
Name: "french", Name: "french",
ShortName: "fr", Aliases: []string{"fr"},
Usage: "sends a greeting in french", Usage: "sends a greeting in french",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{ cli.StringFlag{

View file

@ -10,8 +10,10 @@ import (
type Command struct { type Command struct {
// The name of the command // The name of the command
Name string Name string
// short name of the command. Typically one character // short name of the command. Typically one character (deprecated, use `Aliases`)
ShortName string ShortName string
// A list of aliases for the command
Aliases []string
// A short description of the usage of this command // A short description of the usage of this command
Usage string Usage string
// A longer explanation of how the command works // A longer explanation of how the command works
@ -21,6 +23,9 @@ type Command struct {
// An action to execute before any sub-subcommands are run, but after the context is ready // An action to execute before any sub-subcommands are run, but after the context is ready
// If a non-nil error is returned, no sub-subcommands are run // If a non-nil error is returned, no sub-subcommands are run
Before func(context *Context) error Before func(context *Context) error
// An action to execute after any subcommands are run, but after the subcommand has finished
// It is run even if Action() panics
After func(context *Context) error
// The function to call when this command is invoked // The function to call when this command is invoked
Action func(context *Context) Action func(context *Context)
// List of child commands // List of child commands
@ -36,7 +41,7 @@ type Command struct {
// Invokes the command given the context, parses ctx.Args() to generate command-specific flags // Invokes the command given the context, parses ctx.Args() to generate command-specific flags
func (c Command) Run(ctx *Context) error { func (c Command) Run(ctx *Context) error {
if len(c.Subcommands) > 0 || c.Before != nil { if len(c.Subcommands) > 0 || c.Before != nil || c.After != nil {
return c.startApp(ctx) return c.startApp(ctx)
} }
@ -114,9 +119,24 @@ func (c Command) Run(ctx *Context) error {
return nil return nil
} }
func (c Command) Names() []string {
names := []string{c.Name}
if c.ShortName != "" {
names = append(names, c.ShortName)
}
return append(names, c.Aliases...)
}
// Returns true if Command.Name or Command.ShortName matches given name // Returns true if Command.Name or Command.ShortName matches given name
func (c Command) HasName(name string) bool { func (c Command) HasName(name string) bool {
return c.Name == name || c.ShortName == name for _, n := range c.Names() {
if n == name {
return true
}
}
return false
} }
func (c Command) startApp(ctx *Context) error { func (c Command) startApp(ctx *Context) error {
@ -146,6 +166,7 @@ func (c Command) startApp(ctx *Context) error {
// set the actions // set the actions
app.Before = c.Before app.Before = c.Before
app.After = c.After
if c.Action != nil { if c.Action != nil {
app.Action = c.Action app.Action = c.Action
} else { } else {

View file

@ -17,7 +17,7 @@ func TestCommandDoNotIgnoreFlags(t *testing.T) {
command := cli.Command{ command := cli.Command{
Name: "test-cmd", Name: "test-cmd",
ShortName: "tc", Aliases: []string{"tc"},
Usage: "this is for testing", Usage: "this is for testing",
Description: "testing", Description: "testing",
Action: func(_ *cli.Context) {}, Action: func(_ *cli.Context) {},
@ -37,7 +37,7 @@ func TestCommandIgnoreFlags(t *testing.T) {
command := cli.Command{ command := cli.Command{
Name: "test-cmd", Name: "test-cmd",
ShortName: "tc", Aliases: []string{"tc"},
Usage: "this is for testing", Usage: "this is for testing",
Description: "testing", Description: "testing",
Action: func(_ *cli.Context) {}, Action: func(_ *cli.Context) {},

View file

@ -106,6 +106,11 @@ func (c *Context) GlobalGeneric(name string) interface{} {
return lookupGeneric(name, c.globalSet) return lookupGeneric(name, c.globalSet)
} }
// Returns the number of flags set
func (c *Context) NumFlags() int {
return c.flagSet.NFlag()
}
// Determines if the flag was actually set // Determines if the flag was actually set
func (c *Context) IsSet(name string) bool { func (c *Context) IsSet(name string) bool {
if c.setFlags == nil { if c.setFlags == nil {

View file

@ -97,3 +97,15 @@ func TestContext_GlobalIsSet(t *testing.T) {
expect(t, c.GlobalIsSet("myflagGlobalUnset"), false) expect(t, c.GlobalIsSet("myflagGlobalUnset"), false)
expect(t, c.GlobalIsSet("bogusGlobal"), false) expect(t, c.GlobalIsSet("bogusGlobal"), false)
} }
func TestContext_NumFlags(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Bool("myflag", false, "doc")
set.String("otherflag", "hello world", "doc")
globalSet := flag.NewFlagSet("test", 0)
globalSet.Bool("myflagGlobal", true, "doc")
c := cli.NewContext(nil, set, globalSet)
set.Parse([]string{"--myflag", "--otherflag=foo"})
globalSet.Parse([]string{"--myflagGlobal"})
expect(t, c.NumFlags(), 2)
}

View file

@ -12,14 +12,13 @@ USAGE:
{{.Name}} {{if .Flags}}[global options] {{end}}command{{if .Flags}} [command options]{{end}} [arguments...] {{.Name}} {{if .Flags}}[global options] {{end}}command{{if .Flags}} [command options]{{end}} [arguments...]
VERSION: VERSION:
{{.Version}}{{if or .Author .Email}} {{.Version}}
AUTHOR:{{if .Author}}
{{.Author}}{{if .Email}} - <{{.Email}}>{{end}}{{else}}
{{.Email}}{{end}}{{end}}
AUTHOR(S):
{{range .Authors}}{{ . }}
{{end}}
COMMANDS: COMMANDS:
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
{{end}}{{if .Flags}} {{end}}{{if .Flags}}
GLOBAL OPTIONS: GLOBAL OPTIONS:
{{range .Flags}}{{.}} {{range .Flags}}{{.}}
@ -53,7 +52,7 @@ USAGE:
{{.Name}} command{{if .Flags}} [command options]{{end}} [arguments...] {{.Name}} command{{if .Flags}} [command options]{{end}} [arguments...]
COMMANDS: COMMANDS:
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
{{end}}{{if .Flags}} {{end}}{{if .Flags}}
OPTIONS: OPTIONS:
{{range .Flags}}{{.}} {{range .Flags}}{{.}}
@ -62,7 +61,7 @@ OPTIONS:
var helpCommand = Command{ var helpCommand = Command{
Name: "help", Name: "help",
ShortName: "h", Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command", Usage: "Shows a list of commands or help for one command",
Action: func(c *Context) { Action: func(c *Context) {
args := c.Args() args := c.Args()
@ -76,7 +75,7 @@ var helpCommand = Command{
var helpSubcommand = Command{ var helpSubcommand = Command{
Name: "help", Name: "help",
ShortName: "h", Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command", Usage: "Shows a list of commands or help for one command",
Action: func(c *Context) { Action: func(c *Context) {
args := c.Args() args := c.Args()
@ -103,15 +102,20 @@ func ShowAppHelp(c *Context) {
// Prints the list of subcommands as the default app completion method // Prints the list of subcommands as the default app completion method
func DefaultAppComplete(c *Context) { func DefaultAppComplete(c *Context) {
for _, command := range c.App.Commands { for _, command := range c.App.Commands {
fmt.Fprintln(c.App.Writer, command.Name) for _, name := range command.Names() {
if command.ShortName != "" { fmt.Fprintln(c.App.Writer, name)
fmt.Fprintln(c.App.Writer, command.ShortName)
} }
} }
} }
// Prints help for the given command // Prints help for the given command
func ShowCommandHelp(c *Context, command string) { func ShowCommandHelp(c *Context, command string) {
// show the subcommand help for a command with subcommands
if command == "" {
HelpPrinter(SubcommandHelpTemplate, c.App)
return
}
for _, c := range c.App.Commands { for _, c := range c.App.Commands {
if c.HasName(command) { if c.HasName(command) {
HelpPrinter(CommandHelpTemplate, c) HelpPrinter(CommandHelpTemplate, c)

View file

@ -34,7 +34,7 @@ Parsing and verifying tokens is pretty straight forward. You pass in the token
```go ```go
// Create the token // Create the token
token := jwt.New(SigningMethodHS256) token := jwt.New(jwt.SigningMethodHS256)
// Set some claims // Set some claims
token.Claims["foo"] = "bar" token.Claims["foo"] = "bar"
token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix() token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()

View file

@ -18,7 +18,7 @@ func ExampleParse(myToken string, myLookupKey func(interface{}) (interface{}, er
} }
} }
func ExampleNew(mySigningKey string) (string, error) { func ExampleNew(mySigningKey []byte) (string, error) {
// Create the token // Create the token
token := jwt.New(jwt.SigningMethodHS256) token := jwt.New(jwt.SigningMethodHS256)
// Set some claims // Set some claims

View file

@ -66,6 +66,8 @@ func (m *SigningMethodHMAC) Verify(signingString, signature string, key interfac
return ErrInvalidKey return ErrInvalidKey
} }
// Implements the Sign method from SigningMethod for this signing method.
// Key must be []byte
func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) { func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) {
if keyBytes, ok := key.([]byte); ok { if keyBytes, ok := key.([]byte); ok {
if !m.Hash.Available() { if !m.Hash.Available() {

View file

@ -1,274 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package query implements encoding of structs into URL query parameters.
//
// As a simple example:
//
// type Options struct {
// Query string `url:"q"`
// ShowAll bool `url:"all"`
// Page int `url:"page"`
// }
//
// opt := Options{ "foo", true, 2 }
// v, _ := query.Values(opt)
// fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2"
//
// The exact mapping between Go values and url.Values is described in the
// documentation for the Values() function.
package query
import (
"bytes"
"errors"
"fmt"
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
var timeType = reflect.TypeOf(time.Time{})
var encoderType = reflect.TypeOf(new(Encoder)).Elem()
// Encoder is an interface implemented by any type that wishes to encode
// itself into URL values in a non-standard way.
type Encoder interface {
EncodeValues(key string, v *url.Values) error
}
// Values returns the url.Values encoding of v.
//
// Values expects to be passed a struct, and traverses it recursively using the
// following encoding rules.
//
// Each exported struct field is encoded as a URL parameter unless
//
// - the field's tag is "-", or
// - the field is empty and its tag specifies the "omitempty" option
//
// The empty values are false, 0, any nil pointer or interface value, any array
// slice, map, or string of length zero, and any time.Time that returns true
// for IsZero().
//
// The URL parameter name defaults to the struct field name but can be
// specified in the struct field's tag value. The "url" key in the struct
// field's tag value is the key name, followed by an optional comma and
// options. For example:
//
// // Field is ignored by this package.
// Field int `url:"-"`
//
// // Field appears as URL parameter "myName".
// Field int `url:"myName"`
//
// // Field appears as URL parameter "myName" and the field is omitted if
// // its value is empty
// Field int `url:"myName,omitempty"`
//
// // Field appears as URL parameter "Field" (the default), but the field
// // is skipped if empty. Note the leading comma.
// Field int `url:",omitempty"`
//
// For encoding individual field values, the following type-dependent rules
// apply:
//
// Boolean values default to encoding as the strings "true" or "false".
// Including the "int" option signals that the field should be encoded as the
// strings "1" or "0".
//
// time.Time values default to encoding as RFC3339 timestamps. Including the
// "unix" option signals that the field should be encoded as a Unix time (see
// time.Unix())
//
// Slice and Array values default to encoding as multiple URL values of the
// same name. Including the "comma" option signals that the field should be
// encoded as a single comma-delimited value. Including the "space" option
// similarly encodes the value as a single space-delimited string.
//
// Anonymous struct fields are usually encoded as if their inner exported
// fields were fields in the outer struct, subject to the standard Go
// visibility rules. An anonymous struct field with a name given in its URL
// tag is treated as having that name, rather than being anonymous.
//
// Non-nil pointer values are encoded as the value pointed to.
//
// All other values are encoded using their default string representation.
//
// Multiple fields that encode to the same URL parameter name will be included
// as multiple URL values of the same name.
func Values(v interface{}) (url.Values, error) {
val := reflect.ValueOf(v)
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil, errors.New("query: Values() expects non-nil value")
}
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
}
values := make(url.Values)
err := reflectValue(values, val)
return values, err
}
// reflectValue populates the values parameter from the struct fields in val.
// Embedded structs are followed recursively (using the rules defined in the
// Values function documentation) breadth-first.
func reflectValue(values url.Values, val reflect.Value) error {
var embedded []reflect.Value
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
sf := typ.Field(i)
if sf.PkgPath != "" { // unexported
continue
}
sv := val.Field(i)
tag := sf.Tag.Get("url")
if tag == "-" {
continue
}
name, opts := parseTag(tag)
if name == "" {
if sf.Anonymous && sv.Kind() == reflect.Struct {
// save embedded struct for later processing
embedded = append(embedded, sv)
continue
}
name = sf.Name
}
if opts.Contains("omitempty") && isEmptyValue(sv) {
continue
}
if sv.Type().Implements(encoderType) {
m := sv.Interface().(Encoder)
if err := m.EncodeValues(name, &values); err != nil {
return err
}
continue
}
if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
var del byte
if opts.Contains("comma") {
del = ','
} else if opts.Contains("space") {
del = ' '
}
if del != 0 {
s := new(bytes.Buffer)
first := true
for i := 0; i < sv.Len(); i++ {
if first {
first = false
} else {
s.WriteByte(del)
}
s.WriteString(valueString(sv.Index(i), opts))
}
values.Add(name, s.String())
} else {
for i := 0; i < sv.Len(); i++ {
values.Add(name, valueString(sv.Index(i), opts))
}
}
continue
}
values.Add(name, valueString(sv, opts))
}
for _, f := range embedded {
if err := reflectValue(values, f); err != nil {
return err
}
}
return nil
}
// valueString returns the string representation of a value.
func valueString(v reflect.Value, opts tagOptions) string {
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return ""
}
v = v.Elem()
}
if v.Kind() == reflect.Bool && opts.Contains("int") {
if v.Bool() {
return "1"
}
return "0"
}
if v.Type() == timeType {
t := v.Interface().(time.Time)
if opts.Contains("unix") {
return strconv.FormatInt(t.Unix(), 10)
}
return t.Format(time.RFC3339)
}
return fmt.Sprint(v.Interface())
}
// isEmptyValue checks if a value should be considered empty for the purposes
// of omitting fields with the "omitempty" option.
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
if v.Type() == timeType {
return v.Interface().(time.Time).IsZero()
}
return false
}
// tagOptions is the string following a comma in a struct field's "url" tag, or
// the empty string. It does not include the leading comma.
type tagOptions []string
// parseTag splits a struct field's url tag into its name and comma-separated
// options.
func parseTag(tag string) (string, tagOptions) {
s := strings.Split(tag, ",")
return s[0], s[1:]
}
// Contains checks whether the tagOptions contains the specified option.
func (o tagOptions) Contains(option string) bool {
for _, s := range o {
if s == option {
return true
}
}
return false
}

View file

@ -1,238 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package query
import (
"fmt"
"net/url"
"reflect"
"testing"
"time"
)
func TestValues_types(t *testing.T) {
str := "string"
strPtr := &str
tests := []struct {
in interface{}
want url.Values
}{
{
// basic primitives
struct {
A string
B int
C uint
D float32
E bool
}{},
url.Values{
"A": {""},
"B": {"0"},
"C": {"0"},
"D": {"0"},
"E": {"false"},
},
},
{
// pointers
struct {
A *string
B *int
C **string
}{A: strPtr, C: &strPtr},
url.Values{
"A": {str},
"B": {""},
"C": {str},
},
},
{
// slices and arrays
struct {
A []string
B []string `url:",comma"`
C []string `url:",space"`
D [2]string
E [2]string `url:",comma"`
F [2]string `url:",space"`
G []*string `url:",space"`
H []bool `url:",int,space"`
}{
A: []string{"a", "b"},
B: []string{"a", "b"},
C: []string{"a", "b"},
D: [2]string{"a", "b"},
E: [2]string{"a", "b"},
F: [2]string{"a", "b"},
G: []*string{&str, &str},
H: []bool{true, false},
},
url.Values{
"A": {"a", "b"},
"B": {"a,b"},
"C": {"a b"},
"D": {"a", "b"},
"E": {"a,b"},
"F": {"a b"},
"G": {"string string"},
"H": {"1 0"},
},
},
{
// other types
struct {
A time.Time
B time.Time `url:",unix"`
C bool `url:",int"`
D bool `url:",int"`
}{
A: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC),
B: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC),
C: true,
D: false,
},
url.Values{
"A": {"2000-01-01T12:34:56Z"},
"B": {"946730096"},
"C": {"1"},
"D": {"0"},
},
},
}
for i, tt := range tests {
v, err := Values(tt.in)
if err != nil {
t.Errorf("%d. Values(%q) returned error: %v", i, tt.in, err)
}
if !reflect.DeepEqual(tt.want, v) {
t.Errorf("%d. Values(%q) returned %v, want %v", i, tt.in, v, tt.want)
}
}
}
func TestValues_omitEmpty(t *testing.T) {
str := ""
s := struct {
a string
A string
B string `url:",omitempty"`
C string `url:"-"`
D string `url:"omitempty"` // actually named omitempty, not an option
E *string `url:",omitempty"`
}{E: &str}
v, err := Values(s)
if err != nil {
t.Errorf("Values(%q) returned error: %v", s, err)
}
want := url.Values{
"A": {""},
"omitempty": {""},
"E": {""}, // E is included because the pointer is not empty, even though the string being pointed to is
}
if !reflect.DeepEqual(want, v) {
t.Errorf("Values(%q) returned %v, want %v", s, v, want)
}
}
type A struct {
B
}
type B struct {
C string
}
type D struct {
B
C string
}
func TestValues_embeddedStructs(t *testing.T) {
tests := []struct {
in interface{}
want url.Values
}{
{
A{B{C: "foo"}},
url.Values{"C": {"foo"}},
},
{
D{B: B{C: "bar"}, C: "foo"},
url.Values{"C": {"foo", "bar"}},
},
}
for i, tt := range tests {
v, err := Values(tt.in)
if err != nil {
t.Errorf("%d. Values(%q) returned error: %v", i, tt.in, err)
}
if !reflect.DeepEqual(tt.want, v) {
t.Errorf("%d. Values(%q) returned %v, want %v", i, tt.in, v, tt.want)
}
}
}
func TestValues_invalidInput(t *testing.T) {
_, err := Values("")
if err == nil {
t.Errorf("expected Values() to return an error on invalid input")
}
}
type EncodedArgs []string
func (m EncodedArgs) EncodeValues(key string, v *url.Values) error {
for i, arg := range m {
v.Set(fmt.Sprintf("%s.%d", key, i), arg)
}
return nil
}
func TestValues_Marshaler(t *testing.T) {
s := struct {
Args EncodedArgs `url:"arg"`
}{[]string{"a", "b", "c"}}
v, err := Values(s)
if err != nil {
t.Errorf("Values(%q) returned error: %v", s, err)
}
want := url.Values{
"arg.0": {"a"},
"arg.1": {"b"},
"arg.2": {"c"},
}
if !reflect.DeepEqual(want, v) {
t.Errorf("Values(%q) returned %v, want %v", s, v, want)
}
}
func TestTagParsing(t *testing.T) {
name, opts := parseTag("field,foobar,foo")
if name != "field" {
t.Fatalf("name = %q, want field", name)
}
for _, tt := range []struct {
opt string
want bool
}{
{"foobar", true},
{"foo", true},
{"bar", false},
{"field", false},
} {
if opts.Contains(tt.opt) != tt.want {
t.Errorf("Contains(%q) = %v", tt.opt, !tt.want)
}
}
}

View file

@ -152,6 +152,13 @@ func (r *Router) getRegexpGroup() *routeRegexpGroup {
return nil return nil
} }
func (r *Router) buildVars(m map[string]string) map[string]string {
if r.parent != nil {
m = r.parent.buildVars(m)
}
return m
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Route factories // Route factories
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -224,6 +231,12 @@ func (r *Router) Schemes(schemes ...string) *Route {
return r.NewRoute().Schemes(schemes...) return r.NewRoute().Schemes(schemes...)
} }
// BuildVars registers a new route with a custom function for modifying
// route variables before building a URL.
func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route {
return r.NewRoute().BuildVarsFunc(f)
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Context // Context
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View file

@ -593,6 +593,39 @@ func TestMatcherFunc(t *testing.T) {
} }
} }
func TestBuildVarsFunc(t *testing.T) {
tests := []routeTest{
{
title: "BuildVarsFunc set on route",
route: new(Route).Path(`/111/{v1:\d}{v2:.*}`).BuildVarsFunc(func(vars map[string]string) map[string]string {
vars["v1"] = "3"
vars["v2"] = "a"
return vars
}),
request: newRequest("GET", "http://localhost/111/2"),
path: "/111/3a",
shouldMatch: true,
},
{
title: "BuildVarsFunc set on route and parent route",
route: new(Route).PathPrefix(`/{v1:\d}`).BuildVarsFunc(func(vars map[string]string) map[string]string {
vars["v1"] = "2"
return vars
}).Subrouter().Path(`/{v2:\w}`).BuildVarsFunc(func(vars map[string]string) map[string]string {
vars["v2"] = "b"
return vars
}),
request: newRequest("GET", "http://localhost/1/a"),
path: "/2/b",
shouldMatch: true,
},
}
for _, test := range tests {
testRoute(t, test)
}
}
func TestSubRouter(t *testing.T) { func TestSubRouter(t *testing.T) {
subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter() subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter()
subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter() subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter()

View file

@ -150,11 +150,7 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
} }
// url builds a URL part using the given values. // url builds a URL part using the given values.
func (r *routeRegexp) url(pairs ...string) (string, error) { func (r *routeRegexp) url(values map[string]string) (string, error) {
values, err := mapFromPairs(pairs...)
if err != nil {
return "", err
}
urlValues := make([]interface{}, len(r.varsN)) urlValues := make([]interface{}, len(r.varsN))
for k, v := range r.varsN { for k, v := range r.varsN {
value, ok := values[v] value, ok := values[v]

View file

@ -31,6 +31,8 @@ type Route struct {
name string name string
// Error resulted from building a route. // Error resulted from building a route.
err error err error
buildVarsFunc BuildVarsFunc
} }
// Match matches the route against the request. // Match matches the route against the request.
@ -360,6 +362,19 @@ func (r *Route) Schemes(schemes ...string) *Route {
return r.addMatcher(schemeMatcher(schemes)) return r.addMatcher(schemeMatcher(schemes))
} }
// BuildVarsFunc --------------------------------------------------------------
// BuildVarsFunc is the function signature used by custom build variable
// functions (which can modify route variables before a route's URL is built).
type BuildVarsFunc func(map[string]string) map[string]string
// BuildVarsFunc adds a custom function to be used to modify build variables
// before a route's URL is built.
func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route {
r.buildVarsFunc = f
return r
}
// Subrouter ------------------------------------------------------------------ // Subrouter ------------------------------------------------------------------
// Subrouter creates a subrouter for the route. // Subrouter creates a subrouter for the route.
@ -422,17 +437,20 @@ func (r *Route) URL(pairs ...string) (*url.URL, error) {
if r.regexp == nil { if r.regexp == nil {
return nil, errors.New("mux: route doesn't have a host or path") return nil, errors.New("mux: route doesn't have a host or path")
} }
values, err := r.prepareVars(pairs...)
if err != nil {
return nil, err
}
var scheme, host, path string var scheme, host, path string
var err error
if r.regexp.host != nil { if r.regexp.host != nil {
// Set a default scheme. // Set a default scheme.
scheme = "http" scheme = "http"
if host, err = r.regexp.host.url(pairs...); err != nil { if host, err = r.regexp.host.url(values); err != nil {
return nil, err return nil, err
} }
} }
if r.regexp.path != nil { if r.regexp.path != nil {
if path, err = r.regexp.path.url(pairs...); err != nil { if path, err = r.regexp.path.url(values); err != nil {
return nil, err return nil, err
} }
} }
@ -453,7 +471,11 @@ func (r *Route) URLHost(pairs ...string) (*url.URL, error) {
if r.regexp == nil || r.regexp.host == nil { if r.regexp == nil || r.regexp.host == nil {
return nil, errors.New("mux: route doesn't have a host") return nil, errors.New("mux: route doesn't have a host")
} }
host, err := r.regexp.host.url(pairs...) values, err := r.prepareVars(pairs...)
if err != nil {
return nil, err
}
host, err := r.regexp.host.url(values)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -473,7 +495,11 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
if r.regexp == nil || r.regexp.path == nil { if r.regexp == nil || r.regexp.path == nil {
return nil, errors.New("mux: route doesn't have a path") return nil, errors.New("mux: route doesn't have a path")
} }
path, err := r.regexp.path.url(pairs...) values, err := r.prepareVars(pairs...)
if err != nil {
return nil, err
}
path, err := r.regexp.path.url(values)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -482,6 +508,26 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
}, nil }, nil
} }
// prepareVars converts the route variable pairs into a map. If the route has a
// BuildVarsFunc, it is invoked.
func (r *Route) prepareVars(pairs ...string) (map[string]string, error) {
m, err := mapFromPairs(pairs...)
if err != nil {
return nil, err
}
return r.buildVars(m), nil
}
func (r *Route) buildVars(m map[string]string) map[string]string {
if r.parent != nil {
m = r.parent.buildVars(m)
}
if r.buildVarsFunc != nil {
m = r.buildVarsFunc(m)
}
return m
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// parentRoute // parentRoute
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -490,6 +536,7 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
type parentRoute interface { type parentRoute interface {
getNamedRoutes() map[string]*Route getNamedRoutes() map[string]*Route
getRegexpGroup() *routeRegexpGroup getRegexpGroup() *routeRegexpGroup
buildVars(map[string]string) map[string]string
} }
// getNamedRoutes returns the map where named routes are registered. // getNamedRoutes returns the map where named routes are registered.

View file

@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"strings"
) )
// NewDecoder returns a new Decoder. // NewDecoder returns a new Decoder.
@ -156,11 +157,30 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart,
} }
items = append(items, item) items = append(items, item)
} else { } else {
// If a single value is invalid should we give up if strings.Contains(value, ",") {
// or set a zero value? values := strings.Split(value, ",")
for _, value := range values {
if value == "" {
if d.zeroEmpty {
items = append(items, reflect.Zero(elemT))
}
} else if item := conv(value); item.IsValid() {
if isPtrElem {
ptr := reflect.New(elemT)
ptr.Elem().Set(item)
item = ptr
}
items = append(items, item)
} else {
return ConversionError{path, key} return ConversionError{path, key}
} }
} }
} else {
return ConversionError{path, key}
}
}
}
value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...) value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...)
v.Set(value) v.Set(value)
} else { } else {

View file

@ -1036,3 +1036,44 @@ func TestAllNT(t *testing.T) {
} }
} }
} }
// ----------------------------------------------------------------------------
type S12A struct {
ID []int
}
func TestCSVSlice(t *testing.T) {
data := map[string][]string{
"ID": {"0,1"},
}
s := S12A{}
NewDecoder().Decode(&s, data)
if len(s.ID) != 2 {
t.Errorf("Expected two values in the result list, got %+v", s.ID)
}
if s.ID[0] != 0 || s.ID[1] != 1 {
t.Errorf("Expected []{0, 1} got %+v", s)
}
}
type S12B struct {
ID []string
}
//Decode should not split on , into a slice for string only
func TestCSVStringSlice(t *testing.T) {
data := map[string][]string{
"ID": {"0,1"},
}
s := S12B{}
NewDecoder().Decode(&s, data)
if len(s.ID) != 1 {
t.Errorf("Expected one value in the result list, got %+v", s.ID)
}
if s.ID[0] != "0,1" {
t.Errorf("Expected []{0, 1} got %+v", s)
}
}

View file

@ -61,6 +61,8 @@ code still exists in here.
* Dan Sosedoff (sosedoff) * Dan Sosedoff (sosedoff)
* Daniel Farina (fdr) * Daniel Farina (fdr)
* Eric Chlebek (echlebek) * Eric Chlebek (echlebek)
* Eric Garrido (minusnine)
* Eric Urban (hydrogen18)
* Everyone at The Go Team * Everyone at The Go Team
* Evan Shaw (edsrzf) * Evan Shaw (edsrzf)
* Ewan Chou (coocood) * Ewan Chou (coocood)
@ -94,5 +96,6 @@ code still exists in here.
* Ryan Smith (ryandotsmith) * Ryan Smith (ryandotsmith)
* Samuel Stauffer (samuel) * Samuel Stauffer (samuel)
* Timothée Peignier (cyberdelia) * Timothée Peignier (cyberdelia)
* Travis Cline (tmc)
* TruongSinh Tran-Nguyen (truongsinh) * TruongSinh Tran-Nguyen (truongsinh)
* notedit (notedit) * notedit (notedit)

View file

@ -7,7 +7,6 @@ import (
"bytes" "bytes"
"database/sql" "database/sql"
"database/sql/driver" "database/sql/driver"
"github.com/lib/pq/oid"
"io" "io"
"math/rand" "math/rand"
"net" "net"
@ -17,6 +16,8 @@ import (
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/lib/pq/oid"
) )
var ( var (

View file

@ -3,6 +3,7 @@ package pq
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"github.com/lib/pq/oid" "github.com/lib/pq/oid"
) )

View file

@ -10,7 +10,6 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/lib/pq/oid"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
@ -22,6 +21,8 @@ import (
"strings" "strings"
"time" "time"
"unicode" "unicode"
"github.com/lib/pq/oid"
) )
// Common error types // Common error types
@ -212,17 +213,17 @@ func DialOpen(d Dialer, name string) (_ driver.Conn, err error) {
cn.buf = bufio.NewReader(cn.c) cn.buf = bufio.NewReader(cn.c)
cn.startup(o) cn.startup(o)
// reset the deadline, in case one was set (see dial) // reset the deadline, in case one was set (see dial)
if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" {
err = cn.c.SetDeadline(time.Time{}) err = cn.c.SetDeadline(time.Time{})
}
return cn, err return cn, err
} }
func dial(d Dialer, o values) (net.Conn, error) { func dial(d Dialer, o values) (net.Conn, error) {
ntw, addr := network(o) ntw, addr := network(o)
timeout := o.Get("connect_timeout")
// Zero or not specified means wait indefinitely. // Zero or not specified means wait indefinitely.
if timeout != "" && timeout != "0" { if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" {
seconds, err := strconv.ParseInt(timeout, 10, 0) seconds, err := strconv.ParseInt(timeout, 10, 0)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid value for parameter connect_timeout: %s", err) return nil, fmt.Errorf("invalid value for parameter connect_timeout: %s", err)
@ -436,6 +437,9 @@ func (cn *conn) Commit() (err error) {
_, commandTag, err := cn.simpleExec("COMMIT") _, commandTag, err := cn.simpleExec("COMMIT")
if err != nil { if err != nil {
if cn.isInTransaction() {
cn.bad = true
}
return err return err
} }
if commandTag != "COMMIT" { if commandTag != "COMMIT" {
@ -455,6 +459,9 @@ func (cn *conn) Rollback() (err error) {
cn.checkIsInTransaction(true) cn.checkIsInTransaction(true)
_, commandTag, err := cn.simpleExec("ROLLBACK") _, commandTag, err := cn.simpleExec("ROLLBACK")
if err != nil { if err != nil {
if cn.isInTransaction() {
cn.bad = true
}
return err return err
} }
if commandTag != "ROLLBACK" { if commandTag != "ROLLBACK" {

View file

@ -5,8 +5,9 @@ In most cases clients will use the database/sql package instead of
using this package directly. For example: using this package directly. For example:
import ( import (
_ "github.com/lib/pq"
"database/sql" "database/sql"
_ "github.com/lib/pq"
) )
func main() { func main() {

View file

@ -5,12 +5,13 @@ import (
"database/sql/driver" "database/sql/driver"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/lib/pq/oid"
"math" "math"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/lib/pq/oid"
) )
func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte { func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte {
@ -149,12 +150,6 @@ func appendEscapedText(buf []byte, text string) []byte {
func mustParse(f string, typ oid.Oid, s []byte) time.Time { func mustParse(f string, typ oid.Oid, s []byte) time.Time {
str := string(s) str := string(s)
// Special case until time.Parse bug is fixed:
// http://code.google.com/p/go/issues/detail?id=3487
if str[len(str)-2] == '.' {
str += "0"
}
// check for a 30-minute-offset timezone // check for a 30-minute-offset timezone
if (typ == oid.T_timestamptz || typ == oid.T_timetz) && if (typ == oid.T_timestamptz || typ == oid.T_timetz) &&
str[len(str)-3] == ':' { str[len(str)-3] == ':' {
@ -212,11 +207,72 @@ func (c *locationCache) getLocation(offset int) *time.Location {
return location return location
} }
var infinityTsEnabled = false
var infinityTsNegative time.Time
var infinityTsPositive time.Time
const (
infinityTsEnabledAlready = "pq: infinity timestamp enabled already"
infinityTsNegativeMustBeSmaller = "pq: infinity timestamp: negative value must be smaller (before) than positive"
)
/*
* If EnableInfinityTs is not called, "-infinity" and "infinity" will return
* []byte("-infinity") and []byte("infinity") respectively, and potentially
* cause error "sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time",
* when scanning into a time.Time value.
*
* Once EnableInfinityTs has been called, all connections created using this
* driver will decode Postgres' "-infinity" and "infinity" for "timestamp",
* "timestamp with time zone" and "date" types to the predefined minimum and
* maximum times, respectively. When encoding time.Time values, any time which
* equals or preceeds the predefined minimum time will be encoded to
* "-infinity". Any values at or past the maximum time will similarly be
* encoded to "infinity".
*
*
* If EnableInfinityTs is called with negative >= positive, it will panic.
* Calling EnableInfinityTs after a connection has been established results in
* undefined behavior. If EnableInfinityTs is called more than once, it will
* panic.
*/
func EnableInfinityTs(negative time.Time, positive time.Time) {
if infinityTsEnabled {
panic(infinityTsEnabledAlready)
}
if !negative.Before(positive) {
panic(infinityTsNegativeMustBeSmaller)
}
infinityTsEnabled = true
infinityTsNegative = negative
infinityTsPositive = positive
}
/*
* Testing might want to toggle infinityTsEnabled
*/
func disableInfinityTs() {
infinityTsEnabled = false
}
// This is a time function specific to the Postgres default DateStyle // This is a time function specific to the Postgres default DateStyle
// setting ("ISO, MDY"), the only one we currently support. This // setting ("ISO, MDY"), the only one we currently support. This
// accounts for the discrepancies between the parsing available with // accounts for the discrepancies between the parsing available with
// time.Parse and the Postgres date formatting quirks. // time.Parse and the Postgres date formatting quirks.
func parseTs(currentLocation *time.Location, str string) (result time.Time) { func parseTs(currentLocation *time.Location, str string) interface{} {
switch str {
case "-infinity":
if infinityTsEnabled {
return infinityTsNegative
}
return []byte(str)
case "infinity":
if infinityTsEnabled {
return infinityTsPositive
}
return []byte(str)
}
monSep := strings.IndexRune(str, '-') monSep := strings.IndexRune(str, '-')
// this is Gregorian year, not ISO Year // this is Gregorian year, not ISO Year
// In Gregorian system, the year 1 BC is followed by AD 1 // In Gregorian system, the year 1 BC is followed by AD 1
@ -310,10 +366,18 @@ func parseTs(currentLocation *time.Location, str string) (result time.Time) {
return t return t
} }
// formatTs formats t as time.RFC3339Nano and appends time zone seconds if // formatTs formats t into a format postgres understands.
// needed.
func formatTs(t time.Time) (b []byte) { func formatTs(t time.Time) (b []byte) {
b = []byte(t.Format(time.RFC3339Nano)) if infinityTsEnabled {
// t <= -infinity : ! (t > -infinity)
if !t.After(infinityTsNegative) {
return []byte("-infinity")
}
// t >= infinity : ! (!t < infinity)
if !t.Before(infinityTsPositive) {
return []byte("infinity")
}
}
// Need to send dates before 0001 A.D. with " BC" suffix, instead of the // Need to send dates before 0001 A.D. with " BC" suffix, instead of the
// minus sign preferred by Go. // minus sign preferred by Go.
// Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on // Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on
@ -324,16 +388,11 @@ func formatTs(t time.Time) (b []byte) {
bc = true bc = true
} }
b = []byte(t.Format(time.RFC3339Nano)) b = []byte(t.Format(time.RFC3339Nano))
if bc {
b = append(b, " BC"...)
}
_, offset := t.Zone() _, offset := t.Zone()
offset = offset % 60 offset = offset % 60
if offset == 0 { if offset != 0 {
return b // RFC3339Nano already printed the minus sign
}
if offset < 0 { if offset < 0 {
offset = -offset offset = -offset
} }
@ -342,7 +401,13 @@ func formatTs(t time.Time) (b []byte) {
if offset < 10 { if offset < 10 {
b = append(b, '0') b = append(b, '0')
} }
return strconv.AppendInt(b, int64(offset), 10) b = strconv.AppendInt(b, int64(offset), 10)
}
if bc {
b = append(b, " BC"...)
}
return b
} }
// Parse a bytea value received from the server. Both "hex" and the legacy // Parse a bytea value received from the server. Both "hex" and the legacy
@ -397,7 +462,10 @@ func parseBytea(s []byte) (result []byte) {
func encodeBytea(serverVersion int, v []byte) (result []byte) { func encodeBytea(serverVersion int, v []byte) (result []byte) {
if serverVersion >= 90000 { if serverVersion >= 90000 {
// Use the hex format if we know that the server supports it // Use the hex format if we know that the server supports it
result = []byte(fmt.Sprintf("\\x%x", v)) result = make([]byte, 2+hex.EncodedLen(len(v)))
result[0] = '\\'
result[1] = 'x'
hex.Encode(result[2:], v)
} else { } else {
// .. or resort to "escape" // .. or resort to "escape"
for _, b := range v { for _, b := range v {

View file

@ -1,12 +1,12 @@
package pq package pq
import ( import (
"github.com/lib/pq/oid"
"bytes" "bytes"
"fmt" "fmt"
"testing" "testing"
"time" "time"
"github.com/lib/pq/oid"
) )
func TestScanTimestamp(t *testing.T) { func TestScanTimestamp(t *testing.T) {
@ -78,7 +78,11 @@ func tryParse(str string) (t time.Time, err error) {
return return
} }
}() }()
t = parseTs(nil, str) i := parseTs(nil, str)
t, ok := i.(time.Time)
if !ok {
err = fmt.Errorf("Not a time.Time type, got %#v", i)
}
return return
} }
@ -132,8 +136,18 @@ var formatTimeTests = []struct {
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "2001-02-03T04:05:06.123456789Z"}, {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "2001-02-03T04:05:06.123456789Z"},
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "2001-02-03T04:05:06.123456789+02:00"}, {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "2001-02-03T04:05:06.123456789+02:00"},
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "2001-02-03T04:05:06.123456789-06:00"}, {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "2001-02-03T04:05:06.123456789-06:00"},
{time.Date(1, time.January, 1, 0, 0, 0, 0, time.FixedZone("", 19*60+32)), "0001-01-01T00:00:00+00:19:32"},
{time.Date(2001, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "2001-02-03T04:05:06-07:30:09"}, {time.Date(2001, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "2001-02-03T04:05:06-07:30:09"},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03T04:05:06.123456789Z"},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03T04:05:06.123456789+02:00"},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03T04:05:06.123456789-06:00"},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03T04:05:06.123456789Z BC"},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03T04:05:06.123456789+02:00 BC"},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03T04:05:06.123456789-06:00 BC"},
{time.Date(1, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03T04:05:06-07:30:09"},
{time.Date(0, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03T04:05:06-07:30:09 BC"},
} }
func TestFormatTs(t *testing.T) { func TestFormatTs(t *testing.T) {
@ -249,6 +263,131 @@ func TestTimestampWithOutTimezone(t *testing.T) {
test("2013-01-04T20:14:58.80033Z", "2013-01-04 20:14:58.80033") test("2013-01-04T20:14:58.80033Z", "2013-01-04 20:14:58.80033")
} }
func TestInfinityTimestamp(t *testing.T) {
db := openTestConn(t)
defer db.Close()
var err error
var resultT time.Time
expectedError := fmt.Errorf(`sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time`)
type testCases []struct {
Query string
Param string
ExpectedErr error
ExpectedVal interface{}
}
tc := testCases{
{"SELECT $1::timestamp", "-infinity", expectedError, "-infinity"},
{"SELECT $1::timestamptz", "-infinity", expectedError, "-infinity"},
{"SELECT $1::timestamp", "infinity", expectedError, "infinity"},
{"SELECT $1::timestamptz", "infinity", expectedError, "infinity"},
}
// try to assert []byte to time.Time
for _, q := range tc {
err = db.QueryRow(q.Query, q.Param).Scan(&resultT)
if err.Error() != q.ExpectedErr.Error() {
t.Errorf("Scanning -/+infinity, expected error, %q, got %q", q.ExpectedErr, err)
}
}
// yield []byte
for _, q := range tc {
var resultI interface{}
err = db.QueryRow(q.Query, q.Param).Scan(&resultI)
if err != nil {
t.Errorf("Scanning -/+infinity, expected no error, got %q", err)
}
result, ok := resultI.([]byte)
if !ok {
t.Errorf("Scanning -/+infinity, expected []byte, got %#v", resultI)
}
if string(result) != q.ExpectedVal {
t.Errorf("Scanning -/+infinity, expected %q, got %q", q.ExpectedVal, result)
}
}
y1500 := time.Date(1500, time.January, 1, 0, 0, 0, 0, time.UTC)
y2500 := time.Date(2500, time.January, 1, 0, 0, 0, 0, time.UTC)
EnableInfinityTs(y1500, y2500)
err = db.QueryRow("SELECT $1::timestamp", "infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning infinity, expected no error, got %q", err)
}
if !resultT.Equal(y2500) {
t.Errorf("Scanning infinity, expected %q, got %q", y2500, resultT)
}
err = db.QueryRow("SELECT $1::timestamptz", "infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning infinity, expected no error, got %q", err)
}
if !resultT.Equal(y2500) {
t.Errorf("Scanning Infinity, expected time %q, got %q", y2500, resultT.String())
}
err = db.QueryRow("SELECT $1::timestamp", "-infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning -infinity, expected no error, got %q", err)
}
if !resultT.Equal(y1500) {
t.Errorf("Scanning -infinity, expected time %q, got %q", y1500, resultT.String())
}
err = db.QueryRow("SELECT $1::timestamptz", "-infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning -infinity, expected no error, got %q", err)
}
if !resultT.Equal(y1500) {
t.Errorf("Scanning -infinity, expected time %q, got %q", y1500, resultT.String())
}
y_1500 := time.Date(-1500, time.January, 1, 0, 0, 0, 0, time.UTC)
y11500 := time.Date(11500, time.January, 1, 0, 0, 0, 0, time.UTC)
var s string
err = db.QueryRow("SELECT $1::timestamp::text", y_1500).Scan(&s)
if err != nil {
t.Errorf("Encoding -infinity, expected no error, got %q", err)
}
if s != "-infinity" {
t.Errorf("Encoding -infinity, expected %q, got %q", "-infinity", s)
}
err = db.QueryRow("SELECT $1::timestamptz::text", y_1500).Scan(&s)
if err != nil {
t.Errorf("Encoding -infinity, expected no error, got %q", err)
}
if s != "-infinity" {
t.Errorf("Encoding -infinity, expected %q, got %q", "-infinity", s)
}
err = db.QueryRow("SELECT $1::timestamp::text", y11500).Scan(&s)
if err != nil {
t.Errorf("Encoding infinity, expected no error, got %q", err)
}
if s != "infinity" {
t.Errorf("Encoding infinity, expected %q, got %q", "infinity", s)
}
err = db.QueryRow("SELECT $1::timestamptz::text", y11500).Scan(&s)
if err != nil {
t.Errorf("Encoding infinity, expected no error, got %q", err)
}
if s != "infinity" {
t.Errorf("Encoding infinity, expected %q, got %q", "infinity", s)
}
disableInfinityTs()
var panicErrorString string
func() {
defer func() {
panicErrorString, _ = recover().(string)
}()
EnableInfinityTs(y2500, y1500)
}()
if panicErrorString != infinityTsNegativeMustBeSmaller {
t.Errorf("Expected error, %q, got %q", infinityTsNegativeMustBeSmaller, panicErrorString)
}
}
func TestStringWithNul(t *testing.T) { func TestStringWithNul(t *testing.T) {
db := openTestConn(t) db := openTestConn(t)
defer db.Close() defer db.Close()

View file

@ -2,9 +2,10 @@ package hstore
import ( import (
"database/sql" "database/sql"
_ "github.com/lib/pq"
"os" "os"
"testing" "testing"
_ "github.com/lib/pq"
) )
type Fatalistic interface { type Fatalistic interface {

View file

@ -18,11 +18,11 @@ mechanism to avoid polling the database while waiting for more work to arrive.
package main package main
import ( import (
"github.com/lib/pq"
"database/sql" "database/sql"
"fmt" "fmt"
"time" "time"
"github.com/lib/pq"
) )
func doWork(db *sql.DB, work int64) { func doWork(db *sql.DB, work int64) {

View file

@ -43,6 +43,7 @@ func expectEvent(t *testing.T, eventch <-chan ListenerEventType, et ListenerEven
} }
return nil return nil
case <-time.After(1500 * time.Millisecond): case <-time.After(1500 * time.Millisecond):
panic("expectEvent timeout")
return fmt.Errorf("timeout") return fmt.Errorf("timeout")
} }
} }

View file

@ -5,12 +5,12 @@
package main package main
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"database/sql"
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )

View file

@ -2,6 +2,23 @@
golang + bacteria = bactdb golang + bacteria = bactdb
NAME:
bactdb - a database for bacteria
This is a work in progress --- please stay tuned. USAGE:
bactdb [global options] command [command options] [arguments...]
VERSION:
0.1.0
AUTHOR(S):
Matthew Ryan Dillon <mrdillon@alaska.edu>
COMMANDS:
serve, s Start web server
migrate, m Migrate the database schema
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--help, -h show help
--version, -v print the version

View file

@ -1,101 +0,0 @@
package api
import (
"errors"
"net/http"
"os"
"strings"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/mux"
)
const (
tokenName = "AccessToken"
)
var (
verifyKey, signKey []byte
errWhileSigningToken = errors.New("error while signing token")
errPleaseLogIn = errors.New("please log in")
errWhileParsingCookie = errors.New("error while parsing cookie")
errTokenExpired = errors.New("token expired")
errGenericError = errors.New("generic error")
errAccessDenied = errors.New("insufficient privileges")
)
func SetupCerts() error {
signkey := os.Getenv("PRIVATE_KEY")
if signkey == "" {
return errors.New("please set PRIVATE_KEY")
}
signKey = []byte(signkey)
verifykey := os.Getenv("PUBLIC_KEY")
if verifykey == "" {
return errors.New("please set PUBLIC_KEY")
}
verifyKey = []byte(verifykey)
return nil
}
type authHandler func(http.ResponseWriter, *http.Request) error
// Only accessible with a valid token
func (h authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Even though writeJSON sets the content type, we need to set it here because
// calls to WriteHeader write out the entire header.
w.Header().Set("content-type", "application/json; charset=utf-8")
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
w.WriteHeader(http.StatusUnauthorized)
writeJSON(w, Error{errPleaseLogIn})
return
}
s := strings.Split(authHeader, " ")
// Validate the token
token, err := jwt.Parse(s[1], func(token *jwt.Token) (interface{}, error) {
return []byte(verifyKey), nil
})
// Branch out into the possible error from signing
switch err.(type) {
case nil: // No error
if !token.Valid { // But may still be invalid
w.WriteHeader(http.StatusUnauthorized)
writeJSON(w, Error{errPleaseLogIn})
return
}
case *jwt.ValidationError: // Something was wrong during the validation
vErr := err.(*jwt.ValidationError)
switch vErr.Errors {
case jwt.ValidationErrorExpired:
w.WriteHeader(http.StatusUnauthorized)
writeJSON(w, Error{errTokenExpired})
return
default:
w.WriteHeader(http.StatusInternalServerError)
writeJSON(w, Error{errGenericError})
return
}
default: // Something else went wrong
w.WriteHeader(http.StatusInternalServerError)
writeJSON(w, Error{errGenericError})
return
}
genus := mux.Vars(r)["genus"]
// We don't care about this if we aren't accessing one of the subrouter routes.
if genus != "" && genus != token.Claims["genus"] {
w.WriteHeader(http.StatusInternalServerError)
writeJSON(w, Error{errAccessDenied})
return
}
hErr := h(w, r)
if hErr != nil {
w.WriteHeader(http.StatusInternalServerError)
writeJSON(w, Error{hErr})
}
}

View file

@ -1,92 +0,0 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/thermokarst/bactdb/models"
)
func serveCharacteristicType(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
if err != nil {
return err
}
characteristic_type, err := store.CharacteristicTypes.Get(id)
if err != nil {
return err
}
return writeJSON(w, characteristic_type)
}
func serveCreateCharacteristicType(w http.ResponseWriter, r *http.Request) error {
var characteristic_type models.CharacteristicType
err := json.NewDecoder(r.Body).Decode(&characteristic_type)
if err != nil {
return err
}
created, err := store.CharacteristicTypes.Create(&characteristic_type)
if err != nil {
return err
}
if created {
w.WriteHeader(http.StatusCreated)
}
return writeJSON(w, characteristic_type)
}
func serveCharacteristicTypeList(w http.ResponseWriter, r *http.Request) error {
var opt models.CharacteristicTypeListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
characteristic_types, err := store.CharacteristicTypes.List(&opt)
if err != nil {
return err
}
if characteristic_types == nil {
characteristic_types = []*models.CharacteristicType{}
}
return writeJSON(w, characteristic_types)
}
func serveUpdateCharacteristicType(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
var characteristic_type models.CharacteristicType
err := json.NewDecoder(r.Body).Decode(&characteristic_type)
if err != nil {
return err
}
updated, err := store.CharacteristicTypes.Update(id, &characteristic_type)
if err != nil {
return err
}
if updated {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, characteristic_type)
}
func serveDeleteCharacteristicType(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
deleted, err := store.CharacteristicTypes.Delete(id)
if err != nil {
return err
}
if deleted {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, &models.CharacteristicType{})
}

View file

@ -1,153 +0,0 @@
package api
import (
"testing"
"github.com/thermokarst/bactdb/models"
)
func newCharacteristicType() *models.CharacteristicType {
characteristic_type := models.NewCharacteristicType()
return characteristic_type
}
func TestCharacteristicType_Get(t *testing.T) {
setup()
want := newCharacteristicType()
calledGet := false
store.CharacteristicTypes.(*models.MockCharacteristicTypesService).Get_ = func(id int64) (*models.CharacteristicType, error) {
if id != want.Id {
t.Errorf("wanted request for characteristic_type %d but got %d", want.Id, id)
}
calledGet = true
return want, nil
}
got, err := apiClient.CharacteristicTypes.Get(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledGet {
t.Error("!calledGet")
}
if !normalizeDeepEqual(want, got) {
t.Errorf("got %+v but wanted %+v", got, want)
}
}
func TestCharacteristicType_Create(t *testing.T) {
setup()
want := newCharacteristicType()
calledPost := false
store.CharacteristicTypes.(*models.MockCharacteristicTypesService).Create_ = func(characteristic_type *models.CharacteristicType) (bool, error) {
if !normalizeDeepEqual(want, characteristic_type) {
t.Errorf("wanted request for characteristic_type %d but got %d", want, characteristic_type)
}
calledPost = true
return true, nil
}
success, err := apiClient.CharacteristicTypes.Create(want)
if err != nil {
t.Fatal(err)
}
if !calledPost {
t.Error("!calledPost")
}
if !success {
t.Error("!success")
}
}
func TestCharacteristicType_List(t *testing.T) {
setup()
want := []*models.CharacteristicType{newCharacteristicType()}
wantOpt := &models.CharacteristicTypeListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}
calledList := false
store.CharacteristicTypes.(*models.MockCharacteristicTypesService).List_ = func(opt *models.CharacteristicTypeListOptions) ([]*models.CharacteristicType, error) {
if !normalizeDeepEqual(wantOpt, opt) {
t.Errorf("wanted options %d but got %d", wantOpt, opt)
}
calledList = true
return want, nil
}
characteristic_types, err := apiClient.CharacteristicTypes.List(wantOpt)
if err != nil {
t.Fatal(err)
}
if !calledList {
t.Error("!calledList")
}
if !normalizeDeepEqual(&want, &characteristic_types) {
t.Errorf("got characteristic_types %+v but wanted characteristic_types %+v", characteristic_types, want)
}
}
func TestCharacteristicType_Update(t *testing.T) {
setup()
want := newCharacteristicType()
calledPut := false
store.CharacteristicTypes.(*models.MockCharacteristicTypesService).Update_ = func(id int64, characteristic_type *models.CharacteristicType) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for characteristic_type %d but got %d", want.Id, id)
}
if !normalizeDeepEqual(want, characteristic_type) {
t.Errorf("wanted request for characteristic_type %d but got %d", want, characteristic_type)
}
calledPut = true
return true, nil
}
success, err := apiClient.CharacteristicTypes.Update(want.Id, want)
if err != nil {
t.Fatal(err)
}
if !calledPut {
t.Error("!calledPut")
}
if !success {
t.Error("!success")
}
}
func TestCharacteristicType_Delete(t *testing.T) {
setup()
want := newCharacteristicType()
calledDelete := false
store.CharacteristicTypes.(*models.MockCharacteristicTypesService).Delete_ = func(id int64) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for characteristic_type %d but got %d", want.Id, id)
}
calledDelete = true
return true, nil
}
success, err := apiClient.CharacteristicTypes.Delete(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledDelete {
t.Error("!calledDelete")
}
if !success {
t.Error("!success")
}
}

View file

@ -1,92 +0,0 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/thermokarst/bactdb/models"
)
func serveCharacteristic(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
if err != nil {
return err
}
characteristic, err := store.Characteristics.Get(id)
if err != nil {
return err
}
return writeJSON(w, models.CharacteristicJSON{Characteristic: characteristic})
}
func serveCreateCharacteristic(w http.ResponseWriter, r *http.Request) error {
var characteristic models.CharacteristicJSON
err := json.NewDecoder(r.Body).Decode(&characteristic)
if err != nil {
return err
}
created, err := store.Characteristics.Create(characteristic.Characteristic)
if err != nil {
return err
}
if created {
w.WriteHeader(http.StatusCreated)
}
return writeJSON(w, characteristic)
}
func serveCharacteristicList(w http.ResponseWriter, r *http.Request) error {
var opt models.CharacteristicListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
characteristics, err := store.Characteristics.List(&opt)
if err != nil {
return err
}
if characteristics == nil {
characteristics = []*models.Characteristic{}
}
return writeJSON(w, models.CharacteristicsJSON{Characteristics: characteristics})
}
func serveUpdateCharacteristic(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
var characteristic models.CharacteristicJSON
err := json.NewDecoder(r.Body).Decode(&characteristic)
if err != nil {
return err
}
updated, err := store.Characteristics.Update(id, characteristic.Characteristic)
if err != nil {
return err
}
if updated {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, characteristic)
}
func serveDeleteCharacteristic(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
deleted, err := store.Characteristics.Delete(id)
if err != nil {
return err
}
if deleted {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, nil)
}

View file

@ -1,153 +0,0 @@
package api
import (
"testing"
"github.com/thermokarst/bactdb/models"
)
func newCharacteristic() *models.Characteristic {
characteristic := models.NewCharacteristic()
return characteristic
}
func TestCharacteristic_Get(t *testing.T) {
setup()
want := newCharacteristic()
calledGet := false
store.Characteristics.(*models.MockCharacteristicsService).Get_ = func(id int64) (*models.Characteristic, error) {
if id != want.Id {
t.Errorf("wanted request for characteristic %d but got %d", want.Id, id)
}
calledGet = true
return want, nil
}
got, err := apiClient.Characteristics.Get(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledGet {
t.Error("!calledGet")
}
if !normalizeDeepEqual(want, got) {
t.Errorf("got %+v but wanted %+v", got, want)
}
}
func TestCharacteristic_Create(t *testing.T) {
setup()
want := newCharacteristic()
calledPost := false
store.Characteristics.(*models.MockCharacteristicsService).Create_ = func(characteristic *models.Characteristic) (bool, error) {
if !normalizeDeepEqual(want, characteristic) {
t.Errorf("wanted request for characteristic %d but got %d", want, characteristic)
}
calledPost = true
return true, nil
}
success, err := apiClient.Characteristics.Create(want)
if err != nil {
t.Fatal(err)
}
if !calledPost {
t.Error("!calledPost")
}
if !success {
t.Error("!success")
}
}
func TestCharacteristic_List(t *testing.T) {
setup()
want := []*models.Characteristic{newCharacteristic()}
wantOpt := &models.CharacteristicListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}
calledList := false
store.Characteristics.(*models.MockCharacteristicsService).List_ = func(opt *models.CharacteristicListOptions) ([]*models.Characteristic, error) {
if !normalizeDeepEqual(wantOpt, opt) {
t.Errorf("wanted options %d but got %d", wantOpt, opt)
}
calledList = true
return want, nil
}
characteristics, err := apiClient.Characteristics.List(wantOpt)
if err != nil {
t.Fatal(err)
}
if !calledList {
t.Error("!calledList")
}
if !normalizeDeepEqual(&want, &characteristics) {
t.Errorf("got characteristics %+v but wanted characteristics %+v", characteristics, want)
}
}
func TestCharacteristic_Update(t *testing.T) {
setup()
want := newCharacteristic()
calledPut := false
store.Characteristics.(*models.MockCharacteristicsService).Update_ = func(id int64, characteristic *models.Characteristic) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for characteristic %d but got %d", want.Id, id)
}
if !normalizeDeepEqual(want, characteristic) {
t.Errorf("wanted request for characteristic %d but got %d", want, characteristic)
}
calledPut = true
return true, nil
}
success, err := apiClient.Characteristics.Update(want.Id, want)
if err != nil {
t.Fatal(err)
}
if !calledPut {
t.Error("!calledPut")
}
if !success {
t.Error("!success")
}
}
func TestCharacteristic_Delete(t *testing.T) {
setup()
want := newCharacteristic()
calledDelete := false
store.Characteristics.(*models.MockCharacteristicsService).Delete_ = func(id int64) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for characteristic %d but got %d", want.Id, id)
}
calledDelete = true
return true, nil
}
success, err := apiClient.Characteristics.Delete(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledDelete {
t.Error("!calledDelete")
}
if !success {
t.Error("!success")
}
}

View file

@ -1,94 +0,0 @@
package api
import (
"encoding/json"
"strconv"
"github.com/gorilla/mux"
"net/http"
"github.com/thermokarst/bactdb/models"
)
func serveGenus(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
if err != nil {
return err
}
genus, err := store.Genera.Get(id)
if err != nil {
return err
}
return writeJSON(w, models.GenusJSON{Genus: genus})
}
func serveCreateGenus(w http.ResponseWriter, r *http.Request) error {
var genus models.GenusJSON
err := json.NewDecoder(r.Body).Decode(&genus)
if err != nil {
return err
}
created, err := store.Genera.Create(genus.Genus)
if err != nil {
return err
}
if created {
w.WriteHeader(http.StatusCreated)
}
return writeJSON(w, genus)
}
func serveGenera(w http.ResponseWriter, r *http.Request) error {
var opt models.GenusListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
genera, err := store.Genera.List(&opt)
if err != nil {
return err
}
if genera == nil {
genera = []*models.Genus{}
}
return writeJSON(w, models.GeneraJSON{Genera: genera})
}
func serveUpdateGenus(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
var genus models.GenusJSON
err := json.NewDecoder(r.Body).Decode(&genus)
if err != nil {
return err
}
updated, err := store.Genera.Update(id, genus.Genus)
if err != nil {
return err
}
if updated {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, genus)
}
func serveDeleteGenus(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
deleted, err := store.Genera.Delete(id)
if err != nil {
return err
}
if deleted {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, nil)
}

View file

@ -1,152 +0,0 @@
package api
import (
"testing"
"github.com/thermokarst/bactdb/models"
)
func newGenus() *models.Genus {
genus := models.NewGenus()
genus.Id = 1
return genus
}
func TestGenus_Get(t *testing.T) {
setup()
want := newGenus()
calledGet := false
store.Genera.(*models.MockGeneraService).Get_ = func(id int64) (*models.Genus, error) {
if id != want.Id {
t.Errorf("wanted request for genus %d but got %d", want.Id, id)
}
calledGet = true
return want, nil
}
got, err := apiClient.Genera.Get(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledGet {
t.Error("!calledGet")
}
if !normalizeDeepEqual(want, got) {
t.Errorf("got genus %+v but wanted genus %+v", got, want)
}
}
func TestGenus_Create(t *testing.T) {
setup()
want := newGenus()
calledPost := false
store.Genera.(*models.MockGeneraService).Create_ = func(genus *models.Genus) (bool, error) {
if !normalizeDeepEqual(want, genus) {
t.Errorf("wanted request for genus %d but got %d", want, genus)
}
calledPost = true
return true, nil
}
success, err := apiClient.Genera.Create(want)
if err != nil {
t.Fatal(err)
}
if !calledPost {
t.Error("!calledPost")
}
if !success {
t.Error("!success")
}
}
func TestGenus_List(t *testing.T) {
setup()
want := []*models.Genus{newGenus()}
wantOpt := &models.GenusListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}
calledList := false
store.Genera.(*models.MockGeneraService).List_ = func(opt *models.GenusListOptions) ([]*models.Genus, error) {
if !normalizeDeepEqual(wantOpt, opt) {
t.Errorf("wanted options %d but got %d", wantOpt, opt)
}
calledList = true
return want, nil
}
genera, err := apiClient.Genera.List(wantOpt)
if err != nil {
t.Fatal(err)
}
if !calledList {
t.Error("!calledList")
}
if !normalizeDeepEqual(&want, &genera) {
t.Errorf("got genera %+v but wanted genera %+v", genera, want)
}
}
func TestGenus_Update(t *testing.T) {
setup()
want := newGenus()
calledPut := false
store.Genera.(*models.MockGeneraService).Update_ = func(id int64, genus *models.Genus) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for genus %d but got %d", want.Id, id)
}
if !normalizeDeepEqual(want, genus) {
t.Errorf("wanted request for genus %d but got %d", want, genus)
}
calledPut = true
return true, nil
}
success, err := apiClient.Genera.Update(1, want)
if err != nil {
t.Fatal(err)
}
if !calledPut {
t.Error("!calledPut")
}
if !success {
t.Error("!success")
}
}
func TestGenus_Delete(t *testing.T) {
setup()
want := newGenus()
calledDelete := false
store.Genera.(*models.MockGeneraService).Delete_ = func(id int64) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for genus %d but got %d", want.Id, id)
}
calledDelete = true
return true, nil
}
success, err := apiClient.Genera.Delete(1)
if err != nil {
t.Fatal(err)
}
if !calledDelete {
t.Error("!calledDelete")
}
if !success {
t.Error("!success")
}
}

View file

@ -1,94 +0,0 @@
package api
import (
"net/http"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/thermokarst/bactdb/datastore"
"github.com/thermokarst/bactdb/router"
)
var (
store = datastore.NewDatastore(nil)
schemaDecoder = schema.NewDecoder()
)
func Handler() *mux.Router {
m := router.API()
m.Get(router.User).Handler(authHandler(serveUser))
m.Get(router.CreateUser).Handler(authHandler(serveCreateUser))
m.Get(router.Users).Handler(authHandler(serveUsers))
m.Get(router.GetToken).Handler(handler(serveAuthenticateUser))
m.Get(router.Genus).Handler(authHandler(serveGenus))
m.Get(router.CreateGenus).Handler(authHandler(serveCreateGenus))
m.Get(router.Genera).Handler(authHandler(serveGenera))
m.Get(router.UpdateGenus).Handler(authHandler(serveUpdateGenus))
m.Get(router.DeleteGenus).Handler(authHandler(serveDeleteGenus))
m.Get(router.Species).Handler(authHandler(serveSpecies))
m.Get(router.CreateSpecies).Handler(authHandler(serveCreateSpecies))
m.Get(router.SpeciesList).Handler(authHandler(serveSpeciesList))
m.Get(router.UpdateSpecies).Handler(authHandler(serveUpdateSpecies))
m.Get(router.DeleteSpecies).Handler(authHandler(serveDeleteSpecies))
m.Get(router.Strain).Handler(authHandler(serveStrain))
m.Get(router.CreateStrain).Handler(authHandler(serveCreateStrain))
m.Get(router.Strains).Handler(authHandler(serveStrainList))
m.Get(router.UpdateStrain).Handler(authHandler(serveUpdateStrain))
m.Get(router.DeleteStrain).Handler(authHandler(serveDeleteStrain))
m.Get(router.CharacteristicType).Handler(authHandler(serveCharacteristicType))
m.Get(router.CreateCharacteristicType).Handler(authHandler(serveCreateCharacteristicType))
m.Get(router.CharacteristicTypes).Handler(authHandler(serveCharacteristicTypeList))
m.Get(router.UpdateCharacteristicType).Handler(authHandler(serveUpdateCharacteristicType))
m.Get(router.DeleteCharacteristicType).Handler(authHandler(serveDeleteCharacteristicType))
m.Get(router.Characteristic).Handler(authHandler(serveCharacteristic))
m.Get(router.CreateCharacteristic).Handler(authHandler(serveCreateCharacteristic))
m.Get(router.Characteristics).Handler(authHandler(serveCharacteristicList))
m.Get(router.UpdateCharacteristic).Handler(authHandler(serveUpdateCharacteristic))
m.Get(router.DeleteCharacteristic).Handler(authHandler(serveDeleteCharacteristic))
m.Get(router.TextMeasurementType).Handler(authHandler(serveTextMeasurementType))
m.Get(router.CreateTextMeasurementType).Handler(authHandler(serveCreateTextMeasurementType))
m.Get(router.TextMeasurementTypes).Handler(authHandler(serveTextMeasurementTypeList))
m.Get(router.UpdateTextMeasurementType).Handler(authHandler(serveUpdateTextMeasurementType))
m.Get(router.DeleteTextMeasurementType).Handler(authHandler(serveDeleteTextMeasurementType))
m.Get(router.UnitType).Handler(authHandler(serveUnitType))
m.Get(router.CreateUnitType).Handler(authHandler(serveCreateUnitType))
m.Get(router.UnitTypes).Handler(authHandler(serveUnitTypeList))
m.Get(router.UpdateUnitType).Handler(authHandler(serveUpdateUnitType))
m.Get(router.DeleteUnitType).Handler(authHandler(serveDeleteUnitType))
m.Get(router.Measurement).Handler(authHandler(serveMeasurement))
m.Get(router.CreateMeasurement).Handler(authHandler(serveCreateMeasurement))
m.Get(router.Measurements).Handler(authHandler(serveMeasurementList))
m.Get(router.UpdateMeasurement).Handler(authHandler(serveUpdateMeasurement))
m.Get(router.DeleteMeasurement).Handler(authHandler(serveDeleteMeasurement))
m.Get(router.SubrouterListSpecies).Handler(authHandler(serveSubrouterSpeciesList))
m.Get(router.SubrouterListStrains).Handler(authHandler(serveSubrouterStrainsList))
m.Get(router.SubrouterListMeasurements).Handler(authHandler(serveSubrouterMeasurementsList))
m.Get(router.Health).Handler(handler(healthHandler))
return m
}
type handler func(http.ResponseWriter, *http.Request) error
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := h(w, r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
writeJSON(w, Error{err})
}
}
func healthHandler(w http.ResponseWriter, r *http.Request) error {
return writeJSON(w, Message{"great success"})
}

View file

@ -1,37 +0,0 @@
package api
import (
"encoding/json"
"log"
"net/http"
)
// writeJSON writes a JSON Content-Type header and a JSON-encoded object to
// the http.ResponseWriter.
func writeJSON(w http.ResponseWriter, v interface{}) error {
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
w.Header().Set("content-type", "application/json; charset=utf-8")
_, err = w.Write(data)
return err
}
// Message is for returning simple message payloads to the user
type Message struct {
Message string `json:"message"`
}
// Error is for returning simple error payloads to the user, as well as logging
type Error struct {
Error error
}
func (e Error) MarshalJSON() ([]byte, error) {
log.Println(e.Error)
return json.Marshal(struct {
Error string `json:"error"`
}{e.Error.Error()})
}

View file

@ -1,25 +0,0 @@
package api
import (
"encoding/json"
"fmt"
"reflect"
)
// Helper function that normalizes structs for comparison with reflect.DeepEqual
func normalize(v interface{}) {
j, err := json.Marshal(v)
if err != nil {
panic(fmt.Sprintf("Could not normalize object %+v due to JSON marshalling error: %s", v, err))
}
err = json.Unmarshal(j, v)
if err != nil {
panic(fmt.Sprintf("Could not normalize object %+v due to JSON un-marshalling error: %s", v, err))
}
}
func normalizeDeepEqual(u, v interface{}) bool {
normalize(u)
normalize(v)
return reflect.DeepEqual(u, v)
}

View file

@ -1,111 +0,0 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/thermokarst/bactdb/models"
)
func serveMeasurement(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
if err != nil {
return err
}
measurement, err := store.Measurements.Get(id)
if err != nil {
return err
}
return writeJSON(w, models.MeasurementJSON{Measurement: measurement})
}
func serveCreateMeasurement(w http.ResponseWriter, r *http.Request) error {
var measurement models.MeasurementJSON
err := json.NewDecoder(r.Body).Decode(&measurement)
if err != nil {
return err
}
created, err := store.Measurements.Create(measurement.Measurement)
if err != nil {
return err
}
if created {
w.WriteHeader(http.StatusCreated)
}
return writeJSON(w, measurement)
}
func serveMeasurementList(w http.ResponseWriter, r *http.Request) error {
var opt models.MeasurementListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
measurements, err := store.Measurements.List(&opt)
if err != nil {
return err
}
if measurements == nil {
measurements = []*models.Measurement{}
}
return writeJSON(w, models.MeasurementsJSON{Measurements: measurements})
}
func serveUpdateMeasurement(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
var measurement models.MeasurementJSON
err := json.NewDecoder(r.Body).Decode(&measurement)
if err != nil {
return err
}
updated, err := store.Measurements.Update(id, measurement.Measurement)
if err != nil {
return err
}
if updated {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, measurement)
}
func serveDeleteMeasurement(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
deleted, err := store.Measurements.Delete(id)
if err != nil {
return err
}
if deleted {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, nil)
}
func serveSubrouterMeasurementsList(w http.ResponseWriter, r *http.Request) error {
var opt models.MeasurementListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
opt.Genus = mux.Vars(r)["genus"]
measurements, err := store.Measurements.List(&opt)
if err != nil {
return err
}
if measurements == nil {
measurements = []*models.Measurement{}
}
return writeJSON(w, models.MeasurementsJSON{Measurements: measurements})
}

View file

@ -1,160 +0,0 @@
package api
import (
"database/sql"
"testing"
"github.com/thermokarst/bactdb/models"
)
func newMeasurement() *models.Measurement {
measurement := models.NewMeasurement()
measurement.Id = 1
measurement.StrainId = 2
measurement.CharacteristicId = 3
measurement.TextMeasurementTypeId = models.NullInt64{sql.NullInt64{Int64: 4, Valid: false}}
measurement.UnitTypeId = models.NullInt64{sql.NullInt64{Int64: 5, Valid: true}}
measurement.Notes = models.NullString{sql.NullString{String: "a note", Valid: true}}
return measurement
}
func TestMeasurement_Get(t *testing.T) {
setup()
want := newMeasurement()
calledGet := false
store.Measurements.(*models.MockMeasurementsService).Get_ = func(id int64) (*models.Measurement, error) {
if id != want.Id {
t.Errorf("wanted request for measurement %d but got %d", want.Id, id)
}
calledGet = true
return want, nil
}
got, err := apiClient.Measurements.Get(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledGet {
t.Error("!calledGet")
}
if !normalizeDeepEqual(want, got) {
t.Errorf("got %+v but wanted %+v", got, want)
}
}
func TestMeasurement_Create(t *testing.T) {
setup()
want := newMeasurement()
calledPost := false
store.Measurements.(*models.MockMeasurementsService).Create_ = func(measurement *models.Measurement) (bool, error) {
if !normalizeDeepEqual(want, measurement) {
t.Errorf("wanted request for measurement %d but got %d", want, measurement)
}
calledPost = true
return true, nil
}
success, err := apiClient.Measurements.Create(want)
if err != nil {
t.Fatal(err)
}
if !calledPost {
t.Error("!calledPost")
}
if !success {
t.Error("!success")
}
}
func TestMeasurement_List(t *testing.T) {
setup()
want := []*models.Measurement{newMeasurement()}
wantOpt := &models.MeasurementListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}
calledList := false
store.Measurements.(*models.MockMeasurementsService).List_ = func(opt *models.MeasurementListOptions) ([]*models.Measurement, error) {
if !normalizeDeepEqual(wantOpt, opt) {
t.Errorf("wanted options %d but got %d", wantOpt, opt)
}
calledList = true
return want, nil
}
measurements, err := apiClient.Measurements.List(wantOpt)
if err != nil {
t.Fatal(err)
}
if !calledList {
t.Error("!calledList")
}
if !normalizeDeepEqual(&want, &measurements) {
t.Errorf("got measurements %+v but wanted measurements %+v", measurements, want)
}
}
func TestMeasurement_Update(t *testing.T) {
setup()
want := newMeasurement()
calledPut := false
store.Measurements.(*models.MockMeasurementsService).Update_ = func(id int64, measurement *models.Measurement) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for measurement %d but got %d", want.Id, id)
}
if !normalizeDeepEqual(want, measurement) {
t.Errorf("wanted request for measurement %d but got %d", want, measurement)
}
calledPut = true
return true, nil
}
success, err := apiClient.Measurements.Update(want.Id, want)
if err != nil {
t.Fatal(err)
}
if !calledPut {
t.Error("!calledPut")
}
if !success {
t.Error("!success")
}
}
func TestMeasurement_Delete(t *testing.T) {
setup()
want := newMeasurement()
calledDelete := false
store.Measurements.(*models.MockMeasurementsService).Delete_ = func(id int64) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for measurement %d but got %d", want.Id, id)
}
calledDelete = true
return true, nil
}
success, err := apiClient.Measurements.Delete(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledDelete {
t.Error("!calledDelete")
}
if !success {
t.Error("!success")
}
}

View file

@ -1,97 +0,0 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"github.com/thermokarst/bactdb/datastore"
"github.com/thermokarst/bactdb/models"
"github.com/thermokarst/bactdb/router"
)
func init() {
signKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAyXi+Q3rVv+NkBi3UWBXyylUN5SyHxQUtZvhbA6TwcF2fk3Io
Di3kipVWbfYAYijIxczACieCnadSynJpH4zNVJsRxp8DTG67Nx/K5n3TJyg5hLpa
PE+46lOS7E0O9JPT119zKCGHtHldpgjsPCXGHRXKZdFafSv8ktFhwvK5ZQO1NjH2
+NOsfhp2ubuQXL7O/45fC5wTCj0lLatdXtlTcIxJb7FUj3AsAC7TlKhtkKe+Syox
n1xNMQiYK1R24+goW44JO5uZDYP85ufgRn+DdOQF9DskmiQN9/REhH/5VQjEIYZ3
kjmKlNnjz3Jd1eOdtGALxTq3+neVawWMPBXOcQIDAQABAoIBAHMAYhKgrhxPTwwb
4ua4+JK4BCt5xLIYp3bscv9cigaJ2onOksCtP5Q/dEtmLYfaYehOXJwvO2aEWUTI
E+t3cslFjtsCb16UonbvxeDVl871LgfuW42rsBDJzcbmoY/IRhbdHB2fLhg9YtBg
rYATy8dUZejCnNVwY0bnD9e4t0zJ0lXUVy+dMvl69uNyP8f12LwvLGgCmAOWXh5p
NpGmT8/jRF9BrQvr9bhwxpV2JGsGEEyGvu+ayVR01AiyQ04kh9gZOJOVtsGa1fjx
AvgxzhkfLyAbCAgFTTUuhEbZoxXyCNBdOM0V3PXSbIbW+7gwLwXi71Czo08V050z
5SK9p2UCgYEA8JW+xIaAzYT+ZwPaJ/Ir1+WcisEI+AyCD1c5gblHrCmSwYHdKSX4
ZcX0QAcj+dzuF6SyQStoy1pIooUzadDZxXeyBoeOjGdyobqJpmaEHb9g594w2inS
AsEb4waxvrKlTuhFXnI2JbJrbMyjRBTKWZw4K/FT73IE8hQL9ivXYN8CgYEA1mFu
uLD95N/KOFtQ0JcLUelPZK7gYSdq21YHSbABeeesvabnn3hFEzDl5cXqKFJ/4Ltf
2QhpO4JGgHYNlrsfCvvivV5dRxFKfleoojL/0qlJxOqQVfulscT0XB3wUpoyP+Qr
8AdyvZwUfLWpSaYxDUB7w77U1VayP5JLuULKKq8CgYBOge8QnnullUKXRzCHXIVm
HG1q8fcFSr+eVe5UIKv8yEw1jTUoWlWmkGRWCH566NdhK8NndMzrnviY4DKY0yhd
QeP8MXwY4SENGZwVitqOAoeS4nS6nG8FqxJ4kRSrkAxVpYINgeOdhY18oYKdktM9
Trcdz9B+EI0Amf4VRNUxrQKBgQCTBXTagr9MhFF5vt44fy3LOhcxtGC7ID4/N8t9
tI/+m2yzD9DPY7rzg1hW8Rk6GAINDFOaUxNgNWLGXK/LDH8omEASoLGVuHz/Enza
5+DcBy9JNZhQ72jd9nWi6wFSlN8bRA8B6Qm+kVjXgfocQTZooS1/u9LYkEFkKZ92
6SAejwKBgH6V+dLUefC9w6N99VWpOAom9gE96imo1itTKxIB1WPdFsSDDe/CGXDG
W+l7ZiSjXaAfNF5UtuhO6yY0ob++Aa/d+l+Zo7CWn4SrmwTAp1CdRHxX3KxkqHNi
BsuYClbQh5Z9lOKn8FCNW3NyahJdYeWGhb/ZdeS0n+F6Ov4V+grc
-----END RSA PRIVATE KEY-----`)
verifyKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyXi+Q3rVv+NkBi3UWBXy
ylUN5SyHxQUtZvhbA6TwcF2fk3IoDi3kipVWbfYAYijIxczACieCnadSynJpH4zN
VJsRxp8DTG67Nx/K5n3TJyg5hLpaPE+46lOS7E0O9JPT119zKCGHtHldpgjsPCXG
HRXKZdFafSv8ktFhwvK5ZQO1NjH2+NOsfhp2ubuQXL7O/45fC5wTCj0lLatdXtlT
cIxJb7FUj3AsAC7TlKhtkKe+Syoxn1xNMQiYK1R24+goW44JO5uZDYP85ufgRn+D
dOQF9DskmiQN9/REhH/5VQjEIYZ3kjmKlNnjz3Jd1eOdtGALxTq3+neVawWMPBXO
cQIDAQAB
-----END PUBLIC KEY-----`)
serveMux.Handle("/", http.StripPrefix("/api", Handler()))
}
var (
serveMux = http.NewServeMux()
httpClient = http.Client{
Transport: (*muxTransport)(serveMux),
}
apiClient = models.NewClient(&httpClient)
testToken models.UserSession
)
func setup() {
store = datastore.NewMockDatastore()
u, _ := apiClient.URL(router.GetToken, nil, nil)
resp, _ := httpClient.PostForm(u.String(),
url.Values{"username": {"test_user"}, "password": {"password"}})
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&testToken); err != nil {
panic(err)
}
}
type muxTransport http.ServeMux
// RoundTrip is for testing API requests. It intercepts all requests during testing
// to serve up a local/internal response.
func (t *muxTransport) RoundTrip(req *http.Request) (*http.Response, error) {
rw := httptest.NewRecorder()
rw.Body = new(bytes.Buffer)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", testToken.Token))
(*http.ServeMux)(t).ServeHTTP(rw, req)
return &http.Response{
StatusCode: rw.Code,
Status: http.StatusText(rw.Code),
Header: rw.HeaderMap,
Body: ioutil.NopCloser(rw.Body),
ContentLength: int64(rw.Body.Len()),
Request: req,
}, nil
}

View file

@ -1,111 +0,0 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/thermokarst/bactdb/models"
)
func serveSpecies(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
if err != nil {
return err
}
species, err := store.Species.Get(id)
if err != nil {
return err
}
return writeJSON(w, models.SpeciesJSON{species})
}
func serveCreateSpecies(w http.ResponseWriter, r *http.Request) error {
var species models.SpeciesJSON
err := json.NewDecoder(r.Body).Decode(&species)
if err != nil {
return err
}
created, err := store.Species.Create(species.Species)
if err != nil {
return err
}
if created {
w.WriteHeader(http.StatusCreated)
}
return writeJSON(w, species)
}
func serveSpeciesList(w http.ResponseWriter, r *http.Request) error {
var opt models.SpeciesListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
species, err := store.Species.List(&opt)
if err != nil {
return err
}
if species == nil {
species = []*models.Species{}
}
return writeJSON(w, models.SpeciesListJSON{Species: species})
}
func serveUpdateSpecies(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
var species models.SpeciesJSON
err := json.NewDecoder(r.Body).Decode(&species)
if err != nil {
return err
}
updated, err := store.Species.Update(id, species.Species)
if err != nil {
return err
}
if updated {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, species)
}
func serveDeleteSpecies(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
deleted, err := store.Species.Delete(id)
if err != nil {
return err
}
if deleted {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, nil)
}
func serveSubrouterSpeciesList(w http.ResponseWriter, r *http.Request) error {
var opt models.SpeciesListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
opt.Genus = mux.Vars(r)["genus"]
species, err := store.Species.List(&opt)
if err != nil {
return err
}
if species == nil {
species = []*models.Species{}
}
return writeJSON(w, models.SpeciesListJSON{Species: species})
}

View file

@ -1,154 +0,0 @@
package api
import (
"testing"
"github.com/thermokarst/bactdb/models"
)
func newSpecies() *models.Species {
species := models.NewSpecies()
species.Id = 1
species.GenusId = 1
return species
}
func TestSpecies_Get(t *testing.T) {
setup()
want := newSpecies()
calledGet := false
store.Species.(*models.MockSpeciesService).Get_ = func(id int64) (*models.Species, error) {
if id != want.Id {
t.Errorf("wanted request for species %d but got %d", want.Id, id)
}
calledGet = true
return want, nil
}
got, err := apiClient.Species.Get(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledGet {
t.Error("!calledGet")
}
if !normalizeDeepEqual(want, got) {
t.Errorf("got species %+v but wanted species %+v", got, want)
}
}
func TestSpecies_Create(t *testing.T) {
setup()
want := newSpecies()
calledPost := false
store.Species.(*models.MockSpeciesService).Create_ = func(species *models.Species) (bool, error) {
if !normalizeDeepEqual(want, species) {
t.Errorf("wanted request for species %d but got %d", want, species)
}
calledPost = true
return true, nil
}
success, err := apiClient.Species.Create(want)
if err != nil {
t.Fatal(err)
}
if !calledPost {
t.Error("!calledPost")
}
if !success {
t.Error("!success")
}
}
func TestSpecies_List(t *testing.T) {
setup()
want := []*models.Species{newSpecies()}
wantOpt := &models.SpeciesListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}
calledList := false
store.Species.(*models.MockSpeciesService).List_ = func(opt *models.SpeciesListOptions) ([]*models.Species, error) {
if !normalizeDeepEqual(wantOpt, opt) {
t.Errorf("wanted options %d but got %d", wantOpt, opt)
}
calledList = true
return want, nil
}
species, err := apiClient.Species.List(wantOpt)
if err != nil {
t.Fatal(err)
}
if !calledList {
t.Error("!calledList")
}
if !normalizeDeepEqual(&want, &species) {
t.Errorf("got species %+v but wanted species %+v", species, want)
}
}
func TestSpecies_Update(t *testing.T) {
setup()
want := newSpecies()
calledPut := false
store.Species.(*models.MockSpeciesService).Update_ = func(id int64, species *models.Species) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for species %d but got %d", want.Id, id)
}
if !normalizeDeepEqual(want, species) {
t.Errorf("wanted request for species %d but got %d", want, species)
}
calledPut = true
return true, nil
}
success, err := apiClient.Species.Update(1, want)
if err != nil {
t.Fatal(err)
}
if !calledPut {
t.Error("!calledPut")
}
if !success {
t.Error("!success")
}
}
func TestSpecies_Delete(t *testing.T) {
setup()
want := newSpecies()
calledDelete := false
store.Species.(*models.MockSpeciesService).Delete_ = func(id int64) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for species %d but got %d", want.Id, id)
}
calledDelete = true
return true, nil
}
success, err := apiClient.Species.Delete(1)
if err != nil {
t.Fatal(err)
}
if !calledDelete {
t.Error("!calledDelete")
}
if !success {
t.Error("!success")
}
}

View file

@ -1,111 +0,0 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/thermokarst/bactdb/models"
)
func serveStrain(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
if err != nil {
return err
}
strain, err := store.Strains.Get(id)
if err != nil {
return err
}
return writeJSON(w, models.StrainJSON{Strain: strain})
}
func serveCreateStrain(w http.ResponseWriter, r *http.Request) error {
var strain models.StrainJSON
err := json.NewDecoder(r.Body).Decode(&strain)
if err != nil {
return err
}
created, err := store.Strains.Create(strain.Strain)
if err != nil {
return err
}
if created {
w.WriteHeader(http.StatusCreated)
}
return writeJSON(w, strain)
}
func serveStrainList(w http.ResponseWriter, r *http.Request) error {
var opt models.StrainListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
strains, err := store.Strains.List(&opt)
if err != nil {
return err
}
if strains == nil {
strains = []*models.Strain{}
}
return writeJSON(w, models.StrainsJSON{Strains: strains})
}
func serveUpdateStrain(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
var strain models.StrainJSON
err := json.NewDecoder(r.Body).Decode(&strain)
if err != nil {
return err
}
updated, err := store.Strains.Update(id, strain.Strain)
if err != nil {
return err
}
if updated {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, strain)
}
func serveDeleteStrain(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
deleted, err := store.Strains.Delete(id)
if err != nil {
return err
}
if deleted {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, nil)
}
func serveSubrouterStrainsList(w http.ResponseWriter, r *http.Request) error {
var opt models.StrainListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
opt.Genus = mux.Vars(r)["genus"]
strains, err := store.Strains.List(&opt)
if err != nil {
return err
}
if strains == nil {
strains = []*models.Strain{}
}
return writeJSON(w, models.StrainsJSON{Strains: strains})
}

View file

@ -1,155 +0,0 @@
package api
import (
"testing"
"github.com/thermokarst/bactdb/models"
)
func newStrain() *models.Strain {
strain := models.NewStrain()
strain.Id = 1
strain.SpeciesId = 1
return strain
}
func TestStrain_Get(t *testing.T) {
setup()
want := newStrain()
calledGet := false
store.Strains.(*models.MockStrainsService).Get_ = func(id int64) (*models.Strain, error) {
if id != want.Id {
t.Errorf("wanted request for strain %d but got %d", want.Id, id)
}
calledGet = true
return want, nil
}
got, err := apiClient.Strains.Get(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledGet {
t.Error("!calledGet")
}
if !normalizeDeepEqual(want, got) {
t.Errorf("got strain %+v but wanted strain %+v", got, want)
}
}
func TestStrain_Create(t *testing.T) {
setup()
want := newStrain()
calledPost := false
store.Strains.(*models.MockStrainsService).Create_ = func(strain *models.Strain) (bool, error) {
if !normalizeDeepEqual(want, strain) {
t.Errorf("wanted request for strain %d but got %d", want, strain)
}
calledPost = true
return true, nil
}
success, err := apiClient.Strains.Create(want)
if err != nil {
t.Fatal(err)
}
if !calledPost {
t.Error("!calledPost")
}
if !success {
t.Error("!success")
}
}
func TestStrain_List(t *testing.T) {
setup()
want := []*models.Strain{newStrain()}
wantOpt := &models.StrainListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}
calledList := false
store.Strains.(*models.MockStrainsService).List_ = func(opt *models.StrainListOptions) ([]*models.Strain, error) {
if !normalizeDeepEqual(wantOpt, opt) {
t.Errorf("wanted options %d but got %d", wantOpt, opt)
}
calledList = true
return want, nil
}
strains, err := apiClient.Strains.List(wantOpt)
if err != nil {
t.Fatal(err)
}
if !calledList {
t.Error("!calledList")
}
if !normalizeDeepEqual(&want, &strains) {
t.Errorf("got strains %+v but wanted strains %+v", strains, want)
}
}
func TestStrain_Update(t *testing.T) {
setup()
want := newStrain()
calledPut := false
store.Strains.(*models.MockStrainsService).Update_ = func(id int64, strain *models.Strain) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for strain %d but got %d", want.Id, id)
}
if !normalizeDeepEqual(want, strain) {
t.Errorf("wanted request for strain %d but got %d", want, strain)
}
calledPut = true
return true, nil
}
success, err := apiClient.Strains.Update(1, want)
if err != nil {
t.Fatal(err)
}
if !calledPut {
t.Error("!calledPut")
}
if !success {
t.Error("!success")
}
}
func TestStrain_Delete(t *testing.T) {
setup()
want := newStrain()
calledDelete := false
store.Strains.(*models.MockStrainsService).Delete_ = func(id int64) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for strain %d but got %d", want.Id, id)
}
calledDelete = true
return true, nil
}
success, err := apiClient.Strains.Delete(1)
if err != nil {
t.Fatal(err)
}
if !calledDelete {
t.Error("!calledDelete")
}
if !success {
t.Error("!success")
}
}

View file

@ -1,92 +0,0 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/thermokarst/bactdb/models"
)
func serveTextMeasurementType(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
if err != nil {
return err
}
text_measurement_type, err := store.TextMeasurementTypes.Get(id)
if err != nil {
return err
}
return writeJSON(w, text_measurement_type)
}
func serveCreateTextMeasurementType(w http.ResponseWriter, r *http.Request) error {
var text_measurement_type models.TextMeasurementType
err := json.NewDecoder(r.Body).Decode(&text_measurement_type)
if err != nil {
return err
}
created, err := store.TextMeasurementTypes.Create(&text_measurement_type)
if err != nil {
return err
}
if created {
w.WriteHeader(http.StatusCreated)
}
return writeJSON(w, text_measurement_type)
}
func serveTextMeasurementTypeList(w http.ResponseWriter, r *http.Request) error {
var opt models.TextMeasurementTypeListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
text_measurement_types, err := store.TextMeasurementTypes.List(&opt)
if err != nil {
return err
}
if text_measurement_types == nil {
text_measurement_types = []*models.TextMeasurementType{}
}
return writeJSON(w, text_measurement_types)
}
func serveUpdateTextMeasurementType(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
var text_measurement_type models.TextMeasurementType
err := json.NewDecoder(r.Body).Decode(&text_measurement_type)
if err != nil {
return err
}
updated, err := store.TextMeasurementTypes.Update(id, &text_measurement_type)
if err != nil {
return err
}
if updated {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, text_measurement_type)
}
func serveDeleteTextMeasurementType(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
deleted, err := store.TextMeasurementTypes.Delete(id)
if err != nil {
return err
}
if deleted {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, &models.TextMeasurementType{})
}

View file

@ -1,153 +0,0 @@
package api
import (
"testing"
"github.com/thermokarst/bactdb/models"
)
func newTextMeasurementType() *models.TextMeasurementType {
text_measurement_type := models.NewTextMeasurementType()
return text_measurement_type
}
func TestTextMeasurementType_Get(t *testing.T) {
setup()
want := newTextMeasurementType()
calledGet := false
store.TextMeasurementTypes.(*models.MockTextMeasurementTypesService).Get_ = func(id int64) (*models.TextMeasurementType, error) {
if id != want.Id {
t.Errorf("wanted request for text_measurement_type %d but got %d", want.Id, id)
}
calledGet = true
return want, nil
}
got, err := apiClient.TextMeasurementTypes.Get(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledGet {
t.Error("!calledGet")
}
if !normalizeDeepEqual(want, got) {
t.Errorf("got %+v but wanted %+v", got, want)
}
}
func TestTextMeasurementType_Create(t *testing.T) {
setup()
want := newTextMeasurementType()
calledPost := false
store.TextMeasurementTypes.(*models.MockTextMeasurementTypesService).Create_ = func(text_measurement_type *models.TextMeasurementType) (bool, error) {
if !normalizeDeepEqual(want, text_measurement_type) {
t.Errorf("wanted request for text_measurement_type %d but got %d", want, text_measurement_type)
}
calledPost = true
return true, nil
}
success, err := apiClient.TextMeasurementTypes.Create(want)
if err != nil {
t.Fatal(err)
}
if !calledPost {
t.Error("!calledPost")
}
if !success {
t.Error("!success")
}
}
func TestTextMeasurementType_List(t *testing.T) {
setup()
want := []*models.TextMeasurementType{newTextMeasurementType()}
wantOpt := &models.TextMeasurementTypeListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}
calledList := false
store.TextMeasurementTypes.(*models.MockTextMeasurementTypesService).List_ = func(opt *models.TextMeasurementTypeListOptions) ([]*models.TextMeasurementType, error) {
if !normalizeDeepEqual(wantOpt, opt) {
t.Errorf("wanted options %d but got %d", wantOpt, opt)
}
calledList = true
return want, nil
}
text_measurement_types, err := apiClient.TextMeasurementTypes.List(wantOpt)
if err != nil {
t.Fatal(err)
}
if !calledList {
t.Error("!calledList")
}
if !normalizeDeepEqual(&want, &text_measurement_types) {
t.Errorf("got text_measurement_types %+v but wanted text_measurement_types %+v", text_measurement_types, want)
}
}
func TestTextMeasurementType_Update(t *testing.T) {
setup()
want := newTextMeasurementType()
calledPut := false
store.TextMeasurementTypes.(*models.MockTextMeasurementTypesService).Update_ = func(id int64, text_measurement_type *models.TextMeasurementType) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for text_measurement_type %d but got %d", want.Id, id)
}
if !normalizeDeepEqual(want, text_measurement_type) {
t.Errorf("wanted request for text_measurement_type %d but got %d", want, text_measurement_type)
}
calledPut = true
return true, nil
}
success, err := apiClient.TextMeasurementTypes.Update(want.Id, want)
if err != nil {
t.Fatal(err)
}
if !calledPut {
t.Error("!calledPut")
}
if !success {
t.Error("!success")
}
}
func TestTextMeasurementType_Delete(t *testing.T) {
setup()
want := newTextMeasurementType()
calledDelete := false
store.TextMeasurementTypes.(*models.MockTextMeasurementTypesService).Delete_ = func(id int64) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for text_measurement_type %d but got %d", want.Id, id)
}
calledDelete = true
return true, nil
}
success, err := apiClient.TextMeasurementTypes.Delete(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledDelete {
t.Error("!calledDelete")
}
if !success {
t.Error("!success")
}
}

View file

@ -1,92 +0,0 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/thermokarst/bactdb/models"
)
func serveUnitType(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
if err != nil {
return err
}
unit_type, err := store.UnitTypes.Get(id)
if err != nil {
return err
}
return writeJSON(w, unit_type)
}
func serveCreateUnitType(w http.ResponseWriter, r *http.Request) error {
var unit_type models.UnitType
err := json.NewDecoder(r.Body).Decode(&unit_type)
if err != nil {
return err
}
created, err := store.UnitTypes.Create(&unit_type)
if err != nil {
return err
}
if created {
w.WriteHeader(http.StatusCreated)
}
return writeJSON(w, unit_type)
}
func serveUnitTypeList(w http.ResponseWriter, r *http.Request) error {
var opt models.UnitTypeListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
unit_types, err := store.UnitTypes.List(&opt)
if err != nil {
return err
}
if unit_types == nil {
unit_types = []*models.UnitType{}
}
return writeJSON(w, unit_types)
}
func serveUpdateUnitType(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
var unit_type models.UnitType
err := json.NewDecoder(r.Body).Decode(&unit_type)
if err != nil {
return err
}
updated, err := store.UnitTypes.Update(id, &unit_type)
if err != nil {
return err
}
if updated {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, unit_type)
}
func serveDeleteUnitType(w http.ResponseWriter, r *http.Request) error {
id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
deleted, err := store.UnitTypes.Delete(id)
if err != nil {
return err
}
if deleted {
w.WriteHeader(http.StatusOK)
}
return writeJSON(w, &models.UnitType{})
}

View file

@ -1,153 +0,0 @@
package api
import (
"testing"
"github.com/thermokarst/bactdb/models"
)
func newUnitType() *models.UnitType {
unit_type := models.NewUnitType()
return unit_type
}
func TestUnitType_Get(t *testing.T) {
setup()
want := newUnitType()
calledGet := false
store.UnitTypes.(*models.MockUnitTypesService).Get_ = func(id int64) (*models.UnitType, error) {
if id != want.Id {
t.Errorf("wanted request for unit_type %d but got %d", want.Id, id)
}
calledGet = true
return want, nil
}
got, err := apiClient.UnitTypes.Get(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledGet {
t.Error("!calledGet")
}
if !normalizeDeepEqual(want, got) {
t.Errorf("got %+v but wanted %+v", got, want)
}
}
func TestUnitType_Create(t *testing.T) {
setup()
want := newUnitType()
calledPost := false
store.UnitTypes.(*models.MockUnitTypesService).Create_ = func(unit_type *models.UnitType) (bool, error) {
if !normalizeDeepEqual(want, unit_type) {
t.Errorf("wanted request for unit_type %d but got %d", want, unit_type)
}
calledPost = true
return true, nil
}
success, err := apiClient.UnitTypes.Create(want)
if err != nil {
t.Fatal(err)
}
if !calledPost {
t.Error("!calledPost")
}
if !success {
t.Error("!success")
}
}
func TestUnitType_List(t *testing.T) {
setup()
want := []*models.UnitType{newUnitType()}
wantOpt := &models.UnitTypeListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}
calledList := false
store.UnitTypes.(*models.MockUnitTypesService).List_ = func(opt *models.UnitTypeListOptions) ([]*models.UnitType, error) {
if !normalizeDeepEqual(wantOpt, opt) {
t.Errorf("wanted options %d but got %d", wantOpt, opt)
}
calledList = true
return want, nil
}
unit_types, err := apiClient.UnitTypes.List(wantOpt)
if err != nil {
t.Fatal(err)
}
if !calledList {
t.Error("!calledList")
}
if !normalizeDeepEqual(&want, &unit_types) {
t.Errorf("got unit_types %+v but wanted unit_types %+v", unit_types, want)
}
}
func TestUnitType_Update(t *testing.T) {
setup()
want := newUnitType()
calledPut := false
store.UnitTypes.(*models.MockUnitTypesService).Update_ = func(id int64, unit_type *models.UnitType) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for unit_type %d but got %d", want.Id, id)
}
if !normalizeDeepEqual(want, unit_type) {
t.Errorf("wanted request for unit_type %d but got %d", want, unit_type)
}
calledPut = true
return true, nil
}
success, err := apiClient.UnitTypes.Update(want.Id, want)
if err != nil {
t.Fatal(err)
}
if !calledPut {
t.Error("!calledPut")
}
if !success {
t.Error("!success")
}
}
func TestUnitType_Delete(t *testing.T) {
setup()
want := newUnitType()
calledDelete := false
store.UnitTypes.(*models.MockUnitTypesService).Delete_ = func(id int64) (bool, error) {
if id != want.Id {
t.Errorf("wanted request for unit_type %d but got %d", want.Id, id)
}
calledDelete = true
return true, nil
}
success, err := apiClient.UnitTypes.Delete(want.Id)
if err != nil {
t.Fatal(err)
}
if !calledDelete {
t.Error("!calledDelete")
}
if !success {
t.Error("!success")
}
}

View file

@ -1,87 +0,0 @@
package api
import (
"encoding/json"
"strconv"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/mux"
"net/http"
"github.com/thermokarst/bactdb/models"
)
func serveUser(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
if err != nil {
return err
}
user, err := store.Users.Get(id)
if err != nil {
return err
}
return writeJSON(w, models.UserJSON{user})
}
func serveCreateUser(w http.ResponseWriter, r *http.Request) error {
var user models.UserJSON
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
return err
}
created, err := store.Users.Create(user.User)
if err != nil {
return err
}
if created {
w.WriteHeader(http.StatusCreated)
}
return writeJSON(w, user)
}
func serveUsers(w http.ResponseWriter, r *http.Request) error {
var opt models.UserListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
return err
}
users, err := store.Users.List(&opt)
if err != nil {
return err
}
if users == nil {
users = []*models.User{}
}
return writeJSON(w, models.UsersJSON{Users: users})
}
func serveAuthenticateUser(w http.ResponseWriter, r *http.Request) error {
username := r.FormValue("username")
password := r.FormValue("password")
user_session, err := store.Users.Authenticate(username, password)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return err
}
t := jwt.New(jwt.GetSigningMethod("RS256"))
t.Claims["auth_level"] = user_session.AccessLevel
t.Claims["genus"] = user_session.Genus
t.Claims["exp"] = time.Now().Add(time.Minute * 60 * 24).Unix()
tokenString, err := t.SignedString(signKey)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return err
}
user_session.Token = tokenString
return writeJSON(w, user_session)
}

View file

@ -1,127 +0,0 @@
package api
import (
"testing"
"github.com/thermokarst/bactdb/models"
)
func newUser() *models.User {
user := models.NewUser()
user.Id = 1
return user
}
func TestUser_Get(t *testing.T) {
setup()
wantUser := newUser()
calledGet := false
store.Users.(*models.MockUsersService).Get_ = func(id int64) (*models.User, error) {
if id != wantUser.Id {
t.Errorf("wanted request for user %d but got %d", wantUser.Id, id)
}
calledGet = true
return wantUser, nil
}
gotUser, err := apiClient.Users.Get(wantUser.Id)
if err != nil {
t.Fatal(err)
}
if !calledGet {
t.Error("!calledGet")
}
if !normalizeDeepEqual(wantUser, gotUser) {
t.Errorf("got user %+v but wanted user %+v", wantUser, gotUser)
}
}
func TestUser_Create(t *testing.T) {
setup()
wantUser := newUser()
calledPost := false
store.Users.(*models.MockUsersService).Create_ = func(user *models.User) (bool, error) {
if !normalizeDeepEqual(wantUser, user) {
t.Errorf("wanted request for user %d but got %d", wantUser, user)
}
calledPost = true
return true, nil
}
success, err := apiClient.Users.Create(wantUser)
if err != nil {
t.Fatal(err)
}
if !calledPost {
t.Error("!calledPost")
}
if !success {
t.Error("!success")
}
}
func TestUser_List(t *testing.T) {
setup()
wantUsers := []*models.User{newUser()}
wantOpt := &models.UserListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}
calledList := false
store.Users.(*models.MockUsersService).List_ = func(opt *models.UserListOptions) ([]*models.User, error) {
if !normalizeDeepEqual(wantOpt, opt) {
t.Errorf("wanted options %d but got %d", wantOpt, opt)
}
calledList = true
return wantUsers, nil
}
users, err := apiClient.Users.List(wantOpt)
if err != nil {
t.Fatal(err)
}
if !calledList {
t.Error("!calledList")
}
if !normalizeDeepEqual(&wantUsers, &users) {
t.Errorf("got users %+v but wanted users %+v", users, wantUsers)
}
}
func TestUser_Authenticate(t *testing.T) {
setup()
test_user := newUser()
test_user.Username = "test_user"
var user_session_want models.UserSession
calledAuthenticate := false
store.Users.(*models.MockUsersService).Authenticate_ = func(username string, password string) (*models.UserSession, error) {
calledAuthenticate = true
user_session_want.AccessLevel = "read"
user_session_want.Genus = "hymenobacter"
return &user_session_want, nil
}
user_session, err := apiClient.Users.Authenticate(test_user.Username, "password")
if err != nil {
t.Fatal(err)
}
if !calledAuthenticate {
t.Error("!calledAuthenticate")
}
if !normalizeDeepEqual(user_session, &user_session_want) {
t.Errorf("got session %+v but wanted session %+v", user_session, user_session_want)
}
}

36
auth.go Normal file
View file

@ -0,0 +1,36 @@
package main
import (
"errors"
"os"
)
const (
tokenName = "AccessToken"
)
var (
verifyKey, signKey []byte
errWhileSigningToken = errors.New("error while signing token")
errPleaseLogIn = errors.New("please log in")
errWhileParsingCookie = errors.New("error while parsing cookie")
errTokenExpired = errors.New("token expired")
errGenericError = errors.New("generic error")
errAccessDenied = errors.New("insufficient privileges")
)
func setupCerts() error {
signkey := os.Getenv("PRIVATE_KEY")
if signkey == "" {
return errors.New("please set PRIVATE_KEY")
}
signKey = []byte(signkey)
verifykey := os.Getenv("PUBLIC_KEY")
if verifykey == "" {
return errors.New("please set PUBLIC_KEY")
}
verifyKey = []byte(verifykey)
return nil
}

101
bactdb.go
View file

@ -1,101 +0,0 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"strings"
"github.com/codegangsta/cli"
"github.com/thermokarst/bactdb/api"
"github.com/thermokarst/bactdb/datastore"
)
func main() {
app := cli.NewApp()
app.Name = "bactdb"
app.Usage = "a database for bacteria"
app.Commands = []cli.Command{
{
Name: "serve",
ShortName: "s",
Usage: "Start web server",
Action: cmdServe,
},
{
Name: "createdb",
ShortName: "c",
Usage: "create the database schema",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "drop",
Usage: "drop DB before creating",
},
cli.StringFlag{
Name: "migration_path",
Usage: "path to migrations",
Value: "./datastore/migrations",
},
},
Action: cmdCreateDB,
},
}
app.Run(os.Args)
}
func cmdServe(c *cli.Context) {
var err error
addr := os.Getenv("PORT")
if addr == "" {
addr = "8901"
}
httpAddr := fmt.Sprintf(":%v", addr)
datastore.Connect()
err = api.SetupCerts()
if err != nil {
log.Fatal("SetupCerts: ", err)
}
m := http.NewServeMux()
m.Handle("/api/", http.StripPrefix("/api", corsHandler(api.Handler())))
log.Print("Listening on ", httpAddr)
err = http.ListenAndServe(httpAddr, m)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
func cmdCreateDB(c *cli.Context) {
migrationsPath := c.String("migration_path")
datastore.Connect()
if c.Bool("drop") {
datastore.Drop(migrationsPath)
}
datastore.Create(migrationsPath)
}
func corsHandler(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
domains := os.Getenv("DOMAINS")
allowedDomains := strings.Split(domains, ",")
if origin := r.Header.Get("Origin"); origin != "" {
for _, s := range allowedDomains {
if s == origin {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Headers", r.Header.Get("Access-Control-Request-Headers"))
w.Header().Set("Access-Control-Allow-Methods", r.Header.Get("Access-Control-Request-Method"))
}
}
}
if r.Method != "OPTIONS" {
h.ServeHTTP(w, r)
}
}
}

View file

@ -1,87 +0,0 @@
package datastore
import (
"time"
"github.com/thermokarst/bactdb/models"
)
func init() {
DB.AddTableWithName(models.CharacteristicType{}, "characteristic_types").SetKeys(true, "Id")
}
type characteristicTypesStore struct {
*Datastore
}
func (s *characteristicTypesStore) Get(id int64) (*models.CharacteristicType, error) {
var characteristic_type models.CharacteristicType
if err := s.dbh.SelectOne(&characteristic_type, `SELECT * FROM characteristic_types WHERE id=$1;`, id); err != nil {
return nil, err
}
if &characteristic_type == nil {
return nil, models.ErrCharacteristicTypeNotFound
}
return &characteristic_type, nil
}
func (s *characteristicTypesStore) Create(characteristic_type *models.CharacteristicType) (bool, error) {
currentTime := time.Now()
characteristic_type.CreatedAt = currentTime
characteristic_type.UpdatedAt = currentTime
if err := s.dbh.Insert(characteristic_type); err != nil {
return false, err
}
return true, nil
}
func (s *characteristicTypesStore) List(opt *models.CharacteristicTypeListOptions) ([]*models.CharacteristicType, error) {
if opt == nil {
opt = &models.CharacteristicTypeListOptions{}
}
var characteristic_types []*models.CharacteristicType
err := s.dbh.Select(&characteristic_types, `SELECT * FROM characteristic_types LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset())
if err != nil {
return nil, err
}
return characteristic_types, nil
}
func (s *characteristicTypesStore) Update(id int64, characteristic_type *models.CharacteristicType) (bool, error) {
_, err := s.Get(id)
if err != nil {
return false, err
}
if id != characteristic_type.Id {
return false, models.ErrCharacteristicTypeNotFound
}
characteristic_type.UpdatedAt = time.Now()
changed, err := s.dbh.Update(characteristic_type)
if err != nil {
return false, err
}
if changed == 0 {
return false, ErrNoRowsUpdated
}
return true, nil
}
func (s *characteristicTypesStore) Delete(id int64) (bool, error) {
characteristic_type, err := s.Get(id)
if err != nil {
return false, err
}
deleted, err := s.dbh.Delete(characteristic_type)
if err != nil {
return false, err
}
if deleted == 0 {
return false, ErrNoRowsDeleted
}
return true, nil
}

View file

@ -1,126 +0,0 @@
package datastore
import (
"reflect"
"testing"
"github.com/jmoiron/modl"
"github.com/thermokarst/bactdb/models"
)
func insertCharacteristicType(t *testing.T, tx *modl.Transaction) *models.CharacteristicType {
// clean up our target table
tx.Exec(`DELETE FROM characteristic_types;`)
characteristic_type := newCharacteristicType(t, tx)
if err := tx.Insert(characteristic_type); err != nil {
t.Fatal(err)
}
return characteristic_type
}
func newCharacteristicType(t *testing.T, tx *modl.Transaction) *models.CharacteristicType {
return &models.CharacteristicType{CharacteristicTypeName: "Test Char"}
}
func TestCharacteristicTypesStore_Get_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want := insertCharacteristicType(t, tx)
d := NewDatastore(tx)
characteristic_type, err := d.CharacteristicTypes.Get(want.Id)
if err != nil {
t.Fatal(err)
}
normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
normalizeTime(&characteristic_type.CreatedAt, &characteristic_type.UpdatedAt, &characteristic_type.DeletedAt)
if !reflect.DeepEqual(characteristic_type, want) {
t.Errorf("got characteristic_type %+v, want %+v", characteristic_type, want)
}
}
func TestCharacteristicTypesStore_Create_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
characteristic_type := newCharacteristicType(t, tx)
d := NewDatastore(tx)
created, err := d.CharacteristicTypes.Create(characteristic_type)
if err != nil {
t.Fatal(err)
}
if !created {
t.Error("!created")
}
if characteristic_type.Id == 0 {
t.Error("want nonzero characteristic_type.Id after submitting")
}
}
func TestCharacteristicTypesStore_List_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want_characteristic_type := insertCharacteristicType(t, tx)
want := []*models.CharacteristicType{want_characteristic_type}
d := NewDatastore(tx)
characteristic_types, err := d.CharacteristicTypes.List(&models.CharacteristicTypeListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}})
if err != nil {
t.Fatal(err)
}
for i := range want {
normalizeTime(&want[i].CreatedAt, &want[i].UpdatedAt, &want[i].DeletedAt)
normalizeTime(&characteristic_types[i].CreatedAt, &characteristic_types[i].UpdatedAt, &characteristic_types[i].DeletedAt)
}
if !reflect.DeepEqual(characteristic_types, want) {
t.Errorf("got characteristic_types %+v, want %+v", characteristic_types, want)
}
}
func TestCharacteristicTypesStore_Update_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
characteristic_type := insertCharacteristicType(t, tx)
d := NewDatastore(tx)
// Tweak it
characteristic_type.CharacteristicTypeName = "Updated Char Type"
updated, err := d.CharacteristicTypes.Update(characteristic_type.Id, characteristic_type)
if err != nil {
t.Fatal(err)
}
if !updated {
t.Error("!updated")
}
}
func TestCharacteristicTypesStore_Delete_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
characteristic_type := insertCharacteristicType(t, tx)
d := NewDatastore(tx)
// Delete it
deleted, err := d.CharacteristicTypes.Delete(characteristic_type.Id)
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Error("!delete")
}
}

View file

@ -1,90 +0,0 @@
package datastore
import (
"time"
"github.com/thermokarst/bactdb/models"
)
func init() {
DB.AddTableWithName(models.CharacteristicBase{}, "characteristics").SetKeys(true, "Id")
}
type characteristicsStore struct {
*Datastore
}
func (s *characteristicsStore) Get(id int64) (*models.Characteristic, error) {
var characteristic models.Characteristic
err := s.dbh.SelectOne(&characteristic, `SELECT c.*, array_agg(m.id) AS measurements FROM characteristics c LEFT OUTER JOIN measurements m ON m.characteristic_id=c.id WHERE c.id=$1 GROUP BY c.id;`, id)
if err != nil {
return nil, err
}
if &characteristic == nil {
return nil, models.ErrCharacteristicNotFound
}
return &characteristic, nil
}
func (s *characteristicsStore) Create(characteristic *models.Characteristic) (bool, error) {
currentTime := time.Now()
characteristic.CreatedAt = currentTime
characteristic.UpdatedAt = currentTime
base := characteristic.CharacteristicBase
if err := s.dbh.Insert(base); err != nil {
return false, err
}
characteristic.Id = base.Id
return true, nil
}
func (s *characteristicsStore) List(opt *models.CharacteristicListOptions) ([]*models.Characteristic, error) {
if opt == nil {
opt = &models.CharacteristicListOptions{}
}
var characteristics []*models.Characteristic
err := s.dbh.Select(&characteristics, `SELECT c.*, array_agg(m.id) AS measurements FROM characteristics c LEFT OUTER JOIN measurements m ON m.characteristic_id=c.id GROUP BY c.id;`)
if err != nil {
return nil, err
}
return characteristics, nil
}
func (s *characteristicsStore) Update(id int64, characteristic *models.Characteristic) (bool, error) {
_, err := s.Get(id)
if err != nil {
return false, err
}
if id != characteristic.Id {
return false, models.ErrCharacteristicNotFound
}
characteristic.UpdatedAt = time.Now()
changed, err := s.dbh.Update(characteristic.CharacteristicBase)
if err != nil {
return false, err
}
if changed == 0 {
return false, ErrNoRowsUpdated
}
return true, nil
}
func (s *characteristicsStore) Delete(id int64) (bool, error) {
characteristic, err := s.Get(id)
if err != nil {
return false, err
}
deleted, err := s.dbh.Delete(characteristic.CharacteristicBase)
if err != nil {
return false, err
}
if deleted == 0 {
return false, ErrNoRowsDeleted
}
return true, nil
}

View file

@ -1,130 +0,0 @@
package datastore
import (
"reflect"
"testing"
"github.com/jmoiron/modl"
"github.com/thermokarst/bactdb/models"
)
func insertCharacteristic(t *testing.T, tx *modl.Transaction) *models.Characteristic {
// clean up our target table
tx.Exec(`DELETE FROM characteristics;`)
c := newCharacteristic(t, tx)
if err := tx.Insert(c); err != nil {
t.Fatal(err)
}
return &models.Characteristic{c, []int64(nil)}
}
func newCharacteristic(t *testing.T, tx *modl.Transaction) *models.CharacteristicBase {
// we want to create and insert an characteristic type record, too.
characteristic_type := insertCharacteristicType(t, tx)
return &models.CharacteristicBase{CharacteristicName: "Test Characteristic",
CharacteristicTypeId: characteristic_type.Id}
}
func TestCharacteristicsStore_Get_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want := insertCharacteristic(t, tx)
d := NewDatastore(tx)
characteristic, err := d.Characteristics.Get(want.Id)
if err != nil {
t.Fatal(err)
}
normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
normalizeTime(&characteristic.CreatedAt, &characteristic.UpdatedAt, &characteristic.DeletedAt)
if !reflect.DeepEqual(characteristic, want) {
t.Errorf("got characteristic %+v, want %+v", characteristic, want)
}
}
func TestCharacteristicsStore_Create_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
base_characteristic := newCharacteristic(t, tx)
characteristic := models.Characteristic{base_characteristic, []int64(nil)}
d := NewDatastore(tx)
created, err := d.Characteristics.Create(&characteristic)
if err != nil {
t.Fatal(err)
}
if !created {
t.Error("!created")
}
if characteristic.Id == 0 {
t.Error("want nonzero characteristic.Id after submitting")
}
}
func TestCharacteristicsStore_List_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want_characteristic := insertCharacteristic(t, tx)
want := []*models.Characteristic{want_characteristic}
d := NewDatastore(tx)
characteristics, err := d.Characteristics.List(&models.CharacteristicListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}})
if err != nil {
t.Fatal(err)
}
for i := range want {
normalizeTime(&want[i].CreatedAt, &want[i].UpdatedAt, &want[i].DeletedAt)
normalizeTime(&characteristics[i].CreatedAt, &characteristics[i].UpdatedAt, &characteristics[i].DeletedAt)
}
if !reflect.DeepEqual(characteristics, want) {
t.Errorf("got characteristics %+v, want %+v", characteristics, want)
}
}
func TestCharacteristicsStore_Update_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
characteristic := insertCharacteristic(t, tx)
d := NewDatastore(tx)
// Tweak it
characteristic.CharacteristicName = "Updated Char"
updated, err := d.Characteristics.Update(characteristic.Id, characteristic)
if err != nil {
t.Fatal(err)
}
if !updated {
t.Error("!updated")
}
}
func TestCharacteristicsStore_Delete_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
characteristic := insertCharacteristic(t, tx)
d := NewDatastore(tx)
// Delete it
deleted, err := d.Characteristics.Delete(characteristic.Id)
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Error("!delete")
}
}

View file

@ -1,61 +0,0 @@
package datastore
import (
"errors"
"github.com/jmoiron/modl"
"github.com/thermokarst/bactdb/models"
)
// A datastore access point (in PostgreSQL)
type Datastore struct {
Users models.UsersService
Genera models.GeneraService
Species models.SpeciesService
Strains models.StrainsService
CharacteristicTypes models.CharacteristicTypesService
Characteristics models.CharacteristicsService
TextMeasurementTypes models.TextMeasurementTypesService
UnitTypes models.UnitTypesService
Measurements models.MeasurementsService
dbh modl.SqlExecutor
}
var (
ErrNoRowsUpdated = errors.New(`no rows updated`)
ErrNoRowsDeleted = errors.New(`no rows deleted`)
)
// NewDatastore creates a new client for accessing the datastore (in PostgreSQL).
// If dbh is nil, it uses the global DB handle.
func NewDatastore(dbh modl.SqlExecutor) *Datastore {
if dbh == nil {
dbh = DBH
}
d := &Datastore{dbh: dbh}
d.Users = &usersStore{d}
d.Genera = &generaStore{d}
d.Species = &speciesStore{d}
d.Strains = &strainsStore{d}
d.CharacteristicTypes = &characteristicTypesStore{d}
d.Characteristics = &characteristicsStore{d}
d.TextMeasurementTypes = &textMeasurementTypesStore{d}
d.UnitTypes = &unitTypesStore{d}
d.Measurements = &measurementsStore{d}
return d
}
func NewMockDatastore() *Datastore {
return &Datastore{
Users: &models.MockUsersService{},
Genera: &models.MockGeneraService{},
Species: &models.MockSpeciesService{},
Strains: &models.MockStrainsService{},
CharacteristicTypes: &models.MockCharacteristicTypesService{},
Characteristics: &models.MockCharacteristicsService{},
TextMeasurementTypes: &models.MockTextMeasurementTypesService{},
UnitTypes: &models.MockUnitTypesService{},
Measurements: &models.MockMeasurementsService{},
}
}

View file

@ -1,24 +0,0 @@
package datastore
import (
"fmt"
"time"
"github.com/lib/pq"
"github.com/thermokarst/bactdb/models"
)
func normalizeTime(t ...interface{}) {
for _, v := range t {
switch u := v.(type) {
default:
fmt.Printf("unexpected type %T", u)
case *time.Time:
x, _ := v.(*time.Time)
*x = x.In(time.UTC)
case *models.NullTime:
x, _ := v.(*models.NullTime)
*x = models.NullTime{pq.NullTime{Time: x.Time.In(time.UTC), Valid: x.Valid}}
}
}
}

View file

@ -1,107 +0,0 @@
package datastore
import (
"log"
"os"
"sync"
"github.com/DavidHuie/gomigrate"
"github.com/jmoiron/modl"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
// DB is the global database
var DB = &modl.DbMap{Dialect: modl.PostgresDialect{}}
// DBH is a modl.SqlExecutor interface to DB, the global database. It is better to
// use DBH instead of DB because it prevents you from calling methods that could
// not later be wrapped in a transaction.
var DBH modl.SqlExecutor = DB
var connectOnce sync.Once
// Connect connects to the PostgreSQL database specified by the PG* environment
// variables. It calls log.Fatal if it encounters an error.
func Connect() {
connectOnce.Do(func() {
var err error
conn := setDBCredentials()
DB.Dbx, err = sqlx.Open("postgres", conn)
if err != nil {
log.Fatal("Error connecting to PostgreSQL database (using PG* environment variables): ", err)
}
DB.TraceOn("[modl]", log.New(os.Stdout, "bactdb:", log.Lmicroseconds))
DB.Db = DB.Dbx.DB
})
}
// Create the database schema. It calls log.Fatal if it encounters an error.
func Create(path string) {
migrator, err := gomigrate.NewMigrator(DB.Dbx.DB, gomigrate.Postgres{}, path)
if err != nil {
log.Fatal("Error initializing migrations: ", err)
}
err = migrator.Migrate()
if err != nil {
log.Fatal("Error applying migrations: ", err)
}
}
// Drop the database schema
func Drop(path string) {
migrator, err := gomigrate.NewMigrator(DB.Dbx.DB, gomigrate.Postgres{}, path)
if err != nil {
log.Fatal("Error initializing migrations: ", err)
}
err = migrator.RollbackAll()
if err != nil && err != gomigrate.NoActiveMigrations {
log.Fatal("Error rolling back migrations: ", err)
}
}
// transact calls fn in a DB transaction. If dbh is a transaction, then it just calls
// the function. Otherwise, it begins a transaction, rolling back on failure and
// committing on success.
func transact(dbh modl.SqlExecutor, fn func(fbh modl.SqlExecutor) error) error {
var sharedTx bool
tx, sharedTx := dbh.(*modl.Transaction)
if !sharedTx {
var err error
tx, err = dbh.(*modl.DbMap).Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
}
if err := fn(tx); err != nil {
return err
}
if !sharedTx {
if err := tx.Commit(); err != nil {
return err
}
}
return nil
}
func setDBCredentials() string {
connection := "timezone=UTC "
if heroku := os.Getenv("HEROKU"); heroku == "true" {
url := os.Getenv("DATABASE_URL")
conn, _ := pq.ParseURL(url)
connection += conn
connection += " sslmode=require"
} else {
connection += " sslmode=disable"
}
return connection
}

View file

@ -1,27 +0,0 @@
package datastore
import (
"log"
"os"
"strings"
)
func init() {
// Make sure we don't run the tests on the main DB (will destroy the data)
dbname := os.Getenv("PGDATABASE")
if dbname == "" {
dbname = "bactdbtest"
}
if !strings.HasSuffix(dbname, "test") {
dbname += "test"
}
if err := os.Setenv("PGDATABASE", dbname); err != nil {
log.Fatal(err)
}
// Reset DB
Connect()
migrationsPath := "./migrations"
Drop(migrationsPath)
Create(migrationsPath)
}

View file

@ -1,95 +0,0 @@
package datastore
import (
"strings"
"time"
"github.com/thermokarst/bactdb/models"
)
func init() {
DB.AddTableWithName(models.GenusBase{}, "genera").SetKeys(true, "Id")
}
type generaStore struct {
*Datastore
}
func (s *generaStore) Get(id int64) (*models.Genus, error) {
var genus models.Genus
err := s.dbh.SelectOne(&genus, `SELECT g.*, array_agg(s.id) AS species FROM genera g LEFT OUTER JOIN species s ON s.genus_id=g.id WHERE g.id=$1 GROUP BY g.id;`, id)
if err != nil {
return nil, err
}
if &genus == nil {
return nil, models.ErrGenusNotFound
}
return &genus, nil
}
func (s *generaStore) Create(genus *models.Genus) (bool, error) {
currentTime := time.Now()
genus.CreatedAt = currentTime
genus.UpdatedAt = currentTime
// Ugly --- extract embedded struct
base := genus.GenusBase
if err := s.dbh.Insert(base); err != nil {
if strings.Contains(err.Error(), `violates unique constraint "genus_idx"`) {
return false, err
}
}
genus.Id = base.Id
return true, nil
}
func (s *generaStore) List(opt *models.GenusListOptions) ([]*models.Genus, error) {
if opt == nil {
opt = &models.GenusListOptions{}
}
var genera []*models.Genus
err := s.dbh.Select(&genera, `SELECT g.*, array_agg(s.id) AS species FROM genera g LEFT OUTER JOIN species s ON s.genus_id=g.id GROUP BY g.id LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset())
if err != nil {
return nil, err
}
return genera, nil
}
func (s *generaStore) Update(id int64, genus *models.Genus) (bool, error) {
_, err := s.Get(id)
if err != nil {
return false, err
}
if id != genus.Id {
return false, models.ErrGenusNotFound
}
genus.UpdatedAt = time.Now()
changed, err := s.dbh.Update(genus.GenusBase)
if err != nil {
return false, err
}
if changed == 0 {
return false, ErrNoRowsUpdated
}
return true, nil
}
func (s *generaStore) Delete(id int64) (bool, error) {
genus, err := s.Get(id)
if err != nil {
return false, err
}
deleted, err := s.dbh.Delete(genus.GenusBase)
if err != nil {
return false, err
}
if deleted == 0 {
return false, ErrNoRowsDeleted
}
return true, nil
}

View file

@ -1,125 +0,0 @@
package datastore
import (
"reflect"
"testing"
"github.com/jmoiron/modl"
"github.com/thermokarst/bactdb/models"
)
func insertGenus(t *testing.T, tx *modl.Transaction) *models.Genus {
// Test on a clean database
tx.Exec(`DELETE FROM genera;`)
g := newGenus()
if err := tx.Insert(g); err != nil {
t.Fatal(err)
}
return &models.Genus{g, []int64(nil)}
}
func newGenus() *models.GenusBase {
return &models.GenusBase{GenusName: "Test Genus"}
}
func TestGeneraStore_Get_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want := insertGenus(t, tx)
d := NewDatastore(tx)
genus, err := d.Genera.Get(want.Id)
if err != nil {
t.Fatal(err)
}
normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
normalizeTime(&genus.CreatedAt, &genus.UpdatedAt, &genus.DeletedAt)
if !reflect.DeepEqual(genus, want) {
t.Errorf("got genus %+v, want %+v", genus, want)
}
}
func TestGeneraStore_Create_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
base_genus := newGenus()
genus := models.Genus{base_genus, []int64(nil)}
d := NewDatastore(tx)
created, err := d.Genera.Create(&genus)
if err != nil {
t.Fatal(err)
}
if !created {
t.Error("!created")
}
if genus.Id == 0 {
t.Error("want nonzero genus.Id after submitting")
}
}
func TestGeneraStore_List_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
genus := insertGenus(t, tx)
want := []*models.Genus{genus}
d := NewDatastore(tx)
genera, err := d.Genera.List(&models.GenusListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}})
if err != nil {
t.Fatal(err)
}
for i := range want {
normalizeTime(&want[i].CreatedAt, &want[i].UpdatedAt, &want[i].DeletedAt)
normalizeTime(&genera[i].CreatedAt, &genera[i].UpdatedAt, &genera[i].DeletedAt)
}
if !reflect.DeepEqual(genera, want) {
t.Errorf("got genera %+v, want %+v", genera, want)
}
}
func TestGeneraStore_Update_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
genus := insertGenus(t, tx)
d := NewDatastore(tx)
// Tweak it
genus.GenusName = "Updated Genus"
updated, err := d.Genera.Update(genus.Id, genus)
if err != nil {
t.Fatal(err)
}
if !updated {
t.Error("!updated")
}
}
func TestGeneraStore_Delete_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
genus := insertGenus(t, tx)
d := NewDatastore(tx)
// Delete it
deleted, err := d.Genera.Delete(genus.Id)
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Error("!delete")
}
}

View file

@ -1,108 +0,0 @@
package datastore
import (
"strings"
"time"
"github.com/thermokarst/bactdb/models"
)
func init() {
DB.AddTableWithName(models.Measurement{}, "measurements").SetKeys(true, "Id")
}
type measurementsStore struct {
*Datastore
}
func (s *measurementsStore) Get(id int64) (*models.Measurement, error) {
var measurement models.Measurement
if err := s.dbh.SelectOne(&measurement, `SELECT * FROM measurements WHERE id=$1;`, id); err != nil {
return nil, err
}
if &measurement == nil {
return nil, models.ErrMeasurementNotFound
}
return &measurement, nil
}
func (s *measurementsStore) Create(measurement *models.Measurement) (bool, error) {
currentTime := time.Now()
measurement.CreatedAt = currentTime
measurement.UpdatedAt = currentTime
if err := s.dbh.Insert(measurement); err != nil {
return false, err
}
return true, nil
}
func (s *measurementsStore) List(opt *models.MeasurementListOptions) ([]*models.Measurement, error) {
if opt == nil {
opt = &models.MeasurementListOptions{}
}
sql := `SELECT * FROM measurements`
var conds []string
var vals []interface{}
if opt.Genus != "" {
conds = append(conds, `strain_id IN (SELECT st.id FROM strains st
INNER JOIN species sp ON sp.id = st.species_id
INNER JOIN genera g ON g.id = sp.genus_id
WHERE lower(g.genus_name) = $1)`)
vals = append(vals, opt.Genus)
}
if len(conds) > 0 {
sql += " WHERE (" + strings.Join(conds, ") AND (") + ")"
}
sql += ";"
var measurements []*models.Measurement
err := s.dbh.Select(&measurements, sql, vals...)
if err != nil {
return nil, err
}
return measurements, nil
}
func (s *measurementsStore) Update(id int64, measurement *models.Measurement) (bool, error) {
_, err := s.Get(id)
if err != nil {
return false, err
}
if id != measurement.Id {
return false, models.ErrMeasurementNotFound
}
measurement.UpdatedAt = time.Now()
changed, err := s.dbh.Update(measurement)
if err != nil {
return false, err
}
if changed == 0 {
return false, ErrNoRowsUpdated
}
return true, nil
}
func (s *measurementsStore) Delete(id int64) (bool, error) {
measurement, err := s.Get(id)
if err != nil {
return false, err
}
deleted, err := s.dbh.Delete(measurement)
if err != nil {
return false, err
}
if deleted == 0 {
return false, ErrNoRowsDeleted
}
return true, nil
}

View file

@ -1,139 +0,0 @@
package datastore
import (
"database/sql"
"reflect"
"testing"
"github.com/jmoiron/modl"
"github.com/thermokarst/bactdb/models"
)
func insertMeasurement(t *testing.T, tx *modl.Transaction) *models.Measurement {
// clean up our target table
tx.Exec(`DELETE FROM measurements;`)
measurement := newMeasurement(t, tx)
if err := tx.Insert(measurement); err != nil {
t.Fatal(err)
}
return measurement
}
func newMeasurement(t *testing.T, tx *modl.Transaction) *models.Measurement {
// we have a few things to take care of first...
strain := insertStrain(t, tx)
characteristic := insertCharacteristic(t, tx)
// we want to create and insert a unit type record, too.
unit_type := insertUnitType(t, tx)
return &models.Measurement{
StrainId: strain.Id,
CharacteristicId: characteristic.Id,
NumValue: models.NullFloat64{sql.NullFloat64{Float64: 1.23, Valid: true}},
UnitTypeId: models.NullInt64{sql.NullInt64{Int64: unit_type.Id, Valid: true}},
}
}
func TestMeasurementsStore_Get_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want := insertMeasurement(t, tx)
d := NewDatastore(tx)
measurement, err := d.Measurements.Get(want.Id)
if err != nil {
t.Fatal(err)
}
normalizeTime(&want.CreatedAt, &want.UpdatedAt)
normalizeTime(&measurement.CreatedAt, &measurement.UpdatedAt)
if !reflect.DeepEqual(measurement, want) {
t.Errorf("got measurement %+v, want %+v", measurement, want)
}
}
func TestMeasurementsStore_Create_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
measurement := newMeasurement(t, tx)
d := NewDatastore(tx)
created, err := d.Measurements.Create(measurement)
if err != nil {
t.Fatal(err)
}
if !created {
t.Error("!created")
}
if measurement.Id == 0 {
t.Error("want nonzero measurement.Id after submitting")
}
}
func TestMeasurementsStore_List_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want_measurement := insertMeasurement(t, tx)
want := []*models.Measurement{want_measurement}
d := NewDatastore(tx)
measurements, err := d.Measurements.List(&models.MeasurementListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}})
if err != nil {
t.Fatal(err)
}
for i := range want {
normalizeTime(&want[i].CreatedAt, &want[i].UpdatedAt)
normalizeTime(&measurements[i].CreatedAt, &measurements[i].UpdatedAt)
}
if !reflect.DeepEqual(measurements, want) {
t.Errorf("got measurements %+v, want %+v", measurements, want)
}
}
func TestMeasurementsStore_Update_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
measurement := insertMeasurement(t, tx)
d := NewDatastore(tx)
// Tweak it
measurement.NumValue = models.NullFloat64{sql.NullFloat64{Float64: 4.56, Valid: true}}
updated, err := d.Measurements.Update(measurement.Id, measurement)
if err != nil {
t.Fatal(err)
}
if !updated {
t.Error("!updated")
}
}
func TestMeasurementsStore_Delete_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
measurement := insertMeasurement(t, tx)
d := NewDatastore(tx)
// Delete it
deleted, err := d.Measurements.Delete(measurement.Id)
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Error("!delete")
}
}

View file

@ -1,109 +0,0 @@
package datastore
import (
"strings"
"time"
"github.com/thermokarst/bactdb/models"
)
func init() {
DB.AddTableWithName(models.SpeciesBase{}, "species").SetKeys(true, "Id")
}
type speciesStore struct {
*Datastore
}
func (s *speciesStore) Get(id int64) (*models.Species, error) {
var species models.Species
err := s.dbh.SelectOne(&species, `SELECT sp.*, array_agg(st.id) AS strains FROM species sp LEFT OUTER JOIN strains st ON st.species_id=sp.id WHERE sp.id=$1 GROUP BY sp.id;`, id)
if err != nil {
return nil, err
}
if &species == nil {
return nil, models.ErrSpeciesNotFound
}
return &species, nil
}
func (s *speciesStore) Create(species *models.Species) (bool, error) {
currentTime := time.Now()
species.CreatedAt = currentTime
species.UpdatedAt = currentTime
base := species.SpeciesBase
if err := s.dbh.Insert(base); err != nil {
return false, err
}
species.Id = base.Id
return true, nil
}
func (s *speciesStore) List(opt *models.SpeciesListOptions) ([]*models.Species, error) {
if opt == nil {
opt = &models.SpeciesListOptions{}
}
sql := `SELECT sp.*, array_agg(st.id) AS strains FROM species sp LEFT OUTER JOIN strains st ON st.species_id=sp.id`
var conds []string
var vals []interface{}
if opt.Genus != "" {
conds = append(conds, "sp.genus_id = (SELECT id FROM genera WHERE lower(genus_name) = $1)")
vals = append(vals, opt.Genus)
}
if len(conds) > 0 {
sql += " WHERE (" + strings.Join(conds, ") AND (") + ")"
}
sql += " GROUP BY sp.id;"
var species []*models.Species
err := s.dbh.Select(&species, sql, vals...)
if err != nil {
return nil, err
}
return species, nil
}
func (s *speciesStore) Update(id int64, species *models.Species) (bool, error) {
_, err := s.Get(id)
if err != nil {
return false, err
}
if id != species.Id {
return false, models.ErrSpeciesNotFound
}
species.UpdatedAt = time.Now()
changed, err := s.dbh.Update(species.SpeciesBase)
if err != nil {
return false, err
}
if changed == 0 {
return false, ErrNoRowsUpdated
}
return true, nil
}
func (s *speciesStore) Delete(id int64) (bool, error) {
species, err := s.Get(id)
if err != nil {
return false, err
}
deleted, err := s.dbh.Delete(species.SpeciesBase)
if err != nil {
return false, err
}
if deleted == 0 {
return false, ErrNoRowsDeleted
}
return true, nil
}

View file

@ -1,127 +0,0 @@
package datastore
import (
"reflect"
"testing"
"github.com/jmoiron/modl"
"github.com/thermokarst/bactdb/models"
)
func insertSpecies(t *testing.T, tx *modl.Transaction) *models.Species {
// clean up our target table
tx.Exec(`DELETE FROM species;`)
s := newSpecies(t, tx)
if err := tx.Insert(s); err != nil {
t.Fatal(err)
}
return &models.Species{s, []int64(nil)}
}
func newSpecies(t *testing.T, tx *modl.Transaction) *models.SpeciesBase {
// we want to create and insert a genus record, too
genus := insertGenus(t, tx)
return &models.SpeciesBase{GenusId: genus.Id, SpeciesName: "Test Species"}
}
func TestSpeciesStore_Get_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want := insertSpecies(t, tx)
d := NewDatastore(tx)
species, err := d.Species.Get(want.Id)
if err != nil {
t.Fatal(err)
}
normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
normalizeTime(&species.CreatedAt, &species.UpdatedAt, &species.DeletedAt)
if !reflect.DeepEqual(species, want) {
t.Errorf("got species %+v, want %+v", species, want)
}
}
func TestSpeciesStore_Create_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
base_species := newSpecies(t, tx)
species := models.Species{base_species, []int64(nil)}
d := NewDatastore(tx)
created, err := d.Species.Create(&species)
if err != nil {
t.Fatal(err)
}
if !created {
t.Error("!created")
}
if species.Id == 0 {
t.Error("want nonzero species.Id after submitting")
}
}
func TestSpeciesStore_List_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want_species := insertSpecies(t, tx)
want := []*models.Species{want_species}
d := NewDatastore(tx)
species, err := d.Species.List(&models.SpeciesListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}})
if err != nil {
t.Fatal(err)
}
for i := range want {
normalizeTime(&want[i].CreatedAt, &want[i].UpdatedAt, &want[i].DeletedAt)
normalizeTime(&species[i].CreatedAt, &species[i].UpdatedAt, &species[i].DeletedAt)
}
if !reflect.DeepEqual(species, want) {
t.Errorf("got species %+v, want %+v", species, want)
}
}
func TestSpeciesStore_Update_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
species := insertSpecies(t, tx)
d := NewDatastore(tx)
// Tweak it
species.SpeciesName = "Updated Species"
updated, err := d.Species.Update(species.Id, species)
if err != nil {
t.Fatal(err)
}
if !updated {
t.Error("!updated")
}
}
func TestSpeciesStore_Delete_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
species := insertSpecies(t, tx)
d := NewDatastore(tx)
// Delete it
deleted, err := d.Species.Delete(species.Id)
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Error("!delete")
}
}

View file

@ -1,110 +0,0 @@
package datastore
import (
"strings"
"time"
"github.com/thermokarst/bactdb/models"
)
func init() {
DB.AddTableWithName(models.StrainBase{}, "strains").SetKeys(true, "Id")
}
type strainsStore struct {
*Datastore
}
func (s *strainsStore) Get(id int64) (*models.Strain, error) {
var strain models.Strain
err := s.dbh.SelectOne(&strain, `SELECT s.*, array_agg(m.id) AS measurements FROM strains s LEFT OUTER JOIN measurements m ON m.strain_id=s.id WHERE s.id=$1 GROUP BY s.id;`, id)
if err != nil {
return nil, err
}
if &strain == nil {
return nil, models.ErrStrainNotFound
}
return &strain, nil
}
func (s *strainsStore) Create(strain *models.Strain) (bool, error) {
currentTime := time.Now()
strain.CreatedAt = currentTime
strain.UpdatedAt = currentTime
base := strain.StrainBase
if err := s.dbh.Insert(base); err != nil {
return false, err
}
strain.Id = base.Id
return true, nil
}
func (s *strainsStore) List(opt *models.StrainListOptions) ([]*models.Strain, error) {
if opt == nil {
opt = &models.StrainListOptions{}
}
sql := `SELECT s.*, array_agg(m.id) AS measurements FROM strains s LEFT OUTER JOIN measurements m ON m.strain_id=s.id`
var conds []string
var vals []interface{}
if opt.Genus != "" {
conds = append(conds, `s.species_id IN (SELECT s.id FROM species s
INNER JOIN genera g ON g.id = s.genus_id WHERE lower(g.genus_name) = $1)`)
vals = append(vals, opt.Genus)
}
if len(conds) > 0 {
sql += " WHERE (" + strings.Join(conds, ") AND (") + ")"
}
sql += " GROUP BY s.id;"
var strains []*models.Strain
err := s.dbh.Select(&strains, sql, vals...)
if err != nil {
return nil, err
}
return strains, nil
}
func (s *strainsStore) Update(id int64, strain *models.Strain) (bool, error) {
_, err := s.Get(id)
if err != nil {
return false, err
}
if id != strain.Id {
return false, models.ErrStrainNotFound
}
strain.UpdatedAt = time.Now()
changed, err := s.dbh.Update(strain.StrainBase)
if err != nil {
return false, err
}
if changed == 0 {
return false, ErrNoRowsUpdated
}
return true, nil
}
func (s *strainsStore) Delete(id int64) (bool, error) {
strain, err := s.Get(id)
if err != nil {
return false, err
}
deleted, err := s.dbh.Delete(strain.StrainBase)
if err != nil {
return false, err
}
if deleted == 0 {
return false, ErrNoRowsDeleted
}
return true, nil
}

View file

@ -1,147 +0,0 @@
package datastore
import (
"database/sql"
"reflect"
"testing"
"github.com/jmoiron/modl"
"github.com/thermokarst/bactdb/models"
)
func insertStrain(t *testing.T, tx *modl.Transaction) *models.Strain {
// clean up our target table
tx.Exec(`DELETE FROM strains;`)
s := newStrain(t, tx)
if err := tx.Insert(s); err != nil {
t.Fatal(err)
}
return &models.Strain{s, []int64(nil)}
}
func newStrain(t *testing.T, tx *modl.Transaction) *models.StrainBase {
// we want to create and insert a species (and genus) record too
species := insertSpecies(t, tx)
return &models.StrainBase{
SpeciesId: species.Id,
StrainName: "Test Strain",
StrainType: "Test Type",
Etymology: models.NullString{
sql.NullString{
String: "Test Etymology",
Valid: true,
},
},
AccessionBanks: "Test Bank",
GenbankEmblDdb: models.NullString{
sql.NullString{
String: "Test Genbank",
Valid: true,
},
},
}
}
func TestStrainsStore_Get_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want := insertStrain(t, tx)
d := NewDatastore(tx)
strain, err := d.Strains.Get(want.Id)
if err != nil {
t.Fatal(err)
}
normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
normalizeTime(&strain.CreatedAt, &strain.UpdatedAt, &strain.DeletedAt)
if !reflect.DeepEqual(strain, want) {
t.Errorf("got strain %+v, want %+v", strain, want)
}
}
func TestStrainsStore_Create_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
base_strain := newStrain(t, tx)
strain := models.Strain{base_strain, []int64(nil)}
d := NewDatastore(tx)
created, err := d.Strains.Create(&strain)
if err != nil {
t.Fatal(err)
}
if !created {
t.Error("!created")
}
if strain.Id == 0 {
t.Error("want nonzero strain.Id after submitting")
}
}
func TestStrainsStore_List_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want_strain := insertStrain(t, tx)
want := []*models.Strain{want_strain}
d := NewDatastore(tx)
strains, err := d.Strains.List(&models.StrainListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}})
if err != nil {
t.Fatal(err)
}
for i := range want {
normalizeTime(&want[i].CreatedAt, &want[i].UpdatedAt, &want[i].DeletedAt)
normalizeTime(&strains[i].CreatedAt, &strains[i].UpdatedAt, &strains[i].DeletedAt)
}
if !reflect.DeepEqual(strains, want) {
t.Errorf("got strains %+v, want %+v", strains, want)
}
}
func TestStrainsStore_Update_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
strain := insertStrain(t, tx)
d := NewDatastore(tx)
// Tweak it
strain.StrainName = "Updated Strain"
updated, err := d.Strains.Update(strain.Id, strain)
if err != nil {
t.Fatal(err)
}
if !updated {
t.Error("!updated")
}
}
func TestStrainsStore_Delete_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
strain := insertStrain(t, tx)
d := NewDatastore(tx)
// Delete it
deleted, err := d.Strains.Delete(strain.Id)
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Error("!delete")
}
}

View file

@ -1,87 +0,0 @@
package datastore
import (
"time"
"github.com/thermokarst/bactdb/models"
)
func init() {
DB.AddTableWithName(models.TextMeasurementType{}, "text_measurement_types").SetKeys(true, "Id")
}
type textMeasurementTypesStore struct {
*Datastore
}
func (s *textMeasurementTypesStore) Get(id int64) (*models.TextMeasurementType, error) {
var text_measurement_type models.TextMeasurementType
if err := s.dbh.SelectOne(&text_measurement_type, `SELECT * FROM text_measurement_types WHERE id=$1;`, id); err != nil {
return nil, err
}
if &text_measurement_type == nil {
return nil, models.ErrTextMeasurementTypeNotFound
}
return &text_measurement_type, nil
}
func (s *textMeasurementTypesStore) Create(text_measurement_type *models.TextMeasurementType) (bool, error) {
currentTime := time.Now()
text_measurement_type.CreatedAt = currentTime
text_measurement_type.UpdatedAt = currentTime
if err := s.dbh.Insert(text_measurement_type); err != nil {
return false, err
}
return true, nil
}
func (s *textMeasurementTypesStore) List(opt *models.TextMeasurementTypeListOptions) ([]*models.TextMeasurementType, error) {
if opt == nil {
opt = &models.TextMeasurementTypeListOptions{}
}
var text_measurement_types []*models.TextMeasurementType
err := s.dbh.Select(&text_measurement_types, `SELECT * FROM text_measurement_types LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset())
if err != nil {
return nil, err
}
return text_measurement_types, nil
}
func (s *textMeasurementTypesStore) Update(id int64, text_measurement_type *models.TextMeasurementType) (bool, error) {
_, err := s.Get(id)
if err != nil {
return false, err
}
if id != text_measurement_type.Id {
return false, models.ErrTextMeasurementTypeNotFound
}
text_measurement_type.UpdatedAt = time.Now()
changed, err := s.dbh.Update(text_measurement_type)
if err != nil {
return false, err
}
if changed == 0 {
return false, ErrNoRowsUpdated
}
return true, nil
}
func (s *textMeasurementTypesStore) Delete(id int64) (bool, error) {
text_measurement_type, err := s.Get(id)
if err != nil {
return false, err
}
deleted, err := s.dbh.Delete(text_measurement_type)
if err != nil {
return false, err
}
if deleted == 0 {
return false, ErrNoRowsDeleted
}
return true, nil
}

View file

@ -1,126 +0,0 @@
package datastore
import (
"reflect"
"testing"
"github.com/jmoiron/modl"
"github.com/thermokarst/bactdb/models"
)
func insertTextMeasurementType(t *testing.T, tx *modl.Transaction) *models.TextMeasurementType {
// clean up our target table
tx.Exec(`DELETE FROM text_measurement_types;`)
text_measurement_type := newTextMeasurementType(t, tx)
if err := tx.Insert(text_measurement_type); err != nil {
t.Fatal(err)
}
return text_measurement_type
}
func newTextMeasurementType(t *testing.T, tx *modl.Transaction) *models.TextMeasurementType {
return &models.TextMeasurementType{TextMeasurementName: "Test Text Measurement Type"}
}
func TestTextMeasurementTypesStore_Get_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want := insertTextMeasurementType(t, tx)
d := NewDatastore(tx)
text_measurement_type, err := d.TextMeasurementTypes.Get(want.Id)
if err != nil {
t.Fatal(err)
}
normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
normalizeTime(&text_measurement_type.CreatedAt, &text_measurement_type.UpdatedAt, &text_measurement_type.DeletedAt)
if !reflect.DeepEqual(text_measurement_type, want) {
t.Errorf("got text_measurement_type %+v, want %+v", text_measurement_type, want)
}
}
func TestTextMeasurementTypesStore_Create_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
text_measurement_type := newTextMeasurementType(t, tx)
d := NewDatastore(tx)
created, err := d.TextMeasurementTypes.Create(text_measurement_type)
if err != nil {
t.Fatal(err)
}
if !created {
t.Error("!created")
}
if text_measurement_type.Id == 0 {
t.Error("want nonzero text_measurement_type.Id after submitting")
}
}
func TestTextMeasurementTypesStore_List_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want_text_measurement_type := insertTextMeasurementType(t, tx)
want := []*models.TextMeasurementType{want_text_measurement_type}
d := NewDatastore(tx)
text_measurement_types, err := d.TextMeasurementTypes.List(&models.TextMeasurementTypeListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}})
if err != nil {
t.Fatal(err)
}
for i := range want {
normalizeTime(&want[i].CreatedAt, &want[i].UpdatedAt, &want[i].DeletedAt)
normalizeTime(&text_measurement_types[i].CreatedAt, &text_measurement_types[i].UpdatedAt, &text_measurement_types[i].DeletedAt)
}
if !reflect.DeepEqual(text_measurement_types, want) {
t.Errorf("got text_measurement_types %+v, want %+v", text_measurement_types, want)
}
}
func TestTextMeasurementTypesStore_Update_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
text_measurement_type := insertTextMeasurementType(t, tx)
d := NewDatastore(tx)
// Tweak it
text_measurement_type.TextMeasurementName = "Updated Text Measurement Type"
updated, err := d.TextMeasurementTypes.Update(text_measurement_type.Id, text_measurement_type)
if err != nil {
t.Fatal(err)
}
if !updated {
t.Error("!updated")
}
}
func TestTextMeasurementTypesStore_Delete_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
text_measurement_type := insertTextMeasurementType(t, tx)
d := NewDatastore(tx)
// Delete it
deleted, err := d.TextMeasurementTypes.Delete(text_measurement_type.Id)
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Error("!delete")
}
}

View file

@ -1,87 +0,0 @@
package datastore
import (
"time"
"github.com/thermokarst/bactdb/models"
)
func init() {
DB.AddTableWithName(models.UnitType{}, "unit_types").SetKeys(true, "Id")
}
type unitTypesStore struct {
*Datastore
}
func (s *unitTypesStore) Get(id int64) (*models.UnitType, error) {
var unit_type models.UnitType
if err := s.dbh.SelectOne(&unit_type, `SELECT * FROM unit_types WHERE id=$1;`, id); err != nil {
return nil, err
}
if &unit_type == nil {
return nil, models.ErrUnitTypeNotFound
}
return &unit_type, nil
}
func (s *unitTypesStore) Create(unit_type *models.UnitType) (bool, error) {
currentTime := time.Now()
unit_type.CreatedAt = currentTime
unit_type.UpdatedAt = currentTime
if err := s.dbh.Insert(unit_type); err != nil {
return false, err
}
return true, nil
}
func (s *unitTypesStore) List(opt *models.UnitTypeListOptions) ([]*models.UnitType, error) {
if opt == nil {
opt = &models.UnitTypeListOptions{}
}
var unit_types []*models.UnitType
err := s.dbh.Select(&unit_types, `SELECT * FROM unit_types LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset())
if err != nil {
return nil, err
}
return unit_types, nil
}
func (s *unitTypesStore) Update(id int64, unit_type *models.UnitType) (bool, error) {
_, err := s.Get(id)
if err != nil {
return false, err
}
if id != unit_type.Id {
return false, models.ErrUnitTypeNotFound
}
unit_type.UpdatedAt = time.Now()
changed, err := s.dbh.Update(unit_type)
if err != nil {
return false, err
}
if changed == 0 {
return false, ErrNoRowsUpdated
}
return true, nil
}
func (s *unitTypesStore) Delete(id int64) (bool, error) {
unit_type, err := s.Get(id)
if err != nil {
return false, err
}
deleted, err := s.dbh.Delete(unit_type)
if err != nil {
return false, err
}
if deleted == 0 {
return false, ErrNoRowsDeleted
}
return true, nil
}

View file

@ -1,126 +0,0 @@
package datastore
import (
"reflect"
"testing"
"github.com/jmoiron/modl"
"github.com/thermokarst/bactdb/models"
)
func insertUnitType(t *testing.T, tx *modl.Transaction) *models.UnitType {
// clean up our target table
tx.Exec(`DELETE FROM unit_types;`)
unit_type := newUnitType(t, tx)
if err := tx.Insert(unit_type); err != nil {
t.Fatal(err)
}
return unit_type
}
func newUnitType(t *testing.T, tx *modl.Transaction) *models.UnitType {
return &models.UnitType{Name: "Test Unit Type", Symbol: "x"}
}
func TestUnitTypesStore_Get_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want := insertUnitType(t, tx)
d := NewDatastore(tx)
unit_type, err := d.UnitTypes.Get(want.Id)
if err != nil {
t.Fatal(err)
}
normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
normalizeTime(&unit_type.CreatedAt, &unit_type.UpdatedAt, &unit_type.DeletedAt)
if !reflect.DeepEqual(unit_type, want) {
t.Errorf("got unit_type %+v, want %+v", unit_type, want)
}
}
func TestUnitTypesStore_Create_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
unit_type := newUnitType(t, tx)
d := NewDatastore(tx)
created, err := d.UnitTypes.Create(unit_type)
if err != nil {
t.Fatal(err)
}
if !created {
t.Error("!created")
}
if unit_type.Id == 0 {
t.Error("want nonzero unit_type.Id after submitting")
}
}
func TestUnitTypesStore_List_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want_unit_type := insertUnitType(t, tx)
want := []*models.UnitType{want_unit_type}
d := NewDatastore(tx)
unit_types, err := d.UnitTypes.List(&models.UnitTypeListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}})
if err != nil {
t.Fatal(err)
}
for i := range want {
normalizeTime(&want[i].CreatedAt, &want[i].UpdatedAt, &want[i].DeletedAt)
normalizeTime(&unit_types[i].CreatedAt, &unit_types[i].UpdatedAt, &unit_types[i].DeletedAt)
}
if !reflect.DeepEqual(unit_types, want) {
t.Errorf("got unit_types %+v, want %+v", unit_types, want)
}
}
func TestUnitTypesStore_Update_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
unit_type := insertUnitType(t, tx)
d := NewDatastore(tx)
// Tweak it
unit_type.Name = "Updated Unit Type"
updated, err := d.UnitTypes.Update(unit_type.Id, unit_type)
if err != nil {
t.Fatal(err)
}
if !updated {
t.Error("!updated")
}
}
func TestUnitTypesStore_Delete_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
unit_type := insertUnitType(t, tx)
d := NewDatastore(tx)
// Delete it
deleted, err := d.UnitTypes.Delete(unit_type.Id)
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Error("!delete")
}
}

View file

@ -1,75 +0,0 @@
package datastore
import (
"strings"
"time"
"github.com/thermokarst/bactdb/models"
"golang.org/x/crypto/bcrypt"
)
func init() {
DB.AddTableWithName(models.User{}, "users").SetKeys(true, "Id")
}
type usersStore struct {
*Datastore
}
func (s *usersStore) Get(id int64) (*models.User, error) {
var user models.User
if err := s.dbh.SelectOne(&user, `SELECT * FROM users WHERE id=$1;`, id); err != nil {
return nil, err
}
if &user == nil {
return nil, models.ErrUserNotFound
}
return &user, nil
}
func (s *usersStore) Create(user *models.User) (bool, error) {
currentTime := time.Now()
user.CreatedAt = currentTime
user.UpdatedAt = currentTime
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
if err != nil {
panic(err)
}
user.Password = string(hashedPassword)
if err := s.dbh.Insert(user); err != nil {
if strings.Contains(err.Error(), `violates unique constraint "username_idx"`) {
return false, err
}
}
return true, nil
}
func (s *usersStore) List(opt *models.UserListOptions) ([]*models.User, error) {
if opt == nil {
opt = &models.UserListOptions{}
}
var users []*models.User
err := s.dbh.Select(&users, `SELECT * FROM users LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset())
if err != nil {
return nil, err
}
return users, nil
}
func (s *usersStore) Authenticate(username string, password string) (*models.UserSession, error) {
var users []*models.User
var user_session models.UserSession
if err := s.dbh.Select(&users, `SELECT * FROM users WHERE username=$1;`, username); err != nil {
return nil, err
}
if len(users) == 0 {
return nil, models.ErrUserNotFound
}
if err := bcrypt.CompareHashAndPassword([]byte(users[0].Password), []byte(password)); err != nil {
return nil, err
}
user_session.AccessLevel = "read"
user_session.Genus = "hymenobacter"
return &user_session, nil
}

View file

@ -1,115 +0,0 @@
package datastore
import (
"reflect"
"testing"
"github.com/jmoiron/modl"
"github.com/thermokarst/bactdb/models"
"golang.org/x/crypto/bcrypt"
)
func insertUser(t *testing.T, tx *modl.Transaction) *models.User {
// Test on a clean database
tx.Exec(`DELETE FROM users;`)
user := newUser()
if err := tx.Insert(user); err != nil {
t.Fatal(err)
}
return user
}
func newUser() *models.User {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password"), 10)
return &models.User{
Username: "Test User",
Password: string(hashedPassword),
}
}
func TestUsersStore_Get_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
want := insertUser(t, tx)
d := NewDatastore(tx)
user, err := d.Users.Get(want.Id)
if err != nil {
t.Fatal(err)
}
normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
normalizeTime(&user.CreatedAt, &user.UpdatedAt, &user.DeletedAt)
if !reflect.DeepEqual(user, want) {
t.Errorf("got user %+v, want %+v", user, want)
}
}
func TestUsersStore_Create_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
user := newUser()
d := NewDatastore(tx)
created, err := d.Users.Create(user)
if err != nil {
t.Fatal(err)
}
if !created {
t.Error("!created")
}
if user.Id == 0 {
t.Error("want nonzero user.Id after submitting")
}
}
func TestUsersStore_List_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
user := insertUser(t, tx)
want := []*models.User{user}
d := NewDatastore(tx)
users, err := d.Users.List(&models.UserListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}})
if err != nil {
t.Fatal(err)
}
for i := range want {
normalizeTime(&want[i].CreatedAt, &want[i].UpdatedAt, &want[i].DeletedAt)
normalizeTime(&users[i].CreatedAt, &users[i].UpdatedAt, &users[i].DeletedAt)
}
if !reflect.DeepEqual(users, want) {
t.Errorf("got users %+v, want %+v", users, want)
}
}
func TestUsersStore_Authenticate_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
user := insertUser(t, tx)
want := &models.UserSession{
AccessLevel: "read",
Genus: "hymenobacter",
}
d := NewDatastore(tx)
user_session, err := d.Users.Authenticate(user.Username, "password")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(user_session, want) {
t.Errorf("got session %+v, want %+v", user_session, want)
}
}

View file

@ -1,3 +0,0 @@
FROM postgres:9.3
ADD dbsetup.sh /docker-entrypoint-initdb.d/

View file

@ -1,10 +0,0 @@
#!/bin/bash
gosu postgres postgres --single <<- EOSQL
CREATE USER "$PGUSER" WITH SUPERUSER PASSWORD '$PGPASSWORD';
CREATE DATABASE $PGUSER;
CREATE DATABASE $PGDB;
EOSQL
{ echo; echo "host all \"$PGUSER\" 0.0.0.0/0 md5"; } >> "$PGDATA"/pg_hba.conf

34
fig.yml
View file

@ -1,34 +0,0 @@
data:
build: docker/postgres
environment:
PGUSER: bactdb_user
PGPASSWORD: bactdb_pass
PGDB: bactdbtest
volumes:
- /var/lib/postgresql/data
command: true
db:
image: bactdb_data
environment:
PGUSER: bactdb_user
PGPASSWORD: bactdb_pass
PGDB: bactdbtest
ports:
- "5432:5432"
volumes_from:
- data
app:
build: .
environment:
PGUSER: bactdb_user
PGPASSWORD: bactdb_pass
PGDB: bactdbtest
ports:
- "8901:8901"
volumes:
- .:/bactdb
links:
- db

98
handlers.go Normal file
View file

@ -0,0 +1,98 @@
package main
import (
"net/http"
"os"
"strings"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/mux"
)
func Handler() http.Handler {
m := mux.NewRouter()
// Non-auth routes
m.HandleFunc("/authenticate", serveAuthenticateUser).Methods("POST")
// Path-based pattern matching subrouter
s := m.PathPrefix("/{genus}").Subrouter()
// Strains
s.Handle("/strains", authHandler(serveStrainsList)).Methods("GET")
s.Handle("/strains/{Id:.+}", authHandler(serveStrain)).Methods("GET")
// Measurements
s.Handle("/measurements", authHandler(serveMeasurementsList)).Methods("GET")
s.Handle("/measurements/{Id:.+}", authHandler(serveMeasurement)).Methods("GET")
return corsHandler(m)
}
func corsHandler(h http.Handler) http.Handler {
cors := func(w http.ResponseWriter, r *http.Request) {
domains := os.Getenv("DOMAINS")
allowedDomains := strings.Split(domains, ",")
if origin := r.Header.Get("Origin"); origin != "" {
for _, s := range allowedDomains {
if s == origin {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Headers", r.Header.Get("Access-Control-Request-Headers"))
w.Header().Set("Access-Control-Allow-Methods", r.Header.Get("Access-Control-Request-Method"))
}
}
}
if r.Method != "OPTIONS" {
h.ServeHTTP(w, r)
}
}
return http.HandlerFunc(cors)
}
// Only accessible with a valid token
func authHandler(f func(http.ResponseWriter, *http.Request)) http.Handler {
h := http.HandlerFunc(f)
auth := func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, errPleaseLogIn.Error(), http.StatusUnauthorized)
return
}
s := strings.Split(authHeader, " ")
// Validate the token
token, err := jwt.Parse(s[1], func(token *jwt.Token) (interface{}, error) {
return []byte(verifyKey), nil
})
// Branch out into the possible error from signing
switch err.(type) {
case nil: // No error
if !token.Valid { // But may still be invalid
http.Error(w, errPleaseLogIn.Error(), http.StatusUnauthorized)
return
}
case *jwt.ValidationError: // Something was wrong during the validation
vErr := err.(*jwt.ValidationError)
switch vErr.Errors {
case jwt.ValidationErrorExpired:
http.Error(w, errTokenExpired.Error(), http.StatusUnauthorized)
return
default:
http.Error(w, errGenericError.Error(), http.StatusInternalServerError)
return
}
default: // Something else went wrong
http.Error(w, errGenericError.Error(), http.StatusInternalServerError)
return
}
genus := mux.Vars(r)["genus"]
// We don't care about this if we aren't accessing one of the subrouter routes.
if genus != "" && genus != token.Claims["genus"] {
http.Error(w, errAccessDenied.Error(), http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(auth)
}

29
helpers.go Normal file
View file

@ -0,0 +1,29 @@
package main
// ListOptions specifies general pagination options for fetching a list of results
type ListOptions struct {
PerPage int `url:",omitempty" json:",omitempty"`
Page int `url:",omitempty" json:",omitempty"`
Ids []int `url:",omitempty" json:",omitempty" schema:"ids[]"`
}
func (o ListOptions) PageOrDefault() int {
if o.Page <= 0 {
return 1
}
return o.Page
}
func (o ListOptions) Offset() int {
return (o.PageOrDefault() - 1) * o.PerPageOrDefault()
}
func (o ListOptions) PerPageOrDefault() int {
if o.PerPage <= 0 {
return DefaultPerPage
}
return o.PerPage
}
// DefaultPerPage is the default number of items to return in a paginated result set
const DefaultPerPage = 10

124
main.go Normal file
View file

@ -0,0 +1,124 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"sync"
"github.com/DavidHuie/gomigrate"
"github.com/codegangsta/cli"
"github.com/gorilla/schema"
"github.com/jmoiron/modl"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
var (
DB = &modl.DbMap{Dialect: modl.PostgresDialect{}}
DBH modl.SqlExecutor = DB
schemaDecoder = schema.NewDecoder()
)
func main() {
var connectOnce sync.Once
connectOnce.Do(func() {
var err error
connection := "timezone=UTC "
if heroku := os.Getenv("HEROKU"); heroku == "true" {
url := os.Getenv("DATABASE_URL")
conn, _ := pq.ParseURL(url)
connection += conn
connection += " sslmode=require"
} else {
connection += " sslmode=disable"
}
DB.Dbx, err = sqlx.Open("postgres", connection)
if err != nil {
log.Fatal("Error connecting to PostgreSQL database (using PG* environment variables): ", err)
}
DB.TraceOn("[modl]", log.New(os.Stdout, "bactdb:", log.Lmicroseconds))
DB.Db = DB.Dbx.DB
})
app := cli.NewApp()
app.Name = "bactdb"
app.Usage = "a database for bacteria"
app.Authors = []cli.Author{
cli.Author{
Name: "Matthew Ryan Dillon",
Email: "mrdillon@alaska.edu",
},
}
app.Version = "0.1.0"
app.Commands = []cli.Command{
{
Name: "serve",
ShortName: "s",
Usage: "Start web server",
Action: cmdServe,
},
{
Name: "migrate",
ShortName: "m",
Usage: "Migrate the database schema",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "drop",
Usage: "Drop DB before migrating",
},
cli.StringFlag{
Name: "migration_path",
Usage: "Path to migration DDL",
Value: "./migrations",
},
},
Action: cmdMigrateDb,
},
}
app.Run(os.Args)
}
func cmdServe(c *cli.Context) {
var err error
addr := os.Getenv("PORT")
if addr == "" {
addr = "8901"
}
httpAddr := fmt.Sprintf(":%v", addr)
err = setupCerts()
if err != nil {
log.Fatal("SetupCerts: ", err)
}
m := http.NewServeMux()
m.Handle("/api/", http.StripPrefix("/api", Handler()))
log.Print("Listening on ", httpAddr)
err = http.ListenAndServe(httpAddr, m)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
func cmdMigrateDb(c *cli.Context) {
migrationsPath := c.String("migration_path")
migrator, err := gomigrate.NewMigrator(DB.Dbx.DB, gomigrate.Postgres{}, migrationsPath)
if err != nil {
log.Fatal("Error initializing migrations: ", err)
}
if c.Bool("drop") {
if err = migrator.RollbackAll(); err != nil && err != gomigrate.NoActiveMigrations {
log.Fatal("Error rolling back migrations: ", err)
}
}
// Run migrations
if err = migrator.Migrate(); err != nil {
log.Fatal("Error applying migrations: ", err)
}
}

169
measurements.go Normal file
View file

@ -0,0 +1,169 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
)
var ErrMeasurementNotFound = errors.New("measurement not found")
func init() {
DB.AddTableWithName(MeasurementBase{}, "measurements").SetKeys(true, "Id")
}
// There are three types of supported measurements: fixed-test, free-text,
// & numerical. The table has a constraint that will allow one or the other
// for a particular combination of strain & characteristic, but not both.
// MeasurementBase is what the DB expects to see for inserts/updates
type MeasurementBase struct {
Id int64 `json:"id,omitempty"`
StrainId int64 `db:"strain_id" json:"strain"`
CharacteristicId int64 `db:"characteristic_id" json:"-"`
TextMeasurementTypeId NullInt64 `db:"text_measurement_type_id" json:"-"`
TxtValue NullString `db:"txt_value" json:"txtValue"`
NumValue NullFloat64 `db:"num_value" json:"numValue"`
ConfidenceInterval NullFloat64 `db:"confidence_interval" json:"confidenceInterval"`
UnitTypeId NullInt64 `db:"unit_type_id" json:"-"`
Notes NullString `db:"notes" json:"notes"`
TestMethodId NullInt64 `db:"test_method_id" json:"-"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
}
// Measurement & MeasurementJSON(s) are what ember expects to see
type Measurement struct {
*MeasurementBase
Characteristic NullString `db:"characteristic_name" json:"characteristic"`
TextMeasurementType NullString `db:"text_measurement_type_name" json:"textMeasurementType"`
UnitType NullString `db:"unit_type_name" json:"unitType"`
TestMethod NullString `db:"test_method_name" json:"testMethod"`
}
type MeasurementJSON struct {
Measurement *Measurement `json:"measurement"`
}
type MeasurementsJSON struct {
Measurements []*Measurement `json:"measurements"`
}
type MeasurementListOptions struct {
ListOptions
Genus string
}
func serveMeasurementsList(w http.ResponseWriter, r *http.Request) {
var opt MeasurementListOptions
if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
opt.Genus = mux.Vars(r)["genus"]
measurements, err := dbGetMeasurements(&opt)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if measurements == nil {
measurements = []*Measurement{}
}
data, err := json.Marshal(measurements)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Write(data)
}
func serveMeasurement(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
measurement, err := dbGetMeasurement(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data, err := json.Marshal(measurement)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Write(data)
}
func dbGetMeasurements(opt *MeasurementListOptions) ([]*Measurement, error) {
if opt == nil {
return nil, errors.New("must provide options")
}
var vals []interface{}
sql := `SELECT m.*, c.characteristic_name,
t.text_measurement_name AS text_measurement_type_name,
u.symbol AS unit_type_name, te.name AS test_method_name
FROM measurements m
INNER JOIN strains st ON st.id=m.strain_id
INNER JOIN species sp ON sp.id=st.species_id
INNER JOIN genera g ON g.id=sp.genus_id AND LOWER(g.genus_name)=$1
LEFT OUTER JOIN characteristics c ON c.id=m.characteristic_id
LEFT OUTER JOIN text_measurement_types t ON t.id=m.text_measurement_type_id
LEFT OUTER JOIN unit_types u ON u.id=m.unit_type_id
LEFT OUTER JOIN test_methods te ON te.id=m.test_method_id`
vals = append(vals, opt.Genus)
if len(opt.Ids) != 0 {
var conds []string
m := "m.id IN ("
for i, id := range opt.Ids {
m = m + fmt.Sprintf("$%v,", i+2) // start param index at 2
vals = append(vals, id)
}
m = m[:len(m)-1] + ")"
conds = append(conds, m)
sql += " WHERE (" + strings.Join(conds, ") AND (") + ")"
}
sql += ";"
var measurements []*Measurement
err := DBH.Select(&measurements, sql, vals...)
if err != nil {
return nil, err
}
return measurements, nil
}
func dbGetMeasurement(id int64) (*Measurement, error) {
var measurement Measurement
sql := `SELECT m.*, c.characteristic_name,
t.text_measurement_name AS text_measurement_type_name,
u.symbol AS unit_type_name, te.name AS test_method_name
FROM measurements m
LEFT OUTER JOIN characteristics c ON c.id=m.characteristic_id
LEFT OUTER JOIN text_measurement_types t ON t.id=m.text_measurement_type_id
LEFT OUTER JOIN unit_types u ON u.id=m.unit_type_id
LEFT OUTER JOIN test_methods te ON te.id=m.test_method_id
WHERE m.id=$1;`
if err := DBH.SelectOne(&measurement, sql, id); err != nil {
return nil, err
}
if &measurement == nil {
return nil, ErrMeasurementNotFound
}
return &measurement, nil
}

Some files were not shown because too many files have changed in this diff Show more