From 6030310caa4012f85cb1c09ed57600d30cf5bb75 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Fri, 20 Mar 2015 15:52:29 -0800 Subject: [PATCH] Rebooting --- Dockerfile | 7 - Godeps/Godeps.json | 28 +- .../github.com/DavidHuie/gomigrate/README.md | 2 + .../DavidHuie/gomigrate/gomigrate.go | 47 ++- .../DavidHuie/gomigrate/gomigrate_test.go | 38 ++- .../test_migrations/test1_pg/1_test_down.sql | 2 +- .../test_migrations/test1_pg/1_test_up.sql | 2 +- ...eate_index_function_if_not_exists_down.sql | 1 + ...create_index_function_if_not_exists_up.sql | 23 ++ .../test1_pg/3_multiple_inserts_down.sql | 1 + .../test1_pg/3_multiple_inserts_up.sql | 5 + .../test1_pg/4_dash-test_down.sql | 1 + .../test1_pg/4_dash-test_up.sql | 1 + .../github.com/DavidHuie/gomigrate/utils.go | 9 +- .../src/github.com/codegangsta/cli/README.md | 14 +- .../src/github.com/codegangsta/cli/app.go | 70 ++++- .../github.com/codegangsta/cli/app_test.go | 106 ++++++- .../github.com/codegangsta/cli/cli_test.go | 28 +- .../src/github.com/codegangsta/cli/command.go | 27 +- .../codegangsta/cli/command_test.go | 4 +- .../src/github.com/codegangsta/cli/context.go | 5 + .../codegangsta/cli/context_test.go | 12 + .../src/github.com/codegangsta/cli/help.go | 36 ++- .../src/github.com/dgrijalva/jwt-go/README.md | 2 +- .../dgrijalva/jwt-go/example_test.go | 2 +- .../src/github.com/dgrijalva/jwt-go/hmac.go | 2 + .../google/go-querystring/query/encode.go | 274 ------------------ .../go-querystring/query/encode_test.go | 238 --------------- .../src/github.com/gorilla/mux/mux.go | 13 + .../src/github.com/gorilla/mux/mux_test.go | 33 +++ .../src/github.com/gorilla/mux/regexp.go | 6 +- .../src/github.com/gorilla/mux/route.go | 57 +++- .../src/github.com/gorilla/schema/decoder.go | 26 +- .../github.com/gorilla/schema/decoder_test.go | 41 +++ .../src/github.com/lib/pq/README.md | 3 + .../src/github.com/lib/pq/bench_test.go | 3 +- .../_workspace/src/github.com/lib/pq/buf.go | 1 + .../_workspace/src/github.com/lib/pq/conn.go | 17 +- .../_workspace/src/github.com/lib/pq/doc.go | 3 +- .../src/github.com/lib/pq/encode.go | 118 ++++++-- .../src/github.com/lib/pq/encode_test.go | 147 +++++++++- .../github.com/lib/pq/hstore/hstore_test.go | 3 +- .../github.com/lib/pq/listen_example/doc.go | 4 +- .../src/github.com/lib/pq/notify_test.go | 1 + .../src/github.com/lib/pq/oid/gen.go | 2 +- README.md | 19 +- api/auth.go | 101 ------- api/characteristic_types.go | 92 ------ api/characteristic_types_test.go | 153 ---------- api/characteristics.go | 92 ------ api/characteristics_test.go | 153 ---------- api/genera.go | 94 ------ api/genera_test.go | 152 ---------- api/handler.go | 94 ------ api/helpers.go | 37 --- api/helpers_test.go | 25 -- api/measurements.go | 111 ------- api/measurements_test.go | 160 ---------- api/server_for_test.go | 97 ------- api/species.go | 111 ------- api/species_test.go | 154 ---------- api/strains.go | 111 ------- api/strains_test.go | 155 ---------- api/text_measurement_types.go | 92 ------ api/text_measurement_types_test.go | 153 ---------- api/unit_types.go | 92 ------ api/unit_types_test.go | 153 ---------- api/users.go | 87 ------ api/users_test.go | 127 -------- auth.go | 36 +++ bactdb.go | 101 ------- datastore/characteristic_types.go | 87 ------ datastore/characteristic_types_test.go | 126 -------- datastore/characteristics.go | 90 ------ datastore/characteristics_test.go | 130 --------- datastore/datastore.go | 61 ---- datastore/datastore_test.go | 24 -- datastore/db.go | 107 ------- datastore/db_test.go | 27 -- datastore/genera.go | 95 ------ datastore/genera_test.go | 125 -------- datastore/measurements.go | 108 ------- datastore/measurements_test.go | 139 --------- datastore/species.go | 109 ------- datastore/species_test.go | 127 -------- datastore/strains.go | 110 ------- datastore/strains_test.go | 147 ---------- datastore/text_measurement_types.go | 87 ------ datastore/text_measurement_types_test.go | 126 -------- datastore/unit_types.go | 87 ------ datastore/unit_types_test.go | 126 -------- datastore/users.go | 75 ----- datastore/users_test.go | 115 -------- docker/postgres/Dockerfile | 3 - docker/postgres/dbsetup.sh | 10 - fig.yml | 34 --- handlers.go | 98 +++++++ helpers.go | 29 ++ main.go | 124 ++++++++ measurements.go | 169 +++++++++++ .../00001_AddUsers_down.sql | 0 .../00001_AddUsers_up.sql | 0 .../00002_AddGenera_down.sql | 0 .../00002_AddGenera_up.sql | 0 .../00003_AddSpecies_down.sql | 0 .../00003_AddSpecies_up.sql | 0 .../00004_AddStrain_down.sql | 0 .../00004_AddStrain_up.sql | 0 .../00005_AddCharacteristicTypes_down.sql | 0 .../00005_AddCharacteristicTypes_up.sql | 0 .../00006_AddCharacteristics_down.sql | 0 .../00006_AddCharacteristics_up.sql | 0 .../00007_AddText_Measurement_Types_down.sql | 0 .../00007_AddText_Measurement_Types_up.sql | 0 .../00008_AddUnit_Types_down.sql | 0 .../00008_AddUnit_Types_up.sql | 0 .../00009_AddTest_Methods_down.sql | 0 .../00009_AddTest_Methods_up.sql | 0 .../00010_AddMeasurements_down.sql | 0 .../00010_AddMeasurements_up.sql | 0 models/characteristic_types.go | 208 ------------- models/characteristic_types_test.go | 174 ----------- models/characteristics.go | 225 -------------- models/characteristics_test.go | 175 ----------- models/client.go | 211 -------------- models/client_test.go | 107 ------- models/errors.go | 56 ---- models/genera.go | 226 --------------- models/genera_test.go | 177 ----------- models/measurements.go | 228 --------------- models/measurements_test.go | 180 ------------ models/species.go | 223 -------------- models/species_test.go | 177 ----------- models/strains.go | 253 ---------------- models/strains_test.go | 179 ------------ models/text_measurement_types.go | 208 ------------- models/text_measurement_types_test.go | 174 ----------- models/unit_types.go | 210 -------------- models/unit_types_test.go | 174 ----------- models/users.go | 191 ------------ models/users_test.go | 114 -------- router/api.go | 80 ----- router/routes.go | 62 ---- shippable.yml | 37 --- strains.go | 157 ++++++++++ test.sh | 25 -- testpackages.txt | 3 - models/types.go => types.go | 2 +- users.go | 96 ++++++ 149 files changed, 1489 insertions(+), 9755 deletions(-) delete mode 100644 Dockerfile create mode 100644 Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/2_create_index_function_if_not_exists_down.sql create mode 100644 Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/2_create_index_function_if_not_exists_up.sql create mode 100644 Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/3_multiple_inserts_down.sql create mode 100644 Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/3_multiple_inserts_up.sql create mode 100644 Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/4_dash-test_down.sql create mode 100644 Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/4_dash-test_up.sql delete mode 100644 Godeps/_workspace/src/github.com/google/go-querystring/query/encode.go delete mode 100644 Godeps/_workspace/src/github.com/google/go-querystring/query/encode_test.go delete mode 100644 api/auth.go delete mode 100644 api/characteristic_types.go delete mode 100644 api/characteristic_types_test.go delete mode 100644 api/characteristics.go delete mode 100644 api/characteristics_test.go delete mode 100644 api/genera.go delete mode 100644 api/genera_test.go delete mode 100644 api/handler.go delete mode 100644 api/helpers.go delete mode 100644 api/helpers_test.go delete mode 100644 api/measurements.go delete mode 100644 api/measurements_test.go delete mode 100644 api/server_for_test.go delete mode 100644 api/species.go delete mode 100644 api/species_test.go delete mode 100644 api/strains.go delete mode 100644 api/strains_test.go delete mode 100644 api/text_measurement_types.go delete mode 100644 api/text_measurement_types_test.go delete mode 100644 api/unit_types.go delete mode 100644 api/unit_types_test.go delete mode 100644 api/users.go delete mode 100644 api/users_test.go create mode 100644 auth.go delete mode 100644 bactdb.go delete mode 100644 datastore/characteristic_types.go delete mode 100644 datastore/characteristic_types_test.go delete mode 100644 datastore/characteristics.go delete mode 100644 datastore/characteristics_test.go delete mode 100644 datastore/datastore.go delete mode 100644 datastore/datastore_test.go delete mode 100644 datastore/db.go delete mode 100644 datastore/db_test.go delete mode 100644 datastore/genera.go delete mode 100644 datastore/genera_test.go delete mode 100644 datastore/measurements.go delete mode 100644 datastore/measurements_test.go delete mode 100644 datastore/species.go delete mode 100644 datastore/species_test.go delete mode 100644 datastore/strains.go delete mode 100644 datastore/strains_test.go delete mode 100644 datastore/text_measurement_types.go delete mode 100644 datastore/text_measurement_types_test.go delete mode 100644 datastore/unit_types.go delete mode 100644 datastore/unit_types_test.go delete mode 100644 datastore/users.go delete mode 100644 datastore/users_test.go delete mode 100644 docker/postgres/Dockerfile delete mode 100644 docker/postgres/dbsetup.sh delete mode 100644 fig.yml create mode 100644 handlers.go create mode 100644 helpers.go create mode 100644 main.go create mode 100644 measurements.go rename {datastore/migrations => migrations}/00001_AddUsers_down.sql (100%) rename {datastore/migrations => migrations}/00001_AddUsers_up.sql (100%) rename {datastore/migrations => migrations}/00002_AddGenera_down.sql (100%) rename {datastore/migrations => migrations}/00002_AddGenera_up.sql (100%) rename {datastore/migrations => migrations}/00003_AddSpecies_down.sql (100%) rename {datastore/migrations => migrations}/00003_AddSpecies_up.sql (100%) rename {datastore/migrations => migrations}/00004_AddStrain_down.sql (100%) rename {datastore/migrations => migrations}/00004_AddStrain_up.sql (100%) rename {datastore/migrations => migrations}/00005_AddCharacteristicTypes_down.sql (100%) rename {datastore/migrations => migrations}/00005_AddCharacteristicTypes_up.sql (100%) rename {datastore/migrations => migrations}/00006_AddCharacteristics_down.sql (100%) rename {datastore/migrations => migrations}/00006_AddCharacteristics_up.sql (100%) rename {datastore/migrations => migrations}/00007_AddText_Measurement_Types_down.sql (100%) rename {datastore/migrations => migrations}/00007_AddText_Measurement_Types_up.sql (100%) rename {datastore/migrations => migrations}/00008_AddUnit_Types_down.sql (100%) rename {datastore/migrations => migrations}/00008_AddUnit_Types_up.sql (100%) rename {datastore/migrations => migrations}/00009_AddTest_Methods_down.sql (100%) rename {datastore/migrations => migrations}/00009_AddTest_Methods_up.sql (100%) rename {datastore/migrations => migrations}/00010_AddMeasurements_down.sql (100%) rename {datastore/migrations => migrations}/00010_AddMeasurements_up.sql (100%) delete mode 100644 models/characteristic_types.go delete mode 100644 models/characteristic_types_test.go delete mode 100644 models/characteristics.go delete mode 100644 models/characteristics_test.go delete mode 100644 models/client.go delete mode 100644 models/client_test.go delete mode 100644 models/errors.go delete mode 100644 models/genera.go delete mode 100644 models/genera_test.go delete mode 100644 models/measurements.go delete mode 100644 models/measurements_test.go delete mode 100644 models/species.go delete mode 100644 models/species_test.go delete mode 100644 models/strains.go delete mode 100644 models/strains_test.go delete mode 100644 models/text_measurement_types.go delete mode 100644 models/text_measurement_types_test.go delete mode 100644 models/unit_types.go delete mode 100644 models/unit_types_test.go delete mode 100644 models/users.go delete mode 100644 models/users_test.go delete mode 100644 router/api.go delete mode 100644 router/routes.go delete mode 100644 shippable.yml create mode 100644 strains.go delete mode 100755 test.sh delete mode 100644 testpackages.txt rename models/types.go => types.go (99%) create mode 100644 users.go diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 08aa59e..0000000 --- a/Dockerfile +++ /dev/null @@ -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 - diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 83bbd47..e35d263 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,24 +1,20 @@ { "ImportPath": "github.com/thermokarst/bactdb", - "GoVersion": "go1.4.1", + "GoVersion": "go1.4.2", "Deps": [ { "ImportPath": "github.com/DavidHuie/gomigrate", - "Rev": "7883dd76040debc099811feadd58425b1495103d" + "Rev": "cfb6d067cc41d95acfa2e5f496dd236346a8175e" }, { "ImportPath": "github.com/codegangsta/cli", - "Comment": "1.2.0-62-gbf4a526", - "Rev": "bf4a526f48af7badd25d2cb02d587e1b01be3b50" + "Comment": "1.2.0-93-g2bcd11f", + "Rev": "2bcd11f863d540a1b190dc03b6c4f634c6ae91d4" }, { "ImportPath": "github.com/dgrijalva/jwt-go", - "Comment": "v2.2.0-11-g7c18dce", - "Rev": "7c18dce7b8ff32db34d4b7054d488db656d75cc3" - }, - { - "ImportPath": "github.com/google/go-querystring/query", - "Rev": "d8840cbb2baa915f4836edda4750050a2c0b7aea" + "Comment": "v2.2.0-15-g61124b6", + "Rev": "61124b62ad244d655f87d944aefaa2ae5a0d2f16" }, { "ImportPath": "github.com/gorilla/context", @@ -26,11 +22,11 @@ }, { "ImportPath": "github.com/gorilla/mux", - "Rev": "e444e69cbd2e2e3e0749a2f3c717cec491552bbf" + "Rev": "8a875a034c69b940914d83ea03d3f1299b4d094b" }, { "ImportPath": "github.com/gorilla/schema", - "Rev": "ad8849d14e5cdf1f4423b1f0fc53827ebbdd7477" + "Rev": "c8422571edf3131506bab7df27e18980fe2598d5" }, { "ImportPath": "github.com/jmoiron/modl", @@ -43,16 +39,16 @@ }, { "ImportPath": "github.com/lib/pq", - "Comment": "go1.0-cutoff-13-g19eeca3", - "Rev": "19eeca3e30d2577b1761db471ec130810e67f532" + "Comment": "go1.0-cutoff-29-g30ed220", + "Rev": "30ed2200d7ec99cf17272292f1d4b7b0bd7165db" }, { "ImportPath": "golang.org/x/crypto/bcrypt", - "Rev": "632d287f9f3f54b09809eebbd3cacbcb00b9f2fc" + "Rev": "1351f936d976c60a0a48d728281922cf63eafb8d" }, { "ImportPath": "golang.org/x/crypto/blowfish", - "Rev": "632d287f9f3f54b09809eebbd3cacbcb00b9f2fc" + "Rev": "1351f936d976c60a0a48d728281922cf63eafb8d" } ] } diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/README.md b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/README.md index e76d305..4b4f45f 100644 --- a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/README.md +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/README.md @@ -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 migration should run relative to the other migrations. +`id` should not be `0` as that value is used for internal validations. + ### Example If I'm trying to add a "users" table to the database, I would create diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/gomigrate.go b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/gomigrate.go index e224fd7..68d3611 100644 --- a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/gomigrate.go +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/gomigrate.go @@ -3,7 +3,6 @@ package gomigrate import ( - "bytes" "database/sql" "errors" "io/ioutil" @@ -219,37 +218,25 @@ func (m *Migrator) ApplyMigration(migration *Migration, mType migrationType) err return err } - for n, subMigration := range splitMigrationString(string(sql)) { - if allWhitespace.Match([]byte(subMigration)) { - continue + // Perform the migration. + result, err := transaction.Exec(string(sql)) + if err != nil { + log.Printf("Error executing migration: %v", err) + if rollbackErr := transaction.Rollback(); rollbackErr != nil { + log.Printf("Error rolling back transaction: %v", rollbackErr) + return rollbackErr } - - log.Printf("Applying submigration: %v", n+1) - - for _, line := range bytes.Split([]byte(subMigration), []byte("\n")) { - log.Printf("MIGRATION: %s", line) - } - - // Perform the migration. - result, err := transaction.Exec(string(subMigration)) - if err != nil { - log.Printf("Error executing migration: %v", err) - if rollbackErr := transaction.Rollback(); rollbackErr != nil { - log.Printf("Error rolling back transaction: %v", rollbackErr) - return rollbackErr - } - return err - } - if rowsAffected, err := result.RowsAffected(); err != nil { - log.Printf("Error getting rows affected: %v", err) - if rollbackErr := transaction.Rollback(); rollbackErr != nil { - log.Printf("Error rolling back transaction: %v", rollbackErr) - return rollbackErr - } - return err - } else { - log.Printf("Rows affected: %v", rowsAffected) + return err + } + if rowsAffected, err := result.RowsAffected(); err != nil { + log.Printf("Error getting rows affected: %v", err) + if rollbackErr := transaction.Rollback(); rollbackErr != nil { + log.Printf("Error rolling back transaction: %v", rollbackErr) + return rollbackErr } + return err + } else { + log.Printf("Rows affected: %v", rowsAffected) } // Log the event. diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/gomigrate_test.go b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/gomigrate_test.go index 2c1fa23..14f68f9 100644 --- a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/gomigrate_test.go +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/gomigrate_test.go @@ -16,7 +16,7 @@ var ( adapter Migratable ) -func GetMigrator(test string) *Migrator { +func GetMigrator(test string) (*Migrator, string) { var suffix string if os.Getenv("DB") == "pg" { suffix = "pg" @@ -28,12 +28,16 @@ func GetMigrator(test string) *Migrator { if err != nil { panic(err) } - return m + return m, suffix } func TestNewMigrator(t *testing.T) { - m := GetMigrator("test1") - if len(m.migrations) != 1 { + m, d := GetMigrator("test1") + 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") } @@ -43,10 +47,10 @@ func TestNewMigrator(t *testing.T) { t.Errorf("Invalid migration name detected: %s", migration.Name) } 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 { - t.Errorf("Invalid migration num detected: %s", migration.Status) + t.Errorf("Invalid migration num detected: %d", migration.Status) } cleanup() @@ -79,7 +83,7 @@ func TestCreatingMigratorWhenTableExists(t *testing.T) { } func TestMigrationAndRollback(t *testing.T) { - m := GetMigrator("test1") + m, d := GetMigrator("test1") if err := m.Migrate(); err != nil { t.Error(err) @@ -110,8 +114,16 @@ func TestMigrationAndRollback(t *testing.T) { t.Error("Invalid status for migration") } - if err := m.Rollback(); err != nil { - t.Error(err) + if d == "pg" { + if err := m.RollbackN(4); err != nil { + t.Error(err) + } + } + + if d == "mysql" { + if err := m.Rollback(); err != nil { + t.Error(err) + } } // Ensure that the down migration ran. @@ -120,8 +132,8 @@ func TestMigrationAndRollback(t *testing.T) { "test", ) err := row.Scan(&tableName) - if err != sql.ErrNoRows { - t.Errorf("Migration table should be deleted") + if err != nil && err != sql.ErrNoRows { + t.Errorf("Migration table should be deleted: %v", err) } // Ensure that the migration log is missing. @@ -129,11 +141,11 @@ func TestMigrationAndRollback(t *testing.T) { adapter.GetMigrationSql(), 1, ) - if err := row.Scan(&status); err != sql.ErrNoRows { + if err := row.Scan(&status); err != nil && err != sql.ErrNoRows { t.Error(err) } 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() diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/1_test_down.sql b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/1_test_down.sql index a1807be..28bfbae 100644 --- a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/1_test_down.sql +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/1_test_down.sql @@ -1 +1 @@ -drop table test; +drop table if exists test; diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/1_test_up.sql b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/1_test_up.sql index 24bb66f..613ee12 100644 --- a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/1_test_up.sql +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/1_test_up.sql @@ -1 +1 @@ -create table test(); +create table if not exists test(); diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/2_create_index_function_if_not_exists_down.sql b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/2_create_index_function_if_not_exists_down.sql new file mode 100644 index 0000000..12dc15d --- /dev/null +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/2_create_index_function_if_not_exists_down.sql @@ -0,0 +1 @@ +drop function if exists create_index_if_not_exists (t_name text, i_name text, index_sql text); \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/2_create_index_function_if_not_exists_up.sql b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/2_create_index_function_if_not_exists_up.sql new file mode 100644 index 0000000..806a4a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/2_create_index_function_if_not_exists_up.sql @@ -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; \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/3_multiple_inserts_down.sql b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/3_multiple_inserts_down.sql new file mode 100644 index 0000000..05fe21e --- /dev/null +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/3_multiple_inserts_down.sql @@ -0,0 +1 @@ +drop table tt; diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/3_multiple_inserts_up.sql b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/3_multiple_inserts_up.sql new file mode 100644 index 0000000..3ab6ea5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/3_multiple_inserts_up.sql @@ -0,0 +1,5 @@ +CREATE TABLE tt ( +c text NOT NULL +); +insert into tt values ('a'); +insert into tt values ('x'); diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/4_dash-test_down.sql b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/4_dash-test_down.sql new file mode 100644 index 0000000..ab290eb --- /dev/null +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/4_dash-test_down.sql @@ -0,0 +1 @@ +select 1; diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/4_dash-test_up.sql b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/4_dash-test_up.sql new file mode 100644 index 0000000..ab290eb --- /dev/null +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/test_migrations/test1_pg/4_dash-test_up.sql @@ -0,0 +1 @@ +select 1; diff --git a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/utils.go b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/utils.go index c5267bc..91b13dd 100644 --- a/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/utils.go +++ b/Godeps/_workspace/src/github.com/DavidHuie/gomigrate/utils.go @@ -7,8 +7,8 @@ import ( ) var ( - upMigrationFile = regexp.MustCompile(`(\d+)_(\w+)_up\.sql`) - downMigrationFile = regexp.MustCompile(`(\d+)_(\w+)_down\.sql`) + upMigrationFile = regexp.MustCompile(`(\d+)_([\w-]+)_up\.sql`) + downMigrationFile = regexp.MustCompile(`(\d+)_([\w-]+)_down\.sql`) subMigrationSplit = 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 } -// 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. type uint64slice []uint64 diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/README.md b/Godeps/_workspace/src/github.com/codegangsta/cli/README.md index 0e8327b..3a183b1 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/README.md +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/README.md @@ -210,7 +210,7 @@ Subcommands can be defined for a more git-like command line app. app.Commands = []cli.Command{ { Name: "add", - ShortName: "a", + Names: []string{"a"}, Usage: "add a task to the list", Action: func(c *cli.Context) { println("added task: ", c.Args().First()) @@ -218,7 +218,7 @@ app.Commands = []cli.Command{ }, { Name: "complete", - ShortName: "c", + Names: []string{"c"}, Usage: "complete a task on the list", Action: func(c *cli.Context) { println("completed task: ", c.Args().First()) @@ -226,7 +226,7 @@ app.Commands = []cli.Command{ }, { Name: "template", - ShortName: "r", + Names: []string{"r"}, Usage: "options for task templates", Subcommands: []cli.Command{ { @@ -244,7 +244,7 @@ app.Commands = []cli.Command{ }, }, }, - }, + }, } ... ``` @@ -262,8 +262,8 @@ app := cli.NewApp() app.EnableBashCompletion = true app.Commands = []cli.Command{ { - Name: "complete", - ShortName: "c", + Name: "complete", + Names: []string{"c"}, Usage: "complete a task on the list", Action: func(c *cli.Context) { println("completed task: ", c.Args().First()) @@ -293,6 +293,6 @@ setting the `PROG` variable to the name of your program: ## 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. -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. diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/app.go b/Godeps/_workspace/src/github.com/codegangsta/cli/app.go index deb8e7f..cd29005 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/app.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/app.go @@ -5,6 +5,7 @@ import ( "io" "io/ioutil" "os" + "strings" "text/tabwriter" "text/template" "time" @@ -34,15 +35,20 @@ type App struct { // 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 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 Action func(context *Context) // Execute this function if the proper command cannot be found CommandNotFound func(context *Context, command string) // Compilation date 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 e-mail + // Email of Author (Note: Use App.Authors, this is deprecated) Email string // Writer writer to write output to Writer io.Writer @@ -67,22 +73,28 @@ func NewApp() *App { BashComplete: DefaultAppComplete, Action: helpCommand.Action, Compiled: compileTime(), - Author: "Author", - Email: "unknown@email", Writer: os.Stdout, } } // 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 { defer func() { HelpPrinter = nil }() HelpPrinter = func(templ string, data interface{}) { + funcMap := template.FuncMap{ + "join": strings.Join, + } + 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) if err != nil { panic(err) @@ -111,7 +123,7 @@ func (a *App) Run(arguments []string) error { // parse flags set := flagSet(a.Name, a.Flags) set.SetOutput(ioutil.Discard) - err := set.Parse(arguments[1:]) + err = set.Parse(arguments[1:]) nerr := normalizeFlags(a.Flags, set) if nerr != nil { fmt.Fprintln(a.Writer, nerr) @@ -141,6 +153,15 @@ func (a *App) Run(arguments []string) error { 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 { err := a.Before(context) 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 -func (a *App) RunAsSubcommand(ctx *Context) error { +func (a *App) RunAsSubcommand(ctx *Context) (err error) { // append help to commands if len(a.Commands) > 0 { if a.Command(helpCommand.Name) == nil && !a.HideHelp { @@ -190,7 +211,7 @@ func (a *App) RunAsSubcommand(ctx *Context) error { // parse flags set := flagSet(a.Name, a.Flags) set.SetOutput(ioutil.Discard) - err := set.Parse(ctx.Args().Tail()) + err = set.Parse(ctx.Args().Tail()) nerr := normalizeFlags(a.Flags, set) 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 { err := a.Before(context) if err != nil { @@ -242,11 +272,7 @@ func (a *App) RunAsSubcommand(ctx *Context) error { } // Run default Action - if len(a.Commands) > 0 { - a.Action(context) - } else { - a.Action(ctx) - } + a.Action(context) return nil } @@ -277,3 +303,19 @@ func (a *App) appendFlag(flag 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) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go index 78f0c10..4a40b89 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go @@ -21,6 +21,9 @@ func ExampleApp() { app.Action = func(c *cli.Context) { 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) // Output: // Hello Jeremy @@ -34,13 +37,13 @@ func ExampleAppSubcommand() { app.Commands = []cli.Command{ { Name: "hello", - ShortName: "hi", + Aliases: []string{"hi"}, Usage: "use it to see a description", Description: "This is how we describe hello the function", Subcommands: []cli.Command{ { Name: "english", - ShortName: "en", + Aliases: []string{"en"}, Usage: "sends a greeting in english", Description: "greets someone in english", Flags: []cli.Flag{ @@ -75,7 +78,7 @@ func ExampleAppHelp() { app.Commands = []cli.Command{ { Name: "describeit", - ShortName: "d", + Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", Action: func(c *cli.Context) { @@ -105,7 +108,7 @@ func ExampleAppBashComplete() { app.Commands = []cli.Command{ { Name: "describeit", - ShortName: "d", + Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", Action: func(c *cli.Context) { @@ -159,8 +162,8 @@ var commandAppTests = []struct { func TestApp_Command(t *testing.T) { app := cli.NewApp() - fooCommand := cli.Command{Name: "foobar", ShortName: "f"} - batCommand := cli.Command{Name: "batbaz", ShortName: "b"} + fooCommand := cli.Command{Name: "foobar", Aliases: []string{"f"}} + batCommand := cli.Command{Name: "batbaz", Aliases: []string{"b"}} app.Commands = []cli.Command{ fooCommand, batCommand, @@ -193,6 +196,32 @@ func TestApp_CommandWithArgBeforeFlags(t *testing.T) { 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) { var parsedOption 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) { oldFlag := cli.HelpFlag defer func() { diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go index 879a793..8a8df97 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go @@ -12,17 +12,17 @@ func Example() { app.Usage = "task list on the command line" app.Commands = []cli.Command{ { - Name: "add", - ShortName: "a", - Usage: "add a task to the list", + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", Action: func(c *cli.Context) { println("added task: ", c.Args().First()) }, }, { - Name: "complete", - ShortName: "c", - Usage: "complete a task on the list", + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", Action: func(c *cli.Context) { println("completed task: ", c.Args().First()) }, @@ -38,13 +38,13 @@ func ExampleSubcommand() { app.Commands = []cli.Command{ { Name: "hello", - ShortName: "hi", + Aliases: []string{"hi"}, Usage: "use it to see a description", Description: "This is how we describe hello the function", Subcommands: []cli.Command{ { Name: "english", - ShortName: "en", + Aliases: []string{"en"}, Usage: "sends a greeting in english", Description: "greets someone in english", Flags: []cli.Flag{ @@ -58,9 +58,9 @@ func ExampleSubcommand() { println("Hello, ", c.String("name")) }, }, { - Name: "spanish", - ShortName: "sp", - Usage: "sends a greeting in spanish", + Name: "spanish", + Aliases: []string{"sp"}, + Usage: "sends a greeting in spanish", Flags: []cli.Flag{ cli.StringFlag{ Name: "surname", @@ -72,9 +72,9 @@ func ExampleSubcommand() { println("Hola, ", c.String("surname")) }, }, { - Name: "french", - ShortName: "fr", - Usage: "sends a greeting in french", + Name: "french", + Aliases: []string{"fr"}, + Usage: "sends a greeting in french", Flags: []cli.Flag{ cli.StringFlag{ Name: "nickname", diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/command.go b/Godeps/_workspace/src/github.com/codegangsta/cli/command.go index ffd3ef8..b61691c 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/command.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/command.go @@ -10,8 +10,10 @@ import ( type Command struct { // The name of the command Name string - // short name of the command. Typically one character + // short name of the command. Typically one character (deprecated, use `Aliases`) ShortName string + // A list of aliases for the command + Aliases []string // A short description of the usage of this command Usage string // 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 // If a non-nil error is returned, no sub-subcommands are run 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 Action func(context *Context) // 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 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) } @@ -114,9 +119,24 @@ func (c Command) Run(ctx *Context) error { 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 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 { @@ -146,6 +166,7 @@ func (c Command) startApp(ctx *Context) error { // set the actions app.Before = c.Before + app.After = c.After if c.Action != nil { app.Action = c.Action } else { diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go index c0f556a..4125b0c 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go @@ -17,7 +17,7 @@ func TestCommandDoNotIgnoreFlags(t *testing.T) { command := cli.Command{ Name: "test-cmd", - ShortName: "tc", + Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", Action: func(_ *cli.Context) {}, @@ -37,7 +37,7 @@ func TestCommandIgnoreFlags(t *testing.T) { command := cli.Command{ Name: "test-cmd", - ShortName: "tc", + Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", Action: func(_ *cli.Context) {}, diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/context.go b/Godeps/_workspace/src/github.com/codegangsta/cli/context.go index c9f645b..37221bd 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/context.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/context.go @@ -106,6 +106,11 @@ func (c *Context) GlobalGeneric(name string) interface{} { 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 func (c *Context) IsSet(name string) bool { if c.setFlags == nil { diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go index 7c9a443..d4a1877 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go @@ -97,3 +97,15 @@ func TestContext_GlobalIsSet(t *testing.T) { expect(t, c.GlobalIsSet("myflagGlobalUnset"), 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) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/help.go b/Godeps/_workspace/src/github.com/codegangsta/cli/help.go index bfb2788..7c4f81b 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/help.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/help.go @@ -12,14 +12,13 @@ USAGE: {{.Name}} {{if .Flags}}[global options] {{end}}command{{if .Flags}} [command options]{{end}} [arguments...] VERSION: - {{.Version}}{{if or .Author .Email}} - -AUTHOR:{{if .Author}} - {{.Author}}{{if .Email}} - <{{.Email}}>{{end}}{{else}} - {{.Email}}{{end}}{{end}} + {{.Version}} +AUTHOR(S): + {{range .Authors}}{{ . }} + {{end}} COMMANDS: - {{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} + {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} {{end}}{{if .Flags}} GLOBAL OPTIONS: {{range .Flags}}{{.}} @@ -53,7 +52,7 @@ USAGE: {{.Name}} command{{if .Flags}} [command options]{{end}} [arguments...] COMMANDS: - {{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} + {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} {{end}}{{if .Flags}} OPTIONS: {{range .Flags}}{{.}} @@ -61,9 +60,9 @@ OPTIONS: ` var helpCommand = Command{ - Name: "help", - ShortName: "h", - Usage: "Shows a list of commands or help for one command", + Name: "help", + Aliases: []string{"h"}, + Usage: "Shows a list of commands or help for one command", Action: func(c *Context) { args := c.Args() if args.Present() { @@ -75,9 +74,9 @@ var helpCommand = Command{ } var helpSubcommand = Command{ - Name: "help", - ShortName: "h", - Usage: "Shows a list of commands or help for one command", + Name: "help", + Aliases: []string{"h"}, + Usage: "Shows a list of commands or help for one command", Action: func(c *Context) { args := c.Args() if args.Present() { @@ -103,15 +102,20 @@ func ShowAppHelp(c *Context) { // Prints the list of subcommands as the default app completion method func DefaultAppComplete(c *Context) { for _, command := range c.App.Commands { - fmt.Fprintln(c.App.Writer, command.Name) - if command.ShortName != "" { - fmt.Fprintln(c.App.Writer, command.ShortName) + for _, name := range command.Names() { + fmt.Fprintln(c.App.Writer, name) } } } // Prints help for the given command 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 { if c.HasName(command) { HelpPrinter(CommandHelpTemplate, c) diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/README.md b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/README.md index f0a8f9d..7471d1e 100644 --- a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/README.md +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/README.md @@ -34,7 +34,7 @@ Parsing and verifying tokens is pretty straight forward. You pass in the token ```go // Create the token - token := jwt.New(SigningMethodHS256) + token := jwt.New(jwt.SigningMethodHS256) // Set some claims token.Claims["foo"] = "bar" token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix() diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/example_test.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/example_test.go index bfd7271..edb48e4 100644 --- a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/example_test.go +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/example_test.go @@ -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 token := jwt.New(jwt.SigningMethodHS256) // Set some claims diff --git a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac.go b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac.go index 2d7f1bf..402ff08 100644 --- a/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac.go +++ b/Godeps/_workspace/src/github.com/dgrijalva/jwt-go/hmac.go @@ -66,6 +66,8 @@ func (m *SigningMethodHMAC) Verify(signingString, signature string, key interfac 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) { if keyBytes, ok := key.([]byte); ok { if !m.Hash.Available() { diff --git a/Godeps/_workspace/src/github.com/google/go-querystring/query/encode.go b/Godeps/_workspace/src/github.com/google/go-querystring/query/encode.go deleted file mode 100644 index 31d36e7..0000000 --- a/Godeps/_workspace/src/github.com/google/go-querystring/query/encode.go +++ /dev/null @@ -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 -} diff --git a/Godeps/_workspace/src/github.com/google/go-querystring/query/encode_test.go b/Godeps/_workspace/src/github.com/google/go-querystring/query/encode_test.go deleted file mode 100644 index 8afbd0b..0000000 --- a/Godeps/_workspace/src/github.com/google/go-querystring/query/encode_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/Godeps/_workspace/src/github.com/gorilla/mux/mux.go b/Godeps/_workspace/src/github.com/gorilla/mux/mux.go index 5b5f8e7..af31d23 100644 --- a/Godeps/_workspace/src/github.com/gorilla/mux/mux.go +++ b/Godeps/_workspace/src/github.com/gorilla/mux/mux.go @@ -152,6 +152,13 @@ func (r *Router) getRegexpGroup() *routeRegexpGroup { 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 // ---------------------------------------------------------------------------- @@ -224,6 +231,12 @@ func (r *Router) Schemes(schemes ...string) *Route { 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 // ---------------------------------------------------------------------------- diff --git a/Godeps/_workspace/src/github.com/gorilla/mux/mux_test.go b/Godeps/_workspace/src/github.com/gorilla/mux/mux_test.go index e455bce..c37be8a 100644 --- a/Godeps/_workspace/src/github.com/gorilla/mux/mux_test.go +++ b/Godeps/_workspace/src/github.com/gorilla/mux/mux_test.go @@ -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) { subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter() subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter() diff --git a/Godeps/_workspace/src/github.com/gorilla/mux/regexp.go b/Godeps/_workspace/src/github.com/gorilla/mux/regexp.go index a630548..aa30679 100644 --- a/Godeps/_workspace/src/github.com/gorilla/mux/regexp.go +++ b/Godeps/_workspace/src/github.com/gorilla/mux/regexp.go @@ -150,11 +150,7 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { } // url builds a URL part using the given values. -func (r *routeRegexp) url(pairs ...string) (string, error) { - values, err := mapFromPairs(pairs...) - if err != nil { - return "", err - } +func (r *routeRegexp) url(values map[string]string) (string, error) { urlValues := make([]interface{}, len(r.varsN)) for k, v := range r.varsN { value, ok := values[v] diff --git a/Godeps/_workspace/src/github.com/gorilla/mux/route.go b/Godeps/_workspace/src/github.com/gorilla/mux/route.go index c310e66..d4f0146 100644 --- a/Godeps/_workspace/src/github.com/gorilla/mux/route.go +++ b/Godeps/_workspace/src/github.com/gorilla/mux/route.go @@ -31,6 +31,8 @@ type Route struct { name string // Error resulted from building a route. err error + + buildVarsFunc BuildVarsFunc } // Match matches the route against the request. @@ -360,6 +362,19 @@ func (r *Route) Schemes(schemes ...string) *Route { 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 creates a subrouter for the route. @@ -422,17 +437,20 @@ func (r *Route) URL(pairs ...string) (*url.URL, error) { if r.regexp == nil { 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 err error if r.regexp.host != nil { // Set a default scheme. 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 } } 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 } } @@ -453,7 +471,11 @@ func (r *Route) URLHost(pairs ...string) (*url.URL, error) { if r.regexp == nil || r.regexp.host == nil { 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 { return nil, err } @@ -473,7 +495,11 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { if r.regexp == nil || r.regexp.path == nil { 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 { return nil, err } @@ -482,6 +508,26 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { }, 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 // ---------------------------------------------------------------------------- @@ -490,6 +536,7 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { type parentRoute interface { getNamedRoutes() map[string]*Route getRegexpGroup() *routeRegexpGroup + buildVars(map[string]string) map[string]string } // getNamedRoutes returns the map where named routes are registered. diff --git a/Godeps/_workspace/src/github.com/gorilla/schema/decoder.go b/Godeps/_workspace/src/github.com/gorilla/schema/decoder.go index 4fdce69..6d880f1 100644 --- a/Godeps/_workspace/src/github.com/gorilla/schema/decoder.go +++ b/Godeps/_workspace/src/github.com/gorilla/schema/decoder.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "reflect" + "strings" ) // NewDecoder returns a new Decoder. @@ -156,9 +157,28 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, } items = append(items, item) } else { - // If a single value is invalid should we give up - // or set a zero value? - return ConversionError{path, key} + if strings.Contains(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} + } + } + + } else { + return ConversionError{path, key} + } } } value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...) diff --git a/Godeps/_workspace/src/github.com/gorilla/schema/decoder_test.go b/Godeps/_workspace/src/github.com/gorilla/schema/decoder_test.go index 44b0600..0188a24 100644 --- a/Godeps/_workspace/src/github.com/gorilla/schema/decoder_test.go +++ b/Godeps/_workspace/src/github.com/gorilla/schema/decoder_test.go @@ -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) + } +} diff --git a/Godeps/_workspace/src/github.com/lib/pq/README.md b/Godeps/_workspace/src/github.com/lib/pq/README.md index cb15d6f..aa4350e 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/README.md +++ b/Godeps/_workspace/src/github.com/lib/pq/README.md @@ -61,6 +61,8 @@ code still exists in here. * Dan Sosedoff (sosedoff) * Daniel Farina (fdr) * Eric Chlebek (echlebek) +* Eric Garrido (minusnine) +* Eric Urban (hydrogen18) * Everyone at The Go Team * Evan Shaw (edsrzf) * Ewan Chou (coocood) @@ -94,5 +96,6 @@ code still exists in here. * Ryan Smith (ryandotsmith) * Samuel Stauffer (samuel) * Timothée Peignier (cyberdelia) +* Travis Cline (tmc) * TruongSinh Tran-Nguyen (truongsinh) * notedit (notedit) diff --git a/Godeps/_workspace/src/github.com/lib/pq/bench_test.go b/Godeps/_workspace/src/github.com/lib/pq/bench_test.go index f2eb1da..611edf8 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/bench_test.go +++ b/Godeps/_workspace/src/github.com/lib/pq/bench_test.go @@ -7,7 +7,6 @@ import ( "bytes" "database/sql" "database/sql/driver" - "github.com/lib/pq/oid" "io" "math/rand" "net" @@ -17,6 +16,8 @@ import ( "sync" "testing" "time" + + "github.com/lib/pq/oid" ) var ( diff --git a/Godeps/_workspace/src/github.com/lib/pq/buf.go b/Godeps/_workspace/src/github.com/lib/pq/buf.go index 9f417a1..fd966c3 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/buf.go +++ b/Godeps/_workspace/src/github.com/lib/pq/buf.go @@ -3,6 +3,7 @@ package pq import ( "bytes" "encoding/binary" + "github.com/lib/pq/oid" ) diff --git a/Godeps/_workspace/src/github.com/lib/pq/conn.go b/Godeps/_workspace/src/github.com/lib/pq/conn.go index 44e4833..1a1f09a 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/conn.go +++ b/Godeps/_workspace/src/github.com/lib/pq/conn.go @@ -10,7 +10,6 @@ import ( "encoding/binary" "errors" "fmt" - "github.com/lib/pq/oid" "io" "io/ioutil" "net" @@ -22,6 +21,8 @@ import ( "strings" "time" "unicode" + + "github.com/lib/pq/oid" ) // Common error types @@ -212,17 +213,17 @@ func DialOpen(d Dialer, name string) (_ driver.Conn, err error) { cn.buf = bufio.NewReader(cn.c) cn.startup(o) // reset the deadline, in case one was set (see dial) - err = cn.c.SetDeadline(time.Time{}) + if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" { + err = cn.c.SetDeadline(time.Time{}) + } return cn, err } func dial(d Dialer, o values) (net.Conn, error) { ntw, addr := network(o) - timeout := o.Get("connect_timeout") - // 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) if err != nil { 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") if err != nil { + if cn.isInTransaction() { + cn.bad = true + } return err } if commandTag != "COMMIT" { @@ -455,6 +459,9 @@ func (cn *conn) Rollback() (err error) { cn.checkIsInTransaction(true) _, commandTag, err := cn.simpleExec("ROLLBACK") if err != nil { + if cn.isInTransaction() { + cn.bad = true + } return err } if commandTag != "ROLLBACK" { diff --git a/Godeps/_workspace/src/github.com/lib/pq/doc.go b/Godeps/_workspace/src/github.com/lib/pq/doc.go index 4d7a0e3..f772117 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/doc.go +++ b/Godeps/_workspace/src/github.com/lib/pq/doc.go @@ -5,8 +5,9 @@ In most cases clients will use the database/sql package instead of using this package directly. For example: import ( - _ "github.com/lib/pq" "database/sql" + + _ "github.com/lib/pq" ) func main() { diff --git a/Godeps/_workspace/src/github.com/lib/pq/encode.go b/Godeps/_workspace/src/github.com/lib/pq/encode.go index 3887f72..556986a 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/encode.go +++ b/Godeps/_workspace/src/github.com/lib/pq/encode.go @@ -5,12 +5,13 @@ import ( "database/sql/driver" "encoding/hex" "fmt" - "github.com/lib/pq/oid" "math" "strconv" "strings" "sync" "time" + + "github.com/lib/pq/oid" ) 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 { 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 if (typ == oid.T_timestamptz || typ == oid.T_timetz) && str[len(str)-3] == ':' { @@ -212,11 +207,72 @@ func (c *locationCache) getLocation(offset int) *time.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 // setting ("ISO, MDY"), the only one we currently support. This // accounts for the discrepancies between the parsing available with // 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, '-') // this is Gregorian year, not ISO Year // 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 } -// formatTs formats t as time.RFC3339Nano and appends time zone seconds if -// needed. +// formatTs formats t into a format postgres understands. 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 // minus sign preferred by Go. // Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on @@ -324,25 +388,26 @@ func formatTs(t time.Time) (b []byte) { bc = true } b = []byte(t.Format(time.RFC3339Nano)) - if bc { - b = append(b, " BC"...) - } _, offset := t.Zone() offset = offset % 60 - if offset == 0 { - return b + if offset != 0 { + // RFC3339Nano already printed the minus sign + if offset < 0 { + offset = -offset + } + + b = append(b, ':') + if offset < 10 { + b = append(b, '0') + } + b = strconv.AppendInt(b, int64(offset), 10) } - if offset < 0 { - offset = -offset + if bc { + b = append(b, " BC"...) } - - b = append(b, ':') - if offset < 10 { - b = append(b, '0') - } - return strconv.AppendInt(b, int64(offset), 10) + return b } // 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) { if serverVersion >= 90000 { // 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 { // .. or resort to "escape" for _, b := range v { diff --git a/Godeps/_workspace/src/github.com/lib/pq/encode_test.go b/Godeps/_workspace/src/github.com/lib/pq/encode_test.go index e835f2b..9a33ee0 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/encode_test.go +++ b/Godeps/_workspace/src/github.com/lib/pq/encode_test.go @@ -1,12 +1,12 @@ package pq import ( - "github.com/lib/pq/oid" - "bytes" "fmt" "testing" "time" + + "github.com/lib/pq/oid" ) func TestScanTimestamp(t *testing.T) { @@ -78,7 +78,11 @@ func tryParse(str string) (t time.Time, err error) { 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 } @@ -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("", 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(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(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) { @@ -249,6 +263,131 @@ func TestTimestampWithOutTimezone(t *testing.T) { 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) { db := openTestConn(t) defer db.Close() diff --git a/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore_test.go b/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore_test.go index 8e61e69..c9c108f 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore_test.go +++ b/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore_test.go @@ -2,9 +2,10 @@ package hstore import ( "database/sql" - _ "github.com/lib/pq" "os" "testing" + + _ "github.com/lib/pq" ) type Fatalistic interface { diff --git a/Godeps/_workspace/src/github.com/lib/pq/listen_example/doc.go b/Godeps/_workspace/src/github.com/lib/pq/listen_example/doc.go index 34496f4..5bc99f5 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/listen_example/doc.go +++ b/Godeps/_workspace/src/github.com/lib/pq/listen_example/doc.go @@ -18,11 +18,11 @@ mechanism to avoid polling the database while waiting for more work to arrive. package main import ( - "github.com/lib/pq" - "database/sql" "fmt" "time" + + "github.com/lib/pq" ) func doWork(db *sql.DB, work int64) { diff --git a/Godeps/_workspace/src/github.com/lib/pq/notify_test.go b/Godeps/_workspace/src/github.com/lib/pq/notify_test.go index ae1208d..4fc3cdd 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/notify_test.go +++ b/Godeps/_workspace/src/github.com/lib/pq/notify_test.go @@ -43,6 +43,7 @@ func expectEvent(t *testing.T, eventch <-chan ListenerEventType, et ListenerEven } return nil case <-time.After(1500 * time.Millisecond): + panic("expectEvent timeout") return fmt.Errorf("timeout") } } diff --git a/Godeps/_workspace/src/github.com/lib/pq/oid/gen.go b/Godeps/_workspace/src/github.com/lib/pq/oid/gen.go index f16a51c..cd4aea8 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/oid/gen.go +++ b/Godeps/_workspace/src/github.com/lib/pq/oid/gen.go @@ -5,12 +5,12 @@ package main import ( + "database/sql" "fmt" "log" "os" "os/exec" - "database/sql" _ "github.com/lib/pq" ) diff --git a/README.md b/README.md index 55cd323..9488ef4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,23 @@ 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 + + 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 diff --git a/api/auth.go b/api/auth.go deleted file mode 100644 index 7fd0088..0000000 --- a/api/auth.go +++ /dev/null @@ -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}) - } -} diff --git a/api/characteristic_types.go b/api/characteristic_types.go deleted file mode 100644 index 3febdb4..0000000 --- a/api/characteristic_types.go +++ /dev/null @@ -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{}) -} diff --git a/api/characteristic_types_test.go b/api/characteristic_types_test.go deleted file mode 100644 index 3b0b7ab..0000000 --- a/api/characteristic_types_test.go +++ /dev/null @@ -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") - } -} diff --git a/api/characteristics.go b/api/characteristics.go deleted file mode 100644 index 79b8a9a..0000000 --- a/api/characteristics.go +++ /dev/null @@ -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) -} diff --git a/api/characteristics_test.go b/api/characteristics_test.go deleted file mode 100644 index 75f4bf7..0000000 --- a/api/characteristics_test.go +++ /dev/null @@ -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") - } -} diff --git a/api/genera.go b/api/genera.go deleted file mode 100644 index ff10d43..0000000 --- a/api/genera.go +++ /dev/null @@ -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) -} diff --git a/api/genera_test.go b/api/genera_test.go deleted file mode 100644 index ca0bf2a..0000000 --- a/api/genera_test.go +++ /dev/null @@ -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") - } -} diff --git a/api/handler.go b/api/handler.go deleted file mode 100644 index e25e097..0000000 --- a/api/handler.go +++ /dev/null @@ -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"}) -} diff --git a/api/helpers.go b/api/helpers.go deleted file mode 100644 index 46c8b2f..0000000 --- a/api/helpers.go +++ /dev/null @@ -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()}) -} diff --git a/api/helpers_test.go b/api/helpers_test.go deleted file mode 100644 index 966aedc..0000000 --- a/api/helpers_test.go +++ /dev/null @@ -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) -} diff --git a/api/measurements.go b/api/measurements.go deleted file mode 100644 index 622855f..0000000 --- a/api/measurements.go +++ /dev/null @@ -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}) -} diff --git a/api/measurements_test.go b/api/measurements_test.go deleted file mode 100644 index 4caca5e..0000000 --- a/api/measurements_test.go +++ /dev/null @@ -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") - } -} diff --git a/api/server_for_test.go b/api/server_for_test.go deleted file mode 100644 index 9637664..0000000 --- a/api/server_for_test.go +++ /dev/null @@ -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 -} diff --git a/api/species.go b/api/species.go deleted file mode 100644 index c91686b..0000000 --- a/api/species.go +++ /dev/null @@ -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}) -} diff --git a/api/species_test.go b/api/species_test.go deleted file mode 100644 index 91d52b6..0000000 --- a/api/species_test.go +++ /dev/null @@ -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") - } -} diff --git a/api/strains.go b/api/strains.go deleted file mode 100644 index 807a102..0000000 --- a/api/strains.go +++ /dev/null @@ -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}) -} diff --git a/api/strains_test.go b/api/strains_test.go deleted file mode 100644 index 172ec3f..0000000 --- a/api/strains_test.go +++ /dev/null @@ -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") - } -} diff --git a/api/text_measurement_types.go b/api/text_measurement_types.go deleted file mode 100644 index 08c5b37..0000000 --- a/api/text_measurement_types.go +++ /dev/null @@ -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{}) -} diff --git a/api/text_measurement_types_test.go b/api/text_measurement_types_test.go deleted file mode 100644 index ad1c726..0000000 --- a/api/text_measurement_types_test.go +++ /dev/null @@ -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") - } -} diff --git a/api/unit_types.go b/api/unit_types.go deleted file mode 100644 index 9a4be14..0000000 --- a/api/unit_types.go +++ /dev/null @@ -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{}) -} diff --git a/api/unit_types_test.go b/api/unit_types_test.go deleted file mode 100644 index db39d97..0000000 --- a/api/unit_types_test.go +++ /dev/null @@ -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") - } -} diff --git a/api/users.go b/api/users.go deleted file mode 100644 index 9cd6726..0000000 --- a/api/users.go +++ /dev/null @@ -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) -} diff --git a/api/users_test.go b/api/users_test.go deleted file mode 100644 index d145926..0000000 --- a/api/users_test.go +++ /dev/null @@ -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) - } -} diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..bebf6a6 --- /dev/null +++ b/auth.go @@ -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 +} diff --git a/bactdb.go b/bactdb.go deleted file mode 100644 index 56946fd..0000000 --- a/bactdb.go +++ /dev/null @@ -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) - } - } -} diff --git a/datastore/characteristic_types.go b/datastore/characteristic_types.go deleted file mode 100644 index a40109c..0000000 --- a/datastore/characteristic_types.go +++ /dev/null @@ -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 -} diff --git a/datastore/characteristic_types_test.go b/datastore/characteristic_types_test.go deleted file mode 100644 index 70d7617..0000000 --- a/datastore/characteristic_types_test.go +++ /dev/null @@ -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") - } -} diff --git a/datastore/characteristics.go b/datastore/characteristics.go deleted file mode 100644 index 16d165f..0000000 --- a/datastore/characteristics.go +++ /dev/null @@ -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 -} diff --git a/datastore/characteristics_test.go b/datastore/characteristics_test.go deleted file mode 100644 index 4ee7f13..0000000 --- a/datastore/characteristics_test.go +++ /dev/null @@ -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") - } -} diff --git a/datastore/datastore.go b/datastore/datastore.go deleted file mode 100644 index d4589ad..0000000 --- a/datastore/datastore.go +++ /dev/null @@ -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{}, - } -} diff --git a/datastore/datastore_test.go b/datastore/datastore_test.go deleted file mode 100644 index 5ab58dc..0000000 --- a/datastore/datastore_test.go +++ /dev/null @@ -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}} - } - } -} diff --git a/datastore/db.go b/datastore/db.go deleted file mode 100644 index 8004289..0000000 --- a/datastore/db.go +++ /dev/null @@ -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 -} diff --git a/datastore/db_test.go b/datastore/db_test.go deleted file mode 100644 index e19317e..0000000 --- a/datastore/db_test.go +++ /dev/null @@ -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) -} diff --git a/datastore/genera.go b/datastore/genera.go deleted file mode 100644 index 746e1af..0000000 --- a/datastore/genera.go +++ /dev/null @@ -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 -} diff --git a/datastore/genera_test.go b/datastore/genera_test.go deleted file mode 100644 index 8f042fc..0000000 --- a/datastore/genera_test.go +++ /dev/null @@ -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") - } -} diff --git a/datastore/measurements.go b/datastore/measurements.go deleted file mode 100644 index f887b04..0000000 --- a/datastore/measurements.go +++ /dev/null @@ -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 -} diff --git a/datastore/measurements_test.go b/datastore/measurements_test.go deleted file mode 100644 index 2b901f3..0000000 --- a/datastore/measurements_test.go +++ /dev/null @@ -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") - } -} diff --git a/datastore/species.go b/datastore/species.go deleted file mode 100644 index 2fc5a6b..0000000 --- a/datastore/species.go +++ /dev/null @@ -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 -} diff --git a/datastore/species_test.go b/datastore/species_test.go deleted file mode 100644 index e489630..0000000 --- a/datastore/species_test.go +++ /dev/null @@ -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") - } -} diff --git a/datastore/strains.go b/datastore/strains.go deleted file mode 100644 index ac4208a..0000000 --- a/datastore/strains.go +++ /dev/null @@ -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 -} diff --git a/datastore/strains_test.go b/datastore/strains_test.go deleted file mode 100644 index dc3690b..0000000 --- a/datastore/strains_test.go +++ /dev/null @@ -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") - } -} diff --git a/datastore/text_measurement_types.go b/datastore/text_measurement_types.go deleted file mode 100644 index 5551c0a..0000000 --- a/datastore/text_measurement_types.go +++ /dev/null @@ -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 -} diff --git a/datastore/text_measurement_types_test.go b/datastore/text_measurement_types_test.go deleted file mode 100644 index 713632b..0000000 --- a/datastore/text_measurement_types_test.go +++ /dev/null @@ -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") - } -} diff --git a/datastore/unit_types.go b/datastore/unit_types.go deleted file mode 100644 index a96c08b..0000000 --- a/datastore/unit_types.go +++ /dev/null @@ -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 -} diff --git a/datastore/unit_types_test.go b/datastore/unit_types_test.go deleted file mode 100644 index a22165b..0000000 --- a/datastore/unit_types_test.go +++ /dev/null @@ -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") - } -} diff --git a/datastore/users.go b/datastore/users.go deleted file mode 100644 index ed33b58..0000000 --- a/datastore/users.go +++ /dev/null @@ -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 -} diff --git a/datastore/users_test.go b/datastore/users_test.go deleted file mode 100644 index dd65f67..0000000 --- a/datastore/users_test.go +++ /dev/null @@ -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) - } -} diff --git a/docker/postgres/Dockerfile b/docker/postgres/Dockerfile deleted file mode 100644 index e1af542..0000000 --- a/docker/postgres/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM postgres:9.3 -ADD dbsetup.sh /docker-entrypoint-initdb.d/ - diff --git a/docker/postgres/dbsetup.sh b/docker/postgres/dbsetup.sh deleted file mode 100644 index e63dddb..0000000 --- a/docker/postgres/dbsetup.sh +++ /dev/null @@ -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 - diff --git a/fig.yml b/fig.yml deleted file mode 100644 index 32c497f..0000000 --- a/fig.yml +++ /dev/null @@ -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 - diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..5037659 --- /dev/null +++ b/handlers.go @@ -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) +} diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..c3a846f --- /dev/null +++ b/helpers.go @@ -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 diff --git a/main.go b/main.go new file mode 100644 index 0000000..7f3ea70 --- /dev/null +++ b/main.go @@ -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) + } +} diff --git a/measurements.go b/measurements.go new file mode 100644 index 0000000..c2c2482 --- /dev/null +++ b/measurements.go @@ -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 +} diff --git a/datastore/migrations/00001_AddUsers_down.sql b/migrations/00001_AddUsers_down.sql similarity index 100% rename from datastore/migrations/00001_AddUsers_down.sql rename to migrations/00001_AddUsers_down.sql diff --git a/datastore/migrations/00001_AddUsers_up.sql b/migrations/00001_AddUsers_up.sql similarity index 100% rename from datastore/migrations/00001_AddUsers_up.sql rename to migrations/00001_AddUsers_up.sql diff --git a/datastore/migrations/00002_AddGenera_down.sql b/migrations/00002_AddGenera_down.sql similarity index 100% rename from datastore/migrations/00002_AddGenera_down.sql rename to migrations/00002_AddGenera_down.sql diff --git a/datastore/migrations/00002_AddGenera_up.sql b/migrations/00002_AddGenera_up.sql similarity index 100% rename from datastore/migrations/00002_AddGenera_up.sql rename to migrations/00002_AddGenera_up.sql diff --git a/datastore/migrations/00003_AddSpecies_down.sql b/migrations/00003_AddSpecies_down.sql similarity index 100% rename from datastore/migrations/00003_AddSpecies_down.sql rename to migrations/00003_AddSpecies_down.sql diff --git a/datastore/migrations/00003_AddSpecies_up.sql b/migrations/00003_AddSpecies_up.sql similarity index 100% rename from datastore/migrations/00003_AddSpecies_up.sql rename to migrations/00003_AddSpecies_up.sql diff --git a/datastore/migrations/00004_AddStrain_down.sql b/migrations/00004_AddStrain_down.sql similarity index 100% rename from datastore/migrations/00004_AddStrain_down.sql rename to migrations/00004_AddStrain_down.sql diff --git a/datastore/migrations/00004_AddStrain_up.sql b/migrations/00004_AddStrain_up.sql similarity index 100% rename from datastore/migrations/00004_AddStrain_up.sql rename to migrations/00004_AddStrain_up.sql diff --git a/datastore/migrations/00005_AddCharacteristicTypes_down.sql b/migrations/00005_AddCharacteristicTypes_down.sql similarity index 100% rename from datastore/migrations/00005_AddCharacteristicTypes_down.sql rename to migrations/00005_AddCharacteristicTypes_down.sql diff --git a/datastore/migrations/00005_AddCharacteristicTypes_up.sql b/migrations/00005_AddCharacteristicTypes_up.sql similarity index 100% rename from datastore/migrations/00005_AddCharacteristicTypes_up.sql rename to migrations/00005_AddCharacteristicTypes_up.sql diff --git a/datastore/migrations/00006_AddCharacteristics_down.sql b/migrations/00006_AddCharacteristics_down.sql similarity index 100% rename from datastore/migrations/00006_AddCharacteristics_down.sql rename to migrations/00006_AddCharacteristics_down.sql diff --git a/datastore/migrations/00006_AddCharacteristics_up.sql b/migrations/00006_AddCharacteristics_up.sql similarity index 100% rename from datastore/migrations/00006_AddCharacteristics_up.sql rename to migrations/00006_AddCharacteristics_up.sql diff --git a/datastore/migrations/00007_AddText_Measurement_Types_down.sql b/migrations/00007_AddText_Measurement_Types_down.sql similarity index 100% rename from datastore/migrations/00007_AddText_Measurement_Types_down.sql rename to migrations/00007_AddText_Measurement_Types_down.sql diff --git a/datastore/migrations/00007_AddText_Measurement_Types_up.sql b/migrations/00007_AddText_Measurement_Types_up.sql similarity index 100% rename from datastore/migrations/00007_AddText_Measurement_Types_up.sql rename to migrations/00007_AddText_Measurement_Types_up.sql diff --git a/datastore/migrations/00008_AddUnit_Types_down.sql b/migrations/00008_AddUnit_Types_down.sql similarity index 100% rename from datastore/migrations/00008_AddUnit_Types_down.sql rename to migrations/00008_AddUnit_Types_down.sql diff --git a/datastore/migrations/00008_AddUnit_Types_up.sql b/migrations/00008_AddUnit_Types_up.sql similarity index 100% rename from datastore/migrations/00008_AddUnit_Types_up.sql rename to migrations/00008_AddUnit_Types_up.sql diff --git a/datastore/migrations/00009_AddTest_Methods_down.sql b/migrations/00009_AddTest_Methods_down.sql similarity index 100% rename from datastore/migrations/00009_AddTest_Methods_down.sql rename to migrations/00009_AddTest_Methods_down.sql diff --git a/datastore/migrations/00009_AddTest_Methods_up.sql b/migrations/00009_AddTest_Methods_up.sql similarity index 100% rename from datastore/migrations/00009_AddTest_Methods_up.sql rename to migrations/00009_AddTest_Methods_up.sql diff --git a/datastore/migrations/00010_AddMeasurements_down.sql b/migrations/00010_AddMeasurements_down.sql similarity index 100% rename from datastore/migrations/00010_AddMeasurements_down.sql rename to migrations/00010_AddMeasurements_down.sql diff --git a/datastore/migrations/00010_AddMeasurements_up.sql b/migrations/00010_AddMeasurements_up.sql similarity index 100% rename from datastore/migrations/00010_AddMeasurements_up.sql rename to migrations/00010_AddMeasurements_up.sql diff --git a/models/characteristic_types.go b/models/characteristic_types.go deleted file mode 100644 index 9f107c9..0000000 --- a/models/characteristic_types.go +++ /dev/null @@ -1,208 +0,0 @@ -package models - -import ( - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/thermokarst/bactdb/router" -) - -// A Characteristic Type is a lookup type -type CharacteristicType struct { - Id int64 `json:"id,omitempty"` - CharacteristicTypeName string `db:"characteristic_type_name" json:"characteristicTypeName"` - CreatedAt time.Time `db:"created_at" json:"createdAt"` - UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` -} - -func (m *CharacteristicType) String() string { - return fmt.Sprintf("%v", *m) -} - -func NewCharacteristicType() *CharacteristicType { - return &CharacteristicType{ - CharacteristicTypeName: "Test Char Type", - } -} - -type CharacteristicTypesService interface { - // Get a characteristic type - Get(id int64) (*CharacteristicType, error) - - // List all characteristic types - List(opt *CharacteristicTypeListOptions) ([]*CharacteristicType, error) - - // Create a characteristic type record - Create(characteristic_type *CharacteristicType) (bool, error) - - // Update an existing characteristic type - Update(id int64, characteristic_type *CharacteristicType) (updated bool, err error) - - // Delete an existing characteristic type - Delete(id int64) (deleted bool, err error) -} - -var ( - ErrCharacteristicTypeNotFound = errors.New("characteristic type not found") -) - -type characteristicTypesService struct { - client *Client -} - -func (s *characteristicTypesService) Get(id int64) (*CharacteristicType, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.CharacteristicType, map[string]string{"Id": strId}, nil) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var characteristic_type *CharacteristicType - _, err = s.client.Do(req, &characteristic_type) - if err != nil { - return nil, err - } - - return characteristic_type, nil -} - -func (s *characteristicTypesService) Create(characteristic_type *CharacteristicType) (bool, error) { - url, err := s.client.url(router.CreateCharacteristicType, nil, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("POST", url.String(), characteristic_type) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &characteristic_type) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusCreated, nil -} - -type CharacteristicTypeListOptions struct { - ListOptions -} - -func (s *characteristicTypesService) List(opt *CharacteristicTypeListOptions) ([]*CharacteristicType, error) { - url, err := s.client.url(router.CharacteristicTypes, nil, opt) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var characteristic_types []*CharacteristicType - _, err = s.client.Do(req, &characteristic_types) - if err != nil { - return nil, err - } - - return characteristic_types, nil -} - -func (s *characteristicTypesService) Update(id int64, characteristic_type *CharacteristicType) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.UpdateCharacteristicType, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("PUT", url.String(), characteristic_type) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &characteristic_type) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -func (s *characteristicTypesService) Delete(id int64) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.DeleteCharacteristicType, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("DELETE", url.String(), nil) - if err != nil { - return false, err - } - - var characteristic_type *CharacteristicType - resp, err := s.client.Do(req, &characteristic_type) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -type MockCharacteristicTypesService struct { - Get_ func(id int64) (*CharacteristicType, error) - List_ func(opt *CharacteristicTypeListOptions) ([]*CharacteristicType, error) - Create_ func(characteristic_type *CharacteristicType) (bool, error) - Update_ func(id int64, characteristic_type *CharacteristicType) (bool, error) - Delete_ func(id int64) (bool, error) -} - -var _ CharacteristicTypesService = &MockCharacteristicTypesService{} - -func (s *MockCharacteristicTypesService) Get(id int64) (*CharacteristicType, error) { - if s.Get_ == nil { - return nil, nil - } - return s.Get_(id) -} - -func (s *MockCharacteristicTypesService) Create(characteristic_type *CharacteristicType) (bool, error) { - if s.Create_ == nil { - return false, nil - } - return s.Create_(characteristic_type) -} - -func (s *MockCharacteristicTypesService) List(opt *CharacteristicTypeListOptions) ([]*CharacteristicType, error) { - if s.List_ == nil { - return nil, nil - } - return s.List_(opt) -} - -func (s *MockCharacteristicTypesService) Update(id int64, characteristic_type *CharacteristicType) (bool, error) { - if s.Update_ == nil { - return false, nil - } - return s.Update_(id, characteristic_type) -} - -func (s *MockCharacteristicTypesService) Delete(id int64) (bool, error) { - if s.Delete_ == nil { - return false, nil - } - return s.Delete_(id) -} diff --git a/models/characteristic_types_test.go b/models/characteristic_types_test.go deleted file mode 100644 index 20493e9..0000000 --- a/models/characteristic_types_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package models - -import ( - "net/http" - "reflect" - "testing" - - "github.com/thermokarst/bactdb/router" -) - -func newCharacteristicType() *CharacteristicType { - characteristic_type := NewCharacteristicType() - characteristic_type.Id = 1 - return characteristic_type -} - -func TestCharacteristicTypeService_Get(t *testing.T) { - setup() - defer teardown() - - want := newCharacteristicType() - - var called bool - mux.HandleFunc(urlPath(t, router.CharacteristicType, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - - writeJSON(w, want) - }) - - characteristic_type, err := client.CharacteristicTypes.Get(want.Id) - if err != nil { - t.Errorf("CharacteristicTypes.Get returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - - if !reflect.DeepEqual(characteristic_type, want) { - t.Errorf("CharacteristicTypes.Get return %+v, want %+v", characteristic_type, want) - } -} - -func TestCharacteristicTypeService_Create(t *testing.T) { - setup() - defer teardown() - - want := newCharacteristicType() - - var called bool - mux.HandleFunc(urlPath(t, router.CreateCharacteristicType, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "POST") - testBody(t, r, `{"id":1,"characteristicTypeName":"Test Char Type","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}`+"\n") - - w.WriteHeader(http.StatusCreated) - writeJSON(w, want) - }) - - characteristic_type := newCharacteristicType() - created, err := client.CharacteristicTypes.Create(characteristic_type) - if err != nil { - t.Errorf("CharacteristicTypes.Create returned error: %v", err) - } - - if !created { - t.Error("!created") - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - if !reflect.DeepEqual(characteristic_type, want) { - t.Errorf("CharacteristicTypes.Create returned %+v, want %+v", characteristic_type, want) - } -} - -func TestCharacteristicTypeService_List(t *testing.T) { - setup() - defer teardown() - - want := []*CharacteristicType{newCharacteristicType()} - - var called bool - mux.HandleFunc(urlPath(t, router.CharacteristicTypes, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - testFormValues(t, r, values{}) - - writeJSON(w, want) - }) - - characteristic_types, err := client.CharacteristicTypes.List(nil) - if err != nil { - t.Errorf("CharacteristicTypes.List returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - for _, u := range want { - normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt) - } - - if !reflect.DeepEqual(characteristic_types, want) { - t.Errorf("CharacteristicTypes.List return %+v, want %+v", characteristic_types, want) - } -} - -func TestCharacteristicTypeService_Update(t *testing.T) { - setup() - defer teardown() - - want := newCharacteristicType() - - var called bool - mux.HandleFunc(urlPath(t, router.UpdateCharacteristicType, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "PUT") - testBody(t, r, `{"id":1,"characteristicTypeName":"Test Char Type Updated","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}`+"\n") - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - characteristic_type := newCharacteristicType() - characteristic_type.CharacteristicTypeName = "Test Char Type Updated" - updated, err := client.CharacteristicTypes.Update(characteristic_type.Id, characteristic_type) - if err != nil { - t.Errorf("CharacteristicTypes.Update returned error: %v", err) - } - - if !updated { - t.Error("!updated") - } - - if !called { - t.Fatal("!called") - } -} - -func TestCharacteristicTypeService_Delete(t *testing.T) { - setup() - defer teardown() - - want := newCharacteristicType() - - var called bool - mux.HandleFunc(urlPath(t, router.DeleteCharacteristicType, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "DELETE") - - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - deleted, err := client.CharacteristicTypes.Delete(want.Id) - if err != nil { - t.Errorf("CharacteristicTypes.Delete returned error: %v", err) - } - - if !deleted { - t.Error("!deleted") - } - - if !called { - t.Fatal("!called") - } -} diff --git a/models/characteristics.go b/models/characteristics.go deleted file mode 100644 index 4b878c7..0000000 --- a/models/characteristics.go +++ /dev/null @@ -1,225 +0,0 @@ -package models - -import ( - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/thermokarst/bactdb/router" -) - -// A Characteristic is a lookup type -type CharacteristicBase struct { - Id int64 `json:"id,omitempty"` - CharacteristicName string `db:"characteristic_name" json:"characteristicName"` - CharacteristicTypeId int64 `db:"characteristic_type_id" json:"characteristicTypeId"` - CreatedAt time.Time `db:"created_at" json:"createdAt"` - UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` -} - -type Characteristic struct { - *CharacteristicBase - Measurements NullSliceInt64 `db:"measurements" json:"measurements"` -} - -type CharacteristicJSON struct { - Characteristic *Characteristic `json:"characteristic"` -} - -type CharacteristicsJSON struct { - Characteristics []*Characteristic `json:"characteristics"` -} - -func (m *Characteristic) String() string { - return fmt.Sprintf("%v", *m) -} - -func NewCharacteristic() *Characteristic { - return &Characteristic{ - &CharacteristicBase{ - CharacteristicName: "Test Characteristic", - }, - make([]int64, 0), - } -} - -type CharacteristicsService interface { - // Get an characteristic - Get(id int64) (*Characteristic, error) - - // List all characteristics - List(opt *CharacteristicListOptions) ([]*Characteristic, error) - - // Create an characteristic - Create(characteristic *Characteristic) (bool, error) - - // Update an characteristic - Update(id int64, Characteristic *Characteristic) (updated bool, err error) - - // Delete an characteristic - Delete(id int64) (deleted bool, err error) -} - -var ( - ErrCharacteristicNotFound = errors.New("characteristic not found") -) - -type characteristicsService struct { - client *Client -} - -func (s *characteristicsService) Get(id int64) (*Characteristic, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.Characteristic, map[string]string{"Id": strId}, nil) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var characteristic *CharacteristicJSON - _, err = s.client.Do(req, &characteristic) - if err != nil { - return nil, err - } - - return characteristic.Characteristic, nil -} - -func (s *characteristicsService) Create(characteristic *Characteristic) (bool, error) { - url, err := s.client.url(router.CreateCharacteristic, nil, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("POST", url.String(), CharacteristicJSON{Characteristic: characteristic}) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &characteristic) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusCreated, nil -} - -type CharacteristicListOptions struct { - ListOptions -} - -func (s *characteristicsService) List(opt *CharacteristicListOptions) ([]*Characteristic, error) { - url, err := s.client.url(router.Characteristics, nil, opt) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var characteristics *CharacteristicsJSON - _, err = s.client.Do(req, &characteristics) - if err != nil { - return nil, err - } - - return characteristics.Characteristics, nil -} - -func (s *characteristicsService) Update(id int64, characteristic *Characteristic) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.UpdateCharacteristic, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("PUT", url.String(), CharacteristicJSON{Characteristic: characteristic}) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &characteristic) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -func (s *characteristicsService) Delete(id int64) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.DeleteCharacteristic, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("DELETE", url.String(), nil) - if err != nil { - return false, err - } - - var characteristic *Characteristic - resp, err := s.client.Do(req, &characteristic) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -type MockCharacteristicsService struct { - Get_ func(id int64) (*Characteristic, error) - List_ func(opt *CharacteristicListOptions) ([]*Characteristic, error) - Create_ func(characteristic *Characteristic) (bool, error) - Update_ func(id int64, characteristic *Characteristic) (bool, error) - Delete_ func(id int64) (bool, error) -} - -var _ CharacteristicsService = &MockCharacteristicsService{} - -func (s *MockCharacteristicsService) Get(id int64) (*Characteristic, error) { - if s.Get_ == nil { - return nil, nil - } - return s.Get_(id) -} - -func (s *MockCharacteristicsService) Create(characteristic *Characteristic) (bool, error) { - if s.Create_ == nil { - return false, nil - } - return s.Create_(characteristic) -} - -func (s *MockCharacteristicsService) List(opt *CharacteristicListOptions) ([]*Characteristic, error) { - if s.List_ == nil { - return nil, nil - } - return s.List_(opt) -} - -func (s *MockCharacteristicsService) Update(id int64, characteristic *Characteristic) (bool, error) { - if s.Update_ == nil { - return false, nil - } - return s.Update_(id, characteristic) -} - -func (s *MockCharacteristicsService) Delete(id int64) (bool, error) { - if s.Delete_ == nil { - return false, nil - } - return s.Delete_(id) -} diff --git a/models/characteristics_test.go b/models/characteristics_test.go deleted file mode 100644 index 00fb4e7..0000000 --- a/models/characteristics_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package models - -import ( - "net/http" - "reflect" - "testing" - - "github.com/thermokarst/bactdb/router" -) - -func newCharacteristic() *Characteristic { - characteristic := NewCharacteristic() - characteristic.Id = 1 - return characteristic -} - -func TestCharacteristicService_Get(t *testing.T) { - setup() - defer teardown() - - want := newCharacteristic() - - var called bool - mux.HandleFunc(urlPath(t, router.Characteristic, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - - writeJSON(w, CharacteristicJSON{Characteristic: want}) - }) - - characteristic, err := client.Characteristics.Get(want.Id) - if err != nil { - t.Errorf("Characteristics.Get returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - - if !reflect.DeepEqual(characteristic, want) { - t.Errorf("Characteristics.Get return %+v, want %+v", characteristic, want) - } -} - -func TestCharacteristicService_Create(t *testing.T) { - setup() - defer teardown() - - want := newCharacteristic() - - var called bool - mux.HandleFunc(urlPath(t, router.CreateCharacteristic, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "POST") - testBody(t, r, `{"characteristic":{"id":1,"characteristicName":"Test Characteristic","characteristicTypeId":0,"createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null,"measurements":[]}}`+"\n") - - w.WriteHeader(http.StatusCreated) - writeJSON(w, want) - }) - - characteristic := newCharacteristic() - created, err := client.Characteristics.Create(characteristic) - if err != nil { - t.Errorf("Characteristics.Create returned error: %v", err) - } - - if !created { - t.Error("!created") - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - if !reflect.DeepEqual(characteristic, want) { - t.Errorf("Characteristics.Create returned %+v, want %+v", characteristic, want) - } -} - -func TestCharacteristicService_List(t *testing.T) { - setup() - defer teardown() - - want := []*Characteristic{newCharacteristic()} - - var called bool - mux.HandleFunc(urlPath(t, router.Characteristics, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - testFormValues(t, r, values{}) - - writeJSON(w, CharacteristicsJSON{Characteristics: want}) - }) - - characteristics, err := client.Characteristics.List(nil) - if err != nil { - t.Errorf("Characteristics.List returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - for _, u := range want { - normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt) - } - - if !reflect.DeepEqual(characteristics, want) { - t.Errorf("Characteristics.List return %+v, want %+v", characteristics, want) - } -} - -func TestCharacteristicService_Update(t *testing.T) { - setup() - defer teardown() - - want := newCharacteristic() - - var called bool - mux.HandleFunc(urlPath(t, router.UpdateCharacteristic, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "PUT") - testBody(t, r, `{"characteristic":{"id":1,"characteristicName":"Test Char Updated","characteristicTypeId":0,"createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null,"measurements":[]}}`+"\n") - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - characteristic := newCharacteristic() - characteristic.CharacteristicName = "Test Char Updated" - updated, err := client.Characteristics.Update(characteristic.Id, characteristic) - if err != nil { - t.Errorf("Characteristics.Update returned error: %v", err) - } - - if !updated { - t.Error("!updated") - } - - if !called { - t.Fatal("!called") - } -} - -func TestCharacteristicService_Delete(t *testing.T) { - setup() - defer teardown() - - want := newCharacteristic() - - var called bool - mux.HandleFunc(urlPath(t, router.DeleteCharacteristic, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "DELETE") - testBody(t, r, "") - - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - deleted, err := client.Characteristics.Delete(want.Id) - if err != nil { - t.Errorf("Characteristics.Delete returned error: %v", err) - } - - if !deleted { - t.Error("!deleted") - } - - if !called { - t.Fatal("!called") - } -} diff --git a/models/client.go b/models/client.go deleted file mode 100644 index f094cce..0000000 --- a/models/client.go +++ /dev/null @@ -1,211 +0,0 @@ -package models - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "reflect" - "strings" - - "github.com/google/go-querystring/query" - "github.com/thermokarst/bactdb/router" -) - -// A Client communicates with bactdb's HTTP API. -type Client struct { - Users UsersService - Genera GeneraService - Species SpeciesService - Strains StrainsService - CharacteristicTypes CharacteristicTypesService - Characteristics CharacteristicsService - TextMeasurementTypes TextMeasurementTypesService - UnitTypes UnitTypesService - Measurements MeasurementsService - - // BaseURL for HTTP requests to bactdb's API. - BaseURL *url.URL - - //UserAgent used for HTTP requests to bactdb's API. - UserAgent string - - httpClient *http.Client -} - -const ( - libraryVersion = "0.0.1" - userAgent = "bactdb-client/" + libraryVersion -) - -// NewClient creates a new HTTP API client for bactdb. If httpClient == nil, -// then http.DefaultClient is used. -func NewClient(httpClient *http.Client) *Client { - if httpClient == nil { - httpClient = http.DefaultClient - } - - c := &Client{ - BaseURL: &url.URL{Scheme: "http", Host: "bactdb.org", Path: "/api/"}, - UserAgent: userAgent, - httpClient: httpClient, - } - c.Users = &usersService{c} - c.Genera = &generaService{c} - c.Species = &speciesService{c} - c.Strains = &strainsService{c} - c.CharacteristicTypes = &characteristicTypesService{c} - c.Characteristics = &characteristicsService{c} - c.TextMeasurementTypes = &textMeasurementTypesService{c} - c.UnitTypes = &unitTypesService{c} - c.Measurements = &measurementsService{c} - return c -} - -// 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"` -} - -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 - -// apiRouter is used to generate URLs for bactdb's HTTP API. -var apiRouter = router.API() - -// url generates the URL to the named bactdb API endpoint, using the -// specified route variables and query options. -func (c *Client) url(apiRouteName string, routeVars map[string]string, opt interface{}) (*url.URL, error) { - route := apiRouter.Get(apiRouteName) - if route == nil { - return nil, fmt.Errorf("no API route named %q", apiRouteName) - } - - routeVarsList := make([]string, 2*len(routeVars)) - i := 0 - for name, val := range routeVars { - routeVarsList[i*2] = name - routeVarsList[i*2+1] = val - i++ - } - url, err := route.URL(routeVarsList...) - if err != nil { - return nil, err - } - - // make the route URL path relative to BaseURL by trimming the leading "/" - url.Path = strings.TrimPrefix(url.Path, "/") - - if opt != nil { - err = addOptions(url, opt) - if err != nil { - return nil, err - } - } - - return url, nil -} - -func (c *Client) URL(apiRouteName string, routeVars map[string]string, opt interface{}) (*url.URL, error) { - u, err := c.url(apiRouteName, routeVars, opt) - absURL := c.BaseURL.ResolveReference(u) - return absURL, err -} - -// NewRequest creates an API request. A relative URL can be provided in urlStr, -// in which case it is resolved relative to the BaseURL of the Client. Relative -// URLs should always be specified without a preceding slash. If specified, the -// value pointed to by body is JSON encoded and included as the request body. -func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { - rel, err := url.Parse(urlStr) - if err != nil { - return nil, err - } - - u := c.BaseURL.ResolveReference(rel) - - buf := new(bytes.Buffer) - if body != nil { - err := json.NewEncoder(buf).Encode(body) - if err != nil { - return nil, err - } - } - - req, err := http.NewRequest(method, u.String(), buf) - if err != nil { - return nil, err - } - - req.Header.Add("User-Agent", c.UserAgent) - return req, nil -} - -// Do sends an API request and returns the API response. The API response is -// JSON-decoded and stored in the value pointed to by v, or returned as an error -// if an API error has occurred. -func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - err = CheckResponse(resp) - if err != nil { - // even though there was an error, we still return the response - // in case the caller wants to inspect it further - return resp, err - } - - if v != nil { - if bp, ok := v.(*[]byte); ok { - *bp, err = ioutil.ReadAll(resp.Body) - } else { - err = json.NewDecoder(resp.Body).Decode(v) - } - } - if err != nil { - return nil, fmt.Errorf("error reading response from %s %s: %s", req.Method, req.URL.RequestURI(), err) - } - return resp, nil -} - -// addOptions adds the parameters in opt as URL query parameters to u. opt -// must be a struct whose fields may contain "url" tags. -func addOptions(u *url.URL, opt interface{}) error { - v := reflect.ValueOf(opt) - if v.Kind() == reflect.Ptr && v.IsNil() { - return nil - } - - qs, err := query.Values(opt) - if err != nil { - return err - } - - u.RawQuery = qs.Encode() - return nil -} diff --git a/models/client_test.go b/models/client_test.go deleted file mode 100644 index 3aebdb4..0000000 --- a/models/client_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package models - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/url" - "reflect" - "testing" - "time" - - "github.com/lib/pq" -) - -var ( - // mux is the HTTP request multiplexer used with the test server. - mux *http.ServeMux - - // client is the bactdb client being tested. - client *Client - - // server is a test HTTP server used to provide mock API responses. - server *httptest.Server -) - -// setup sets up a test HTTP server along with a Client that is -// configured to talk to that test server. Tests should register handlers on -// mux which provide mock responses for the API method being tested. -func setup() { - // test server - mux = http.NewServeMux() - server = httptest.NewServer(mux) - - // bactdb client configured to use test server - client = NewClient(nil) - url, _ := url.Parse(server.URL) - client.BaseURL = url -} - -// teardown closes the test HTTP server. -func teardown() { - server.Close() -} - -func urlPath(t *testing.T, routeName string, routeVars map[string]string) string { - url, err := client.url(routeName, routeVars, nil) - if err != nil { - t.Fatalf("Error constructing URL path for route %q with vars %+v: %s", routeName, routeVars, err) - } - return "/" + url.Path -} - -func writeJSON(w http.ResponseWriter, v interface{}) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - err := json.NewEncoder(w).Encode(v) - if err != nil { - panic("writeJSON: " + err.Error()) - } -} - -func testMethod(t *testing.T, r *http.Request, want string) { - if want != r.Method { - t.Errorf("Request method = %v, want %v", r.Method, want) - } -} - -type values map[string]string - -func testFormValues(t *testing.T, r *http.Request, values values) { - want := url.Values{} - for k, v := range values { - want.Add(k, v) - } - - r.ParseForm() - if !reflect.DeepEqual(want, r.Form) { - t.Errorf("Request parameters = %v, want %v", r.Form, want) - } -} - -func testBody(t *testing.T, r *http.Request, want string) { - b, err := ioutil.ReadAll(r.Body) - if err != nil { - t.Errorf("Unable to read body") - } - str := string(b) - if want != str { - t.Errorf("Body = %s, want: %s", str, want) - } -} - -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 *NullTime: - x, _ := v.(*NullTime) - *x = NullTime{pq.NullTime{Time: x.Time.In(time.UTC), Valid: x.Valid}} - } - } -} diff --git a/models/errors.go b/models/errors.go deleted file mode 100644 index d11b172..0000000 --- a/models/errors.go +++ /dev/null @@ -1,56 +0,0 @@ -package models - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" -) - -// An ErrorResponse reports errors caused by an API request. -type ErrorResponse struct { - Response *http.Response `json:",omitempty"` - Message string -} - -func (r *ErrorResponse) Error() string { - return fmt.Sprintf("%v %v: %d %v", - r.Response.Request.Method, r.Response.Request.URL, - r.Response.StatusCode, r.Message) -} - -func (r *ErrorResponse) HTTPStatusCode() int { - return r.Response.StatusCode -} - -// CheckResponse checks the API response for errors, and returns them if -// present. A response is considered an error if it has a status code outside -// the 200 range. API error responses are expected to have either no response -// body, or a JSON response body that maps to ErrorResponse. Any other -// response body will be silently ignored. -func CheckResponse(r *http.Response) error { - if c := r.StatusCode; 200 <= c && c <= 299 { - return nil - } - errorResponse := &ErrorResponse{Response: r} - data, err := ioutil.ReadAll(r.Body) - if err == nil && data != nil { - json.Unmarshal(data, errorResponse) - } - return errorResponse -} - -func IsHTTPErrorCode(err error, statusCode int) bool { - if err == nil { - return false - } - - type httpError interface { - Error() string - HTTPStatusCode() int - } - if httpErr, ok := err.(httpError); ok { - return statusCode == httpErr.HTTPStatusCode() - } - return false -} diff --git a/models/genera.go b/models/genera.go deleted file mode 100644 index c1094a2..0000000 --- a/models/genera.go +++ /dev/null @@ -1,226 +0,0 @@ -package models - -import ( - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/thermokarst/bactdb/router" -) - -// A Genus is a high-level classifier in bactdb. -type GenusBase struct { - Id int64 `json:"id,omitempty"` - GenusName string `db:"genus_name" json:"genusName"` - CreatedAt time.Time `db:"created_at" json:"createdAt"` - UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` -} - -type Genus struct { - *GenusBase - Species NullSliceInt64 `db:"species" json:"species"` -} - -type GenusJSON struct { - Genus *Genus `json:"genus"` -} - -type GeneraJSON struct { - Genera []*Genus `json:"genera"` -} - -func (m *Genus) String() string { - return fmt.Sprintf("%v", *m) -} - -func (m *GenusBase) String() string { - return fmt.Sprintf("%v", *m) -} - -func NewGenus() *Genus { - return &Genus{&GenusBase{GenusName: "Test Genus"}, make([]int64, 0)} -} - -// GeneraService interacts with the genus-related endpoints in bactdb's API. -type GeneraService interface { - // Get a genus. - Get(id int64) (*Genus, error) - - // List all genera. - List(opt *GenusListOptions) ([]*Genus, error) - - // Create a new genus. The newly created genus's ID is written to genus.Id - Create(genus *Genus) (created bool, err error) - - // Update an existing genus. - Update(id int64, genus *Genus) (updated bool, err error) - - // Delete an existing genus. - Delete(id int64) (deleted bool, err error) -} - -var ( - ErrGenusNotFound = errors.New("genus not found") -) - -type generaService struct { - client *Client -} - -func (s *generaService) Get(id int64) (*Genus, error) { - // Pass in key value pairs as strings, so that the gorilla mux URL - // generation is happy. - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.Genus, map[string]string{"Id": strId}, nil) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var genus *GenusJSON - _, err = s.client.Do(req, &genus) - if err != nil { - return nil, err - } - - return genus.Genus, nil -} - -func (s *generaService) Create(genus *Genus) (bool, error) { - url, err := s.client.url(router.CreateGenus, nil, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("POST", url.String(), GenusJSON{Genus: genus}) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &genus) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusCreated, nil -} - -type GenusListOptions struct { - ListOptions -} - -func (s *generaService) List(opt *GenusListOptions) ([]*Genus, error) { - url, err := s.client.url(router.Genera, nil, opt) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var genera *GeneraJSON - _, err = s.client.Do(req, &genera) - if err != nil { - return nil, err - } - - return genera.Genera, nil -} - -func (s *generaService) Update(id int64, genus *Genus) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.UpdateGenus, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("PUT", url.String(), GenusJSON{Genus: genus}) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &genus) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -func (s *generaService) Delete(id int64) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.DeleteGenus, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("DELETE", url.String(), nil) - if err != nil { - return false, err - } - - var genus *Genus - resp, err := s.client.Do(req, &genus) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -type MockGeneraService struct { - Get_ func(id int64) (*Genus, error) - List_ func(opt *GenusListOptions) ([]*Genus, error) - Create_ func(genus *Genus) (bool, error) - Update_ func(id int64, genus *Genus) (bool, error) - Delete_ func(id int64) (bool, error) -} - -var _ GeneraService = &MockGeneraService{} - -func (s *MockGeneraService) Get(id int64) (*Genus, error) { - if s.Get_ == nil { - return nil, nil - } - return s.Get_(id) -} - -func (s *MockGeneraService) Create(genus *Genus) (bool, error) { - if s.Create_ == nil { - return false, nil - } - return s.Create_(genus) -} - -func (s *MockGeneraService) List(opt *GenusListOptions) ([]*Genus, error) { - if s.List_ == nil { - return nil, nil - } - return s.List_(opt) -} - -func (s *MockGeneraService) Update(id int64, genus *Genus) (bool, error) { - if s.Update_ == nil { - return false, nil - } - return s.Update_(id, genus) -} - -func (s *MockGeneraService) Delete(id int64) (bool, error) { - if s.Delete_ == nil { - return false, nil - } - return s.Delete_(id) -} diff --git a/models/genera_test.go b/models/genera_test.go deleted file mode 100644 index c749b78..0000000 --- a/models/genera_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package models - -import ( - "net/http" - "reflect" - "testing" - - "github.com/thermokarst/bactdb/router" -) - -func newGenus() *Genus { - genus := NewGenus() - genus.Id = 1 - return genus -} - -func TestGeneraService_Get(t *testing.T) { - setup() - defer teardown() - - want := newGenus() - - var called bool - mux.HandleFunc(urlPath(t, router.Genus, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - - writeJSON(w, GenusJSON{Genus: want}) - }) - - genus, err := client.Genera.Get(want.Id) - if err != nil { - t.Errorf("Genera.Get returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - - if !reflect.DeepEqual(genus, want) { - t.Errorf("Genera.Get returned %+v, want %+v", genus, want) - } -} - -func TestGeneraService_Create(t *testing.T) { - setup() - defer teardown() - - want := newGenus() - - var called bool - mux.HandleFunc(urlPath(t, router.CreateGenus, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "POST") - testBody(t, r, `{"genus":{"id":1,"genusName":"Test Genus","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null,"species":[]}}`+"\n") - - w.WriteHeader(http.StatusCreated) - writeJSON(w, want) - }) - - genus := newGenus() - created, err := client.Genera.Create(genus) - if err != nil { - t.Errorf("Genera.Create returned error: %v", err) - } - - if !created { - t.Error("!created") - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - - if !reflect.DeepEqual(genus, want) { - t.Errorf("Genera.Create returned %+v, want %+v", genus, want) - } -} - -func TestGeneraService_List(t *testing.T) { - setup() - defer teardown() - - want := []*Genus{newGenus()} - - var called bool - mux.HandleFunc(urlPath(t, router.Genera, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - testFormValues(t, r, values{}) - - writeJSON(w, GeneraJSON{Genera: want}) - }) - - genera, err := client.Genera.List(nil) - if err != nil { - t.Errorf("Genera.List returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - for _, u := range want { - normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt) - } - - if !reflect.DeepEqual(genera, want) { - t.Errorf("Genera.List return %+v, want %+v", genera, want) - } -} - -func TestGeneraService_Update(t *testing.T) { - setup() - defer teardown() - - want := newGenus() - - var called bool - mux.HandleFunc(urlPath(t, router.UpdateGenus, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "PUT") - testBody(t, r, `{"genus":{"id":1,"genusName":"Test Genus Updated","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null,"species":[]}}`+"\n") - - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - genus := newGenus() - genus.GenusName = "Test Genus Updated" - updated, err := client.Genera.Update(genus.Id, genus) - if err != nil { - t.Errorf("Genera.Update returned error: %v", err) - } - - if !updated { - t.Error("!updated") - } - - if !called { - t.Fatal("!called") - } -} - -func TestGeneraService_Delete(t *testing.T) { - setup() - defer teardown() - - want := newGenus() - - var called bool - mux.HandleFunc(urlPath(t, router.DeleteGenus, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "DELETE") - testBody(t, r, "") - - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - deleted, err := client.Genera.Delete(1) - if err != nil { - t.Errorf("Genera.Delete returned error: %v", err) - } - - if !deleted { - t.Error("!deleted") - } - - if !called { - t.Fatal("!called") - } -} diff --git a/models/measurements.go b/models/measurements.go deleted file mode 100644 index 580a8f9..0000000 --- a/models/measurements.go +++ /dev/null @@ -1,228 +0,0 @@ -package models - -import ( - "database/sql" - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/thermokarst/bactdb/router" -) - -// A Measurement is the main data type for this application -// There are two types of supported measurements: text & numerical. The table -// has a constraint that will allow one or the other for a particular -// combination of strain & characteristic, but not both. -type Measurement struct { - Id int64 `json:"id,omitempty"` - StrainId int64 `db:"strain_id" json:"strain"` - CharacteristicId int64 `db:"characteristic_id" json:"characteristic"` - TextMeasurementTypeId NullInt64 `db:"text_measurement_type_id" json:"textMeasurementTypeId"` - 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:"unitTypeId"` - Notes NullString `db:"notes" json:"notes"` - TestMethodId NullInt64 `db:"test_method_id" json:"testMethodId"` - CreatedAt time.Time `db:"created_at" json:"createdAt"` - UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` -} - -type MeasurementJSON struct { - Measurement *Measurement `json:"measurement"` -} - -type MeasurementsJSON struct { - Measurements []*Measurement `json:"measurements"` -} - -func (m *Measurement) String() string { - return fmt.Sprintf("%v", *m) -} - -func NewMeasurement() *Measurement { - return &Measurement{ - NumValue: NullFloat64{sql.NullFloat64{Float64: 1.23, Valid: true}}, - } -} - -type MeasurementsService interface { - // Get a measurement - Get(id int64) (*Measurement, error) - - // List all measurements - List(opt *MeasurementListOptions) ([]*Measurement, error) - - // Create a measurement - Create(measurement *Measurement) (bool, error) - - // Update an existing measurement - Update(id int64, MeasurementType *Measurement) (bool, error) - - // Delete a measurement - Delete(id int64) (deleted bool, err error) -} - -var ( - ErrMeasurementNotFound = errors.New("measurement not found") -) - -type measurementsService struct { - client *Client -} - -func (s *measurementsService) Get(id int64) (*Measurement, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.Measurement, map[string]string{"Id": strId}, nil) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var measurement *MeasurementJSON - _, err = s.client.Do(req, &measurement) - if err != nil { - return nil, err - } - - return measurement.Measurement, nil -} - -func (s *measurementsService) Create(measurement *Measurement) (bool, error) { - url, err := s.client.url(router.CreateMeasurement, nil, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("POST", url.String(), MeasurementJSON{Measurement: measurement}) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &measurement) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusCreated, nil -} - -type MeasurementListOptions struct { - ListOptions - Genus string -} - -func (s *measurementsService) List(opt *MeasurementListOptions) ([]*Measurement, error) { - url, err := s.client.url(router.Measurements, nil, opt) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var measurements *MeasurementsJSON - _, err = s.client.Do(req, &measurements) - if err != nil { - return nil, err - } - - return measurements.Measurements, nil -} - -func (s *measurementsService) Update(id int64, measurement *Measurement) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.UpdateMeasurement, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("PUT", url.String(), MeasurementJSON{Measurement: measurement}) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &measurement) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -func (s *measurementsService) Delete(id int64) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.DeleteMeasurement, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("DELETE", url.String(), nil) - if err != nil { - return false, err - } - - var measurement *Measurement - resp, err := s.client.Do(req, &measurement) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -type MockMeasurementsService struct { - Get_ func(id int64) (*Measurement, error) - List_ func(opt *MeasurementListOptions) ([]*Measurement, error) - Create_ func(measurement *Measurement) (bool, error) - Update_ func(id int64, measurement *Measurement) (bool, error) - Delete_ func(id int64) (bool, error) -} - -var _ MeasurementsService = &MockMeasurementsService{} - -func (s *MockMeasurementsService) Get(id int64) (*Measurement, error) { - if s.Get_ == nil { - return nil, nil - } - return s.Get_(id) -} - -func (s *MockMeasurementsService) Create(measurement *Measurement) (bool, error) { - if s.Create_ == nil { - return false, nil - } - return s.Create_(measurement) -} - -func (s *MockMeasurementsService) List(opt *MeasurementListOptions) ([]*Measurement, error) { - if s.List_ == nil { - return nil, nil - } - return s.List_(opt) -} - -func (s *MockMeasurementsService) Update(id int64, measurement *Measurement) (bool, error) { - if s.Update_ == nil { - return false, nil - } - return s.Update_(id, measurement) -} - -func (s *MockMeasurementsService) Delete(id int64) (bool, error) { - if s.Delete_ == nil { - return false, nil - } - return s.Delete_(id) -} diff --git a/models/measurements_test.go b/models/measurements_test.go deleted file mode 100644 index 243e062..0000000 --- a/models/measurements_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package models - -import ( - "database/sql" - "net/http" - "reflect" - "testing" - - "github.com/thermokarst/bactdb/router" -) - -func newMeasurement() *Measurement { - measurement := NewMeasurement() - measurement.Id = 1 - measurement.StrainId = 1 - measurement.CharacteristicId = 1 - measurement.UnitTypeId = NullInt64{sql.NullInt64{Int64: 1, Valid: true}} - measurement.Notes = NullString{sql.NullString{String: "a note", Valid: true}} - return measurement -} - -func TestMeasurementService_Get(t *testing.T) { - setup() - defer teardown() - - want := newMeasurement() - - var called bool - mux.HandleFunc(urlPath(t, router.Measurement, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - - writeJSON(w, MeasurementJSON{Measurement: want}) - }) - - measurement, err := client.Measurements.Get(want.Id) - if err != nil { - t.Errorf("Measurements.Get returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt) - - if !reflect.DeepEqual(measurement, want) { - t.Errorf("Measurements.Get return %+v, want %+v", measurement, want) - } -} - -func TestMeasurementService_Create(t *testing.T) { - setup() - defer teardown() - - want := newMeasurement() - - var called bool - mux.HandleFunc(urlPath(t, router.CreateMeasurement, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "POST") - testBody(t, r, `{"measurement":{"id":1,"strain":1,"characteristic":1,"textMeasurementTypeId":null,"txtValue":null,"numValue":1.23,"confidenceInterval":null,"unitTypeId":1,"notes":"a note","testMethodId":null,"createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z"}}`+"\n") - - w.WriteHeader(http.StatusCreated) - writeJSON(w, want) - }) - - measurement := newMeasurement() - created, err := client.Measurements.Create(measurement) - if err != nil { - t.Errorf("Measurements.Create returned error: %v", err) - } - - if !created { - t.Error("!created") - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt) - if !reflect.DeepEqual(measurement, want) { - t.Errorf("Measurements.Create returned %+v, want %+v", measurement, want) - } -} - -func TestMeasurementService_List(t *testing.T) { - setup() - defer teardown() - - want := []*Measurement{newMeasurement()} - - var called bool - mux.HandleFunc(urlPath(t, router.Measurements, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - testFormValues(t, r, values{}) - - writeJSON(w, MeasurementsJSON{Measurements: want}) - }) - - measurements, err := client.Measurements.List(nil) - if err != nil { - t.Errorf("Measurements.List returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - for _, u := range want { - normalizeTime(&u.CreatedAt, &u.UpdatedAt) - } - - if !reflect.DeepEqual(measurements, want) { - t.Errorf("Measurements.List return %+v, want %+v", measurements, want) - } -} - -func TestMeasurementService_Update(t *testing.T) { - setup() - defer teardown() - - want := newMeasurement() - - var called bool - mux.HandleFunc(urlPath(t, router.UpdateMeasurement, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "PUT") - testBody(t, r, `{"measurement":{"id":1,"strain":1,"characteristic":1,"textMeasurementTypeId":null,"txtValue":null,"numValue":4.56,"confidenceInterval":null,"unitTypeId":1,"notes":"a note","testMethodId":null,"createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z"}}`+"\n") - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - measurement := newMeasurement() - measurement.NumValue = NullFloat64{sql.NullFloat64{Float64: 4.56, Valid: true}} - updated, err := client.Measurements.Update(measurement.Id, measurement) - if err != nil { - t.Errorf("Measurements.Update returned error: %v", err) - } - - if !updated { - t.Error("!updated") - } - - if !called { - t.Fatal("!called") - } -} - -func TestMeasurementService_Delete(t *testing.T) { - setup() - defer teardown() - - want := newMeasurement() - - var called bool - mux.HandleFunc(urlPath(t, router.DeleteMeasurement, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "DELETE") - testBody(t, r, "") - - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - deleted, err := client.Measurements.Delete(want.Id) - if err != nil { - t.Errorf("Measurements.Delete returned error: %v", err) - } - - if !deleted { - t.Error("!deleted") - } - - if !called { - t.Fatal("!called") - } -} diff --git a/models/species.go b/models/species.go deleted file mode 100644 index e10ba8d..0000000 --- a/models/species.go +++ /dev/null @@ -1,223 +0,0 @@ -package models - -import ( - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/thermokarst/bactdb/router" -) - -// A Species is a high-level classifier in bactdb. -type SpeciesBase struct { - Id int64 `json:"id,omitempty"` - GenusId int64 `db:"genus_id" json:"genus"` - SpeciesName string `db:"species_name" json:"speciesName"` - CreatedAt time.Time `db:"created_at" json:"createdAt"` - UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` -} - -type Species struct { - *SpeciesBase - Strains NullSliceInt64 `db:"strains" json:"strains"` -} - -type SpeciesJSON struct { - Species *Species `json:"species"` -} - -type SpeciesListJSON struct { - Species []*Species `json:"species"` -} - -func (m *Species) String() string { - return fmt.Sprintf("%v", *m) -} - -func NewSpecies() *Species { - return &Species{&SpeciesBase{SpeciesName: "Test Species"}, make([]int64, 0)} -} - -// SpeciesService interacts with the species-related endpoints in bactdb's API. -type SpeciesService interface { - // Get a species - Get(id int64) (*Species, error) - - // List all species - List(opt *SpeciesListOptions) ([]*Species, error) - - // Create a species record - Create(species *Species) (bool, error) - - // Update an existing species - Update(id int64, species *Species) (updated bool, err error) - - // Delete an existing species - Delete(id int64) (deleted bool, err error) -} - -var ( - ErrSpeciesNotFound = errors.New("species not found") -) - -type speciesService struct { - client *Client -} - -func (s *speciesService) Get(id int64) (*Species, error) { - // Pass in key value pairs as strings, sp that the gorilla mux URL generation is happy - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.Species, map[string]string{"Id": strId}, nil) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var species *SpeciesJSON - _, err = s.client.Do(req, &species) - if err != nil { - return nil, err - } - - return species.Species, nil -} - -func (s *speciesService) Create(species *Species) (bool, error) { - url, err := s.client.url(router.CreateSpecies, nil, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("POST", url.String(), SpeciesJSON{Species: species}) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &species) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusCreated, nil -} - -type SpeciesListOptions struct { - ListOptions - Genus string -} - -func (s *speciesService) List(opt *SpeciesListOptions) ([]*Species, error) { - url, err := s.client.url(router.SpeciesList, nil, opt) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var species *SpeciesListJSON - _, err = s.client.Do(req, &species) - if err != nil { - return nil, err - } - - return species.Species, nil -} - -func (s *speciesService) Update(id int64, species *Species) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.UpdateSpecies, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("PUT", url.String(), SpeciesJSON{Species: species}) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &species) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -func (s *speciesService) Delete(id int64) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.DeleteSpecies, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("DELETE", url.String(), nil) - if err != nil { - return false, err - } - - var species *Species - resp, err := s.client.Do(req, &species) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -type MockSpeciesService struct { - Get_ func(id int64) (*Species, error) - List_ func(opt *SpeciesListOptions) ([]*Species, error) - Create_ func(species *Species) (bool, error) - Update_ func(id int64, species *Species) (bool, error) - Delete_ func(id int64) (bool, error) -} - -var _ SpeciesService = &MockSpeciesService{} - -func (s *MockSpeciesService) Get(id int64) (*Species, error) { - if s.Get_ == nil { - return nil, nil - } - return s.Get_(id) -} - -func (s *MockSpeciesService) Create(species *Species) (bool, error) { - if s.Create_ == nil { - return false, nil - } - return s.Create_(species) -} - -func (s *MockSpeciesService) List(opt *SpeciesListOptions) ([]*Species, error) { - if s.List_ == nil { - return nil, nil - } - return s.List_(opt) -} - -func (s *MockSpeciesService) Update(id int64, species *Species) (bool, error) { - if s.Update_ == nil { - return false, nil - } - return s.Update_(id, species) -} - -func (s *MockSpeciesService) Delete(id int64) (bool, error) { - if s.Delete_ == nil { - return false, nil - } - return s.Delete_(id) -} diff --git a/models/species_test.go b/models/species_test.go deleted file mode 100644 index 31f0f8d..0000000 --- a/models/species_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package models - -import ( - "net/http" - "reflect" - "testing" - - "github.com/thermokarst/bactdb/router" -) - -func newSpecies() *Species { - species := NewSpecies() - species.Id = 1 - species.GenusId = 1 - return species -} - -func TestSpeciesService_Get(t *testing.T) { - setup() - defer teardown() - - want := newSpecies() - - var called bool - mux.HandleFunc(urlPath(t, router.Species, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - - writeJSON(w, SpeciesJSON{Species: want}) - }) - - species, err := client.Species.Get(want.Id) - if err != nil { - t.Errorf("Species.Get returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - - if !reflect.DeepEqual(species, want) { - t.Errorf("Species.Get returned %+v, want %+v", species, want) - } -} - -func TestSpeciesService_Create(t *testing.T) { - setup() - defer teardown() - - want := newSpecies() - - var called bool - mux.HandleFunc(urlPath(t, router.CreateSpecies, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "POST") - testBody(t, r, `{"species":{"id":1,"genus":1,"speciesName":"Test Species","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null,"strains":[]}}`+"\n") - - w.WriteHeader(http.StatusCreated) - writeJSON(w, want) - }) - - species := newSpecies() - created, err := client.Species.Create(species) - if err != nil { - t.Errorf("Species.Create returned error: %v", err) - } - - if !created { - t.Error("!created") - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - if !reflect.DeepEqual(species, want) { - t.Errorf("Species.Create returned %+v, want %+v", species, want) - } -} - -func TestSpeciesService_List(t *testing.T) { - setup() - defer teardown() - - want := []*Species{newSpecies()} - - var called bool - mux.HandleFunc(urlPath(t, router.SpeciesList, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - testFormValues(t, r, values{}) - - writeJSON(w, SpeciesListJSON{Species: want}) - }) - - species, err := client.Species.List(nil) - if err != nil { - t.Errorf("Species.List returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - for _, u := range want { - normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt) - } - - if !reflect.DeepEqual(species, want) { - t.Errorf("Species.List return %+v, want %+v", species, want) - } -} - -func TestSpeciesService_Update(t *testing.T) { - setup() - defer teardown() - - want := newSpecies() - - var called bool - mux.HandleFunc(urlPath(t, router.UpdateSpecies, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "PUT") - testBody(t, r, `{"species":{"id":1,"genus":1,"speciesName":"Test Species Updated","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null,"strains":[]}}`+"\n") - - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - species := newSpecies() - species.SpeciesName = "Test Species Updated" - updated, err := client.Species.Update(species.Id, species) - if err != nil { - t.Errorf("Species.Update returned error: %v", err) - } - - if !updated { - t.Error("!updated") - } - - if !called { - t.Fatal("!called") - } -} - -func TestSpeciesService_Delete(t *testing.T) { - setup() - defer teardown() - - want := newSpecies() - - var called bool - mux.HandleFunc(urlPath(t, router.DeleteSpecies, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "DELETE") - testBody(t, r, "") - - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - deleted, err := client.Species.Delete(want.Id) - if err != nil { - t.Errorf("Species.Delete returned error: %v", err) - } - - if !deleted { - t.Error("!deleted") - } - - if !called { - t.Fatal("!called") - } -} diff --git a/models/strains.go b/models/strains.go deleted file mode 100644 index 6515683..0000000 --- a/models/strains.go +++ /dev/null @@ -1,253 +0,0 @@ -package models - -import ( - "database/sql" - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/thermokarst/bactdb/router" -) - -// A Strain is a subclass of species -type StrainBase struct { - Id int64 `json:"id,omitempty"` - SpeciesId int64 `db:"species_id" json:"species"` - StrainName string `db:"strain_name" json:"strainName"` - StrainType string `db:"strain_type" json:"strainType"` - Etymology NullString `db:"etymology" json:"etymology"` - AccessionBanks string `db:"accession_banks" json:"accessionBanks"` - GenbankEmblDdb NullString `db:"genbank_embl_ddb" json:"genbankEmblDdb"` - IsolatedFrom NullString `db:"isolated_from" json:"isolatedFrom"` - CreatedAt time.Time `db:"created_at" json:"createdAt"` - UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` -} - -type Strain struct { - *StrainBase - Measurements NullSliceInt64 `db:"measurements" json:"measurements"` -} - -type StrainJSON struct { - Strain *Strain `json:"strain"` -} - -type StrainsJSON struct { - Strains []*Strain `json:"strains"` -} - -func (s *Strain) String() string { - return fmt.Sprintf("%v", *s) -} - -func NewStrain() *Strain { - return &Strain{ - &StrainBase{ - StrainName: "Test Strain", - StrainType: "Test Type", - Etymology: NullString{ - sql.NullString{ - String: "Test Etymology", - Valid: true, - }, - }, - AccessionBanks: "Test Accession", - GenbankEmblDdb: NullString{ - sql.NullString{ - String: "Test Genbank", - Valid: true, - }, - }, - IsolatedFrom: NullString{ - sql.NullString{ - String: "", - Valid: false, - }, - }, - }, - make([]int64, 0), - } -} - -// StrainService interacts with the strain-related endpoints in bactdb's API -type StrainsService interface { - // Get a strain - Get(id int64) (*Strain, error) - - // List all strains - List(opt *StrainListOptions) ([]*Strain, error) - - // Create a strain record - Create(strain *Strain) (bool, error) - - // Update an existing strain - Update(id int64, strain *Strain) (updated bool, err error) - - // Delete an existing strain - Delete(id int64) (deleted bool, err error) -} - -var ( - ErrStrainNotFound = errors.New("strain not found") -) - -type strainsService struct { - client *Client -} - -func (s *strainsService) Get(id int64) (*Strain, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.Strain, map[string]string{"Id": strId}, nil) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var strain *StrainJSON - _, err = s.client.Do(req, &strain) - if err != nil { - return nil, err - } - - return strain.Strain, nil -} - -func (s *strainsService) Create(strain *Strain) (bool, error) { - url, err := s.client.url(router.CreateStrain, nil, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("POST", url.String(), StrainJSON{Strain: strain}) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &strain) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusCreated, nil -} - -type StrainListOptions struct { - ListOptions - Genus string -} - -func (s *strainsService) List(opt *StrainListOptions) ([]*Strain, error) { - url, err := s.client.url(router.Strains, nil, opt) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var strains *StrainsJSON - _, err = s.client.Do(req, &strains) - if err != nil { - return nil, err - } - - return strains.Strains, nil -} - -func (s *strainsService) Update(id int64, strain *Strain) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.UpdateStrain, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("PUT", url.String(), StrainJSON{Strain: strain}) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &strain) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -func (s *strainsService) Delete(id int64) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.DeleteStrain, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("DELETE", url.String(), nil) - if err != nil { - return false, err - } - - var strain *Strain - resp, err := s.client.Do(req, &strain) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -type MockStrainsService struct { - Get_ func(id int64) (*Strain, error) - List_ func(opt *StrainListOptions) ([]*Strain, error) - Create_ func(strain *Strain) (bool, error) - Update_ func(id int64, strain *Strain) (bool, error) - Delete_ func(id int64) (bool, error) -} - -var _ StrainsService = &MockStrainsService{} - -func (s *MockStrainsService) Get(id int64) (*Strain, error) { - if s.Get_ == nil { - return nil, nil - } - return s.Get_(id) -} - -func (s *MockStrainsService) Create(strain *Strain) (bool, error) { - if s.Create_ == nil { - return false, nil - } - return s.Create_(strain) -} - -func (s *MockStrainsService) List(opt *StrainListOptions) ([]*Strain, error) { - if s.List_ == nil { - return nil, nil - } - return s.List_(opt) -} - -func (s *MockStrainsService) Update(id int64, strain *Strain) (bool, error) { - if s.Update_ == nil { - return false, nil - } - return s.Update_(id, strain) -} - -func (s *MockStrainsService) Delete(id int64) (bool, error) { - if s.Delete_ == nil { - return false, nil - } - return s.Delete_(id) -} diff --git a/models/strains_test.go b/models/strains_test.go deleted file mode 100644 index 6a58a5e..0000000 --- a/models/strains_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package models - -import ( - "net/http" - "reflect" - "testing" - - "github.com/thermokarst/bactdb/router" -) - -func newStrain() *Strain { - strain := NewStrain() - strain.Id = 1 - strain.SpeciesId = 1 - return strain -} - -func TestStrainService_Get(t *testing.T) { - setup() - defer teardown() - - want := newStrain() - - var called bool - mux.HandleFunc(urlPath(t, router.Strain, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - - writeJSON(w, StrainJSON{Strain: want}) - }) - - strain, err := client.Strains.Get(want.Id) - if err != nil { - t.Errorf("Strain.Get returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - - if !reflect.DeepEqual(strain, want) { - t.Errorf("Strain.Get return %+v, want %+v", strain, want) - } -} - -func TestStrainService_Create(t *testing.T) { - setup() - defer teardown() - - want := newStrain() - - var called bool - mux.HandleFunc(urlPath(t, router.CreateStrain, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "POST") - testBody(t, r, `{"strain":{"id":1,"species":1,"strainName":"Test Strain","strainType":"Test Type","etymology":"Test Etymology","accessionBanks":"Test Accession","genbankEmblDdb":"Test Genbank","isolatedFrom":null,"createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null,"measurements":[]}}`+"\n") - - w.WriteHeader(http.StatusCreated) - writeJSON(w, want) - }) - - strain := newStrain() - created, err := client.Strains.Create(strain) - if err != nil { - t.Errorf("Strains.Create returned error: %v", err) - } - - if !created { - t.Error("!created") - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - if !reflect.DeepEqual(strain, want) { - t.Errorf("Strains.Create returned %+v, want %+v", strain, want) - } -} - -func TestStrainService_List(t *testing.T) { - setup() - defer teardown() - - want := []*Strain{newStrain()} - - var called bool - mux.HandleFunc(urlPath(t, router.Strains, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - testFormValues(t, r, values{}) - - writeJSON(w, StrainsJSON{Strains: want}) - }) - - strains, err := client.Strains.List(nil) - if err != nil { - t.Errorf("Strains.List returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - for _, u := range want { - normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt) - } - - if !reflect.DeepEqual(strains, want) { - t.Errorf("Strains.List return %+v, want %+v", strains, want) - } -} - -func TestStrainService_Update(t *testing.T) { - setup() - defer teardown() - - want := newStrain() - - var called bool - mux.HandleFunc(urlPath(t, router.UpdateStrain, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "PUT") - testBody(t, r, `{"strain":{"id":1,"species":1,"strainName":"Test Strain Updated","strainType":"Test Type Updated","etymology":"Test Etymology","accessionBanks":"Test Accession Updated","genbankEmblDdb":"Test Genbank Updated","isolatedFrom":null,"createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null,"measurements":[]}}`+"\n") - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - strain := newStrain() - strain.StrainName = "Test Strain Updated" - strain.StrainType = "Test Type Updated" - strain.AccessionBanks = "Test Accession Updated" - strain.GenbankEmblDdb.String = "Test Genbank Updated" - updated, err := client.Strains.Update(strain.Id, strain) - if err != nil { - t.Errorf("Strains.Update returned error: %v", err) - } - - if !updated { - t.Error("!updated") - } - - if !called { - t.Fatal("!called") - } -} - -func TestStrainService_Delete(t *testing.T) { - setup() - defer teardown() - - want := newStrain() - - var called bool - mux.HandleFunc(urlPath(t, router.DeleteStrain, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "DELETE") - testBody(t, r, "") - - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - deleted, err := client.Strains.Delete(want.Id) - if err != nil { - t.Errorf("Strains.Delete returned error: %v", err) - } - - if !deleted { - t.Error("!deleted") - } - - if !called { - t.Fatal("!called") - } -} diff --git a/models/text_measurement_types.go b/models/text_measurement_types.go deleted file mode 100644 index d674169..0000000 --- a/models/text_measurement_types.go +++ /dev/null @@ -1,208 +0,0 @@ -package models - -import ( - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/thermokarst/bactdb/router" -) - -// A TextMeasurementType is a lookup type -type TextMeasurementType struct { - Id int64 `json:"id,omitempty"` - TextMeasurementName string `db:"text_measurement_name" json:"textMeasurementName"` - CreatedAt time.Time `db:"created_at" json:"createdAt"` - UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` -} - -func (m *TextMeasurementType) String() string { - return fmt.Sprintf("%v", *m) -} - -func NewTextMeasurementType() *TextMeasurementType { - return &TextMeasurementType{ - TextMeasurementName: "Test Text Measurement Type", - } -} - -type TextMeasurementTypesService interface { - // Get a text measurement type - Get(id int64) (*TextMeasurementType, error) - - // List all text measurement types - List(opt *TextMeasurementTypeListOptions) ([]*TextMeasurementType, error) - - // Create a text measurement type - Create(text_measurement_type *TextMeasurementType) (bool, error) - - // Update a text measurement type - Update(id int64, TextMeasurementType *TextMeasurementType) (updated bool, err error) - - // Delete a text measurement type - Delete(id int64) (deleted bool, err error) -} - -var ( - ErrTextMeasurementTypeNotFound = errors.New("text measurement type not found") -) - -type textMeasurementTypesService struct { - client *Client -} - -func (s *textMeasurementTypesService) Get(id int64) (*TextMeasurementType, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.TextMeasurementType, map[string]string{"Id": strId}, nil) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var text_measurement_type *TextMeasurementType - _, err = s.client.Do(req, &text_measurement_type) - if err != nil { - return nil, err - } - - return text_measurement_type, nil -} - -func (s *textMeasurementTypesService) Create(text_measurement_type *TextMeasurementType) (bool, error) { - url, err := s.client.url(router.CreateTextMeasurementType, nil, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("POST", url.String(), text_measurement_type) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &text_measurement_type) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusCreated, nil -} - -type TextMeasurementTypeListOptions struct { - ListOptions -} - -func (s *textMeasurementTypesService) List(opt *TextMeasurementTypeListOptions) ([]*TextMeasurementType, error) { - url, err := s.client.url(router.TextMeasurementTypes, nil, opt) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var text_measurement_types []*TextMeasurementType - _, err = s.client.Do(req, &text_measurement_types) - if err != nil { - return nil, err - } - - return text_measurement_types, nil -} - -func (s *textMeasurementTypesService) Update(id int64, text_measurement_type *TextMeasurementType) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.UpdateTextMeasurementType, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("PUT", url.String(), text_measurement_type) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &text_measurement_type) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -func (s *textMeasurementTypesService) Delete(id int64) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.DeleteTextMeasurementType, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("DELETE", url.String(), nil) - if err != nil { - return false, err - } - - var text_measurement_type *TextMeasurementType - resp, err := s.client.Do(req, &text_measurement_type) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -type MockTextMeasurementTypesService struct { - Get_ func(id int64) (*TextMeasurementType, error) - List_ func(opt *TextMeasurementTypeListOptions) ([]*TextMeasurementType, error) - Create_ func(text_measurement_type *TextMeasurementType) (bool, error) - Update_ func(id int64, text_measurement_type *TextMeasurementType) (bool, error) - Delete_ func(id int64) (bool, error) -} - -var _ TextMeasurementTypesService = &MockTextMeasurementTypesService{} - -func (s *MockTextMeasurementTypesService) Get(id int64) (*TextMeasurementType, error) { - if s.Get_ == nil { - return nil, nil - } - return s.Get_(id) -} - -func (s *MockTextMeasurementTypesService) Create(text_measurement_type *TextMeasurementType) (bool, error) { - if s.Create_ == nil { - return false, nil - } - return s.Create_(text_measurement_type) -} - -func (s *MockTextMeasurementTypesService) List(opt *TextMeasurementTypeListOptions) ([]*TextMeasurementType, error) { - if s.List_ == nil { - return nil, nil - } - return s.List_(opt) -} - -func (s *MockTextMeasurementTypesService) Update(id int64, text_measurement_type *TextMeasurementType) (bool, error) { - if s.Update_ == nil { - return false, nil - } - return s.Update_(id, text_measurement_type) -} - -func (s *MockTextMeasurementTypesService) Delete(id int64) (bool, error) { - if s.Delete_ == nil { - return false, nil - } - return s.Delete_(id) -} diff --git a/models/text_measurement_types_test.go b/models/text_measurement_types_test.go deleted file mode 100644 index fe958c4..0000000 --- a/models/text_measurement_types_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package models - -import ( - "net/http" - "reflect" - "testing" - - "github.com/thermokarst/bactdb/router" -) - -func newTextMeasurementType() *TextMeasurementType { - text_measurement_type := NewTextMeasurementType() - text_measurement_type.Id = 1 - return text_measurement_type -} - -func TestTextMeasurementTypeService_Get(t *testing.T) { - setup() - defer teardown() - - want := newTextMeasurementType() - - var called bool - mux.HandleFunc(urlPath(t, router.TextMeasurementType, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - - writeJSON(w, want) - }) - - text_measurement_type, err := client.TextMeasurementTypes.Get(want.Id) - if err != nil { - t.Errorf("TextMeasurementTypes.Get returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - - if !reflect.DeepEqual(text_measurement_type, want) { - t.Errorf("TextMeasurementTypes.Get return %+v, want %+v", text_measurement_type, want) - } -} - -func TestTextMeasurementTypeService_Create(t *testing.T) { - setup() - defer teardown() - - want := newTextMeasurementType() - - var called bool - mux.HandleFunc(urlPath(t, router.CreateTextMeasurementType, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "POST") - testBody(t, r, `{"id":1,"textMeasurementName":"Test Text Measurement Type","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}`+"\n") - - w.WriteHeader(http.StatusCreated) - writeJSON(w, want) - }) - - text_measurement_type := newTextMeasurementType() - created, err := client.TextMeasurementTypes.Create(text_measurement_type) - if err != nil { - t.Errorf("TextMeasurementTypes.Create returned error: %v", err) - } - - if !created { - t.Error("!created") - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - if !reflect.DeepEqual(text_measurement_type, want) { - t.Errorf("TextMeasurementTypes.Create returned %+v, want %+v", text_measurement_type, want) - } -} - -func TestTextMeasurementTypeService_List(t *testing.T) { - setup() - defer teardown() - - want := []*TextMeasurementType{newTextMeasurementType()} - - var called bool - mux.HandleFunc(urlPath(t, router.TextMeasurementTypes, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - testFormValues(t, r, values{}) - - writeJSON(w, want) - }) - - text_measurement_type, err := client.TextMeasurementTypes.List(nil) - if err != nil { - t.Errorf("TextMeasurementTypes.List returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - for _, u := range want { - normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt) - } - - if !reflect.DeepEqual(text_measurement_type, want) { - t.Errorf("TextMeasurementTypes.List return %+v, want %+v", text_measurement_type, want) - } -} - -func TestTextMeasurementTypeService_Update(t *testing.T) { - setup() - defer teardown() - - want := newTextMeasurementType() - - var called bool - mux.HandleFunc(urlPath(t, router.UpdateTextMeasurementType, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "PUT") - testBody(t, r, `{"id":1,"textMeasurementName":"Test Text Measurement Type Updated","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}`+"\n") - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - text_measurement_type := newTextMeasurementType() - text_measurement_type.TextMeasurementName = "Test Text Measurement Type Updated" - updated, err := client.TextMeasurementTypes.Update(text_measurement_type.Id, text_measurement_type) - if err != nil { - t.Errorf("TextMeasurementTypes.Update returned error: %v", err) - } - - if !updated { - t.Error("!updated") - } - - if !called { - t.Fatal("!called") - } -} - -func TestTextMeasurementTypeService_Delete(t *testing.T) { - setup() - defer teardown() - - want := newTextMeasurementType() - - var called bool - mux.HandleFunc(urlPath(t, router.DeleteTextMeasurementType, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "DELETE") - - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - deleted, err := client.TextMeasurementTypes.Delete(want.Id) - if err != nil { - t.Errorf("TextMeasurementTypes.Delete returned error: %v", err) - } - - if !deleted { - t.Error("!deleted") - } - - if !called { - t.Fatal("!called") - } -} diff --git a/models/unit_types.go b/models/unit_types.go deleted file mode 100644 index a893242..0000000 --- a/models/unit_types.go +++ /dev/null @@ -1,210 +0,0 @@ -package models - -import ( - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/thermokarst/bactdb/router" -) - -// A UnitType is a lookup type -type UnitType struct { - Id int64 `json:"id,omitempty"` - Name string `db:"name" json:"name"` - Symbol string `db:"symbol" json:"symbol"` - CreatedAt time.Time `db:"created_at" json:"createdAt"` - UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` -} - -func (m *UnitType) String() string { - return fmt.Sprintf("%v", *m) -} - -func NewUnitType() *UnitType { - return &UnitType{ - Name: "Test Unit Type", - Symbol: "x", - } -} - -type UnitTypesService interface { - // Get a unit type - Get(id int64) (*UnitType, error) - - // List all unit types - List(opt *UnitTypeListOptions) ([]*UnitType, error) - - // Create a unit type - Create(unit_type *UnitType) (bool, error) - - // Update a unit type - Update(id int64, UnitType *UnitType) (bool, error) - - // Delete a unit type - Delete(id int64) (deleted bool, err error) -} - -var ( - ErrUnitTypeNotFound = errors.New("unit type not found") -) - -type unitTypesService struct { - client *Client -} - -func (s *unitTypesService) Get(id int64) (*UnitType, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.UnitType, map[string]string{"Id": strId}, nil) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var unit_type *UnitType - _, err = s.client.Do(req, &unit_type) - if err != nil { - return nil, err - } - - return unit_type, nil -} - -func (s *unitTypesService) Create(unit_type *UnitType) (bool, error) { - url, err := s.client.url(router.CreateUnitType, nil, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("POST", url.String(), unit_type) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &unit_type) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusCreated, nil -} - -type UnitTypeListOptions struct { - ListOptions -} - -func (s *unitTypesService) List(opt *UnitTypeListOptions) ([]*UnitType, error) { - url, err := s.client.url(router.UnitTypes, nil, opt) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var unit_types []*UnitType - _, err = s.client.Do(req, &unit_types) - if err != nil { - return nil, err - } - - return unit_types, nil -} - -func (s *unitTypesService) Update(id int64, unit_type *UnitType) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.UpdateUnitType, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("PUT", url.String(), unit_type) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &unit_type) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -func (s *unitTypesService) Delete(id int64) (bool, error) { - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.DeleteUnitType, map[string]string{"Id": strId}, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("DELETE", url.String(), nil) - if err != nil { - return false, err - } - - var unit_type *UnitType - resp, err := s.client.Do(req, &unit_type) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusOK, nil -} - -type MockUnitTypesService struct { - Get_ func(id int64) (*UnitType, error) - List_ func(opt *UnitTypeListOptions) ([]*UnitType, error) - Create_ func(unit_type *UnitType) (bool, error) - Update_ func(id int64, unit_type *UnitType) (bool, error) - Delete_ func(id int64) (bool, error) -} - -var _ UnitTypesService = &MockUnitTypesService{} - -func (s *MockUnitTypesService) Get(id int64) (*UnitType, error) { - if s.Get_ == nil { - return nil, nil - } - return s.Get_(id) -} - -func (s *MockUnitTypesService) Create(unit_type *UnitType) (bool, error) { - if s.Create_ == nil { - return false, nil - } - return s.Create_(unit_type) -} - -func (s *MockUnitTypesService) List(opt *UnitTypeListOptions) ([]*UnitType, error) { - if s.List_ == nil { - return nil, nil - } - return s.List_(opt) -} - -func (s *MockUnitTypesService) Update(id int64, unit_type *UnitType) (bool, error) { - if s.Update_ == nil { - return false, nil - } - return s.Update_(id, unit_type) -} - -func (s *MockUnitTypesService) Delete(id int64) (bool, error) { - if s.Delete_ == nil { - return false, nil - } - return s.Delete_(id) -} diff --git a/models/unit_types_test.go b/models/unit_types_test.go deleted file mode 100644 index c08e0b2..0000000 --- a/models/unit_types_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package models - -import ( - "net/http" - "reflect" - "testing" - - "github.com/thermokarst/bactdb/router" -) - -func newUnitType() *UnitType { - unit_type := NewUnitType() - unit_type.Id = 1 - return unit_type -} - -func TestUnitTypeService_Get(t *testing.T) { - setup() - defer teardown() - - want := newUnitType() - - var called bool - mux.HandleFunc(urlPath(t, router.UnitType, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - - writeJSON(w, want) - }) - - unit_type, err := client.UnitTypes.Get(want.Id) - if err != nil { - t.Errorf("UnitTypes.Get returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - - if !reflect.DeepEqual(unit_type, want) { - t.Errorf("UnitTypes.Get return %+v, want %+v", unit_type, want) - } -} - -func TestUnitTypeService_Create(t *testing.T) { - setup() - defer teardown() - - want := newUnitType() - - var called bool - mux.HandleFunc(urlPath(t, router.CreateUnitType, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "POST") - testBody(t, r, `{"id":1,"name":"Test Unit Type","symbol":"x","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}`+"\n") - - w.WriteHeader(http.StatusCreated) - writeJSON(w, want) - }) - - unit_type := newUnitType() - created, err := client.UnitTypes.Create(unit_type) - if err != nil { - t.Errorf("UnitTypes.Create returned error: %v", err) - } - - if !created { - t.Error("!created") - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - if !reflect.DeepEqual(unit_type, want) { - t.Errorf("UnitTypes.Create returned %+v, want %+v", unit_type, want) - } -} - -func TestUnitTypeService_List(t *testing.T) { - setup() - defer teardown() - - want := []*UnitType{newUnitType()} - - var called bool - mux.HandleFunc(urlPath(t, router.UnitTypes, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - testFormValues(t, r, values{}) - - writeJSON(w, want) - }) - - unit_types, err := client.UnitTypes.List(nil) - if err != nil { - t.Errorf("UnitTypes.List returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - for _, u := range want { - normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt) - } - - if !reflect.DeepEqual(unit_types, want) { - t.Errorf("UnitTypes.List return %+v, want %+v", unit_types, want) - } -} - -func TestUnitTypeService_Update(t *testing.T) { - setup() - defer teardown() - - want := newUnitType() - - var called bool - mux.HandleFunc(urlPath(t, router.UpdateUnitType, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "PUT") - testBody(t, r, `{"id":1,"name":"Test Unit Type Updated","symbol":"x","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}`+"\n") - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - unit_type := newUnitType() - unit_type.Name = "Test Unit Type Updated" - updated, err := client.UnitTypes.Update(unit_type.Id, unit_type) - if err != nil { - t.Errorf("UnitTypes.Update returned error: %v", err) - } - - if !updated { - t.Error("!updated") - } - - if !called { - t.Fatal("!called") - } -} - -func TestUnitTypeService_Delete(t *testing.T) { - setup() - defer teardown() - - want := newUnitType() - - var called bool - mux.HandleFunc(urlPath(t, router.DeleteUnitType, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "DELETE") - - w.WriteHeader(http.StatusOK) - writeJSON(w, want) - }) - - deleted, err := client.UnitTypes.Delete(want.Id) - if err != nil { - t.Errorf("UnitTypes.Delete returned error: %v", err) - } - - if !deleted { - t.Error("!deleted") - } - - if !called { - t.Fatal("!called") - } -} diff --git a/models/users.go b/models/users.go deleted file mode 100644 index 3b23b35..0000000 --- a/models/users.go +++ /dev/null @@ -1,191 +0,0 @@ -package models - -import ( - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/thermokarst/bactdb/router" -) - -// A User is a person that has administrative access to bactdb. -// Todo: add password -type User struct { - Id int64 `json:"id,omitempty"` - Username string `db:"username" json:"username"` - Password string `db:"password" json:"-"` - CreatedAt time.Time `db:"created_at" json:"createdAt"` - UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` - DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` -} - -type UserJSON struct { - User *User `json:"user"` -} - -type UsersJSON struct { - Users []*User `json:"users"` -} - -func (m *User) String() string { - return fmt.Sprintf("%v", *m) -} - -func NewUser() *User { - return &User{Username: "Test User"} -} - -// UsersService interacts with the user-related endpoints in bactdb's API. -type UsersService interface { - // Get a user. - Get(id int64) (*User, error) - - // List all users. - List(opt *UserListOptions) ([]*User, error) - - // Create a new user. The newly created user's ID is written to user.Id - Create(user *User) (created bool, err error) - - // Authenticate a user, returns their access level. - Authenticate(username string, password string) (user_session *UserSession, err error) -} - -type UserSession struct { - Token string `json:"token"` - AccessLevel string `json:"access_level"` - Genus string `json:"genus"` -} - -var ( - ErrUserNotFound = errors.New("user not found") -) - -type usersService struct { - client *Client -} - -func (s *usersService) Get(id int64) (*User, error) { - // Pass in key value pairs as strings, so that the gorilla mux URL - // generation is happy. - strId := strconv.FormatInt(id, 10) - - url, err := s.client.url(router.User, map[string]string{"Id": strId}, nil) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var user *UserJSON - _, err = s.client.Do(req, &user) - if err != nil { - return nil, err - } - - return user.User, nil -} - -func (s *usersService) Create(user *User) (bool, error) { - url, err := s.client.url(router.CreateUser, nil, nil) - if err != nil { - return false, err - } - - req, err := s.client.NewRequest("POST", url.String(), UserJSON{User: user}) - if err != nil { - return false, err - } - - resp, err := s.client.Do(req, &user) - if err != nil { - return false, err - } - - return resp.StatusCode == http.StatusCreated, nil -} - -type UserListOptions struct { - ListOptions -} - -func (s *usersService) List(opt *UserListOptions) ([]*User, error) { - url, err := s.client.url(router.Users, nil, opt) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - - var users *UsersJSON - _, err = s.client.Do(req, &users) - if err != nil { - return nil, err - } - - return users.Users, nil -} - -func (s *usersService) Authenticate(username string, password string) (*UserSession, error) { - url, err := s.client.url(router.GetToken, nil, nil) - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest("POST", url.String(), nil) - if err != nil { - return nil, err - } - - var user_session *UserSession - _, err = s.client.Do(req, &user_session) - if err != nil { - return nil, err - } - - return user_session, nil -} - -type MockUsersService struct { - Get_ func(id int64) (*User, error) - List_ func(opt *UserListOptions) ([]*User, error) - Create_ func(user *User) (bool, error) - Authenticate_ func(username string, password string) (*UserSession, error) -} - -var _ UsersService = &MockUsersService{} - -func (s *MockUsersService) Get(id int64) (*User, error) { - if s.Get_ == nil { - return nil, nil - } - return s.Get_(id) -} - -func (s *MockUsersService) Create(user *User) (bool, error) { - if s.Create_ == nil { - return false, nil - } - return s.Create_(user) -} - -func (s *MockUsersService) List(opt *UserListOptions) ([]*User, error) { - if s.List_ == nil { - return nil, nil - } - return s.List_(opt) -} - -func (s *MockUsersService) Authenticate(username string, password string) (*UserSession, error) { - if s.Authenticate_ == nil { - return &UserSession{}, nil - } - return s.Authenticate_(username, password) -} diff --git a/models/users_test.go b/models/users_test.go deleted file mode 100644 index cbf9c63..0000000 --- a/models/users_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package models - -import ( - "net/http" - "reflect" - "testing" - - "github.com/thermokarst/bactdb/router" -) - -func newUser() *User { - user := NewUser() - user.Id = 1 - return user -} - -func TestUsersService_Get(t *testing.T) { - setup() - defer teardown() - - want := newUser() - - var called bool - mux.HandleFunc(urlPath(t, router.User, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - - writeJSON(w, UserJSON{User: want}) - }) - - user, err := client.Users.Get(1) - if err != nil { - t.Errorf("Users.Get returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - - if !reflect.DeepEqual(user, want) { - t.Errorf("Users.Get returned %+v, want %+v", user, want) - } -} - -func TestUsersService_Create(t *testing.T) { - setup() - defer teardown() - - want := newUser() - - var called bool - mux.HandleFunc(urlPath(t, router.CreateUser, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "POST") - testBody(t, r, `{"user":{"id":1,"username":"Test User","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}}`+"\n") - - w.WriteHeader(http.StatusCreated) - writeJSON(w, want) - }) - - user := newUser() - created, err := client.Users.Create(user) - if err != nil { - t.Errorf("Users.Create returned error: %v", err) - } - - if !created { - t.Error("!created") - } - - if !called { - t.Fatal("!called") - } - - normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) - if !reflect.DeepEqual(user, want) { - t.Errorf("Users.Create returned %+v, want %+v", user, want) - } -} - -func TestUsersService_List(t *testing.T) { - setup() - defer teardown() - - want := []*User{newUser()} - - var called bool - mux.HandleFunc(urlPath(t, router.Users, nil), func(w http.ResponseWriter, r *http.Request) { - called = true - testMethod(t, r, "GET") - testFormValues(t, r, values{}) - - writeJSON(w, UsersJSON{Users: want}) - }) - - users, err := client.Users.List(nil) - if err != nil { - t.Errorf("Users.List returned error: %v", err) - } - - if !called { - t.Fatal("!called") - } - - for _, u := range want { - normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt) - } - - if !reflect.DeepEqual(users, want) { - t.Errorf("Users.List return %+v, want %+v", users, want) - } -} diff --git a/router/api.go b/router/api.go deleted file mode 100644 index a1e5733..0000000 --- a/router/api.go +++ /dev/null @@ -1,80 +0,0 @@ -package router - -import "github.com/gorilla/mux" - -func API() *mux.Router { - m := mux.NewRouter() - - // Users - m.Path("/users").Methods("GET").Name(Users) - m.Path("/users").Methods("POST").Name(CreateUser) - m.Path("/users/{Id:.+}").Methods("GET").Name(User) - m.Path("/authenticate").Methods("POST").Name(GetToken) - - // Genera - m.Path("/genera").Methods("GET").Name(Genera) - m.Path("/genera").Methods("POST").Name(CreateGenus) - m.Path("/genera/{Id:.+}").Methods("GET").Name(Genus) - m.Path("/genera/{Id:.+}").Methods("PUT").Name(UpdateGenus) - m.Path("/genera/{Id:.+}").Methods("DELETE").Name(DeleteGenus) - - // Species - m.Path("/species").Methods("GET").Name(SpeciesList) - m.Path("/species").Methods("POST").Name(CreateSpecies) - m.Path("/species/{Id:.+}").Methods("GET").Name(Species) - m.Path("/species/{Id:.+}").Methods("PUT").Name(UpdateSpecies) - m.Path("/species/{Id:.+}").Methods("DELETE").Name(DeleteSpecies) - - // Strains - m.Path("/strains").Methods("GET").Name(Strains) - m.Path("/strains").Methods("POST").Name(CreateStrain) - m.Path("/strains/{Id:.+}").Methods("GET").Name(Strain) - m.Path("/strains/{Id:.+}").Methods("PUT").Name(UpdateStrain) - m.Path("/strains/{Id:.+}").Methods("DELETE").Name(DeleteStrain) - - // CharacteristicTypes - m.Path("/characteristic_types").Methods("GET").Name(CharacteristicTypes) - m.Path("/characteristic_types").Methods("POST").Name(CreateCharacteristicType) - m.Path("/characteristic_types/{Id:.+}").Methods("GET").Name(CharacteristicType) - m.Path("/characteristic_types/{Id:.+}").Methods("PUT").Name(UpdateCharacteristicType) - m.Path("/characteristic_types/{Id:.+}").Methods("DELETE").Name(DeleteCharacteristicType) - - // Characteristics - m.Path("/characteristics").Methods("GET").Name(Characteristics) - m.Path("/characteristics").Methods("POST").Name(CreateCharacteristic) - m.Path("/characteristics/{Id:.+}").Methods("GET").Name(Characteristic) - m.Path("/characteristics/{Id:.+}").Methods("PUT").Name(UpdateCharacteristic) - m.Path("/characteristics/{Id:.+}").Methods("DELETE").Name(DeleteCharacteristic) - - // TextMeasurementTypes - m.Path("/text_measurement_types").Methods("GET").Name(TextMeasurementTypes) - m.Path("/text_measurement_types").Methods("POST").Name(CreateTextMeasurementType) - m.Path("/text_measurement_types/{Id:.+}").Methods("GET").Name(TextMeasurementType) - m.Path("/text_measurement_types/{Id:.+}").Methods("PUT").Name(UpdateTextMeasurementType) - m.Path("/text_measurement_types/{Id:.+}").Methods("DELETE").Name(DeleteTextMeasurementType) - - // UnitTypes - m.Path("/unit_types").Methods("GET").Name(UnitTypes) - m.Path("/unit_types").Methods("POST").Name(CreateUnitType) - m.Path("/unit_types/{Id:.+}").Methods("GET").Name(UnitType) - m.Path("/unit_types/{Id:.+}").Methods("PUT").Name(UpdateUnitType) - m.Path("/unit_types/{Id:.+}").Methods("DELETE").Name(DeleteUnitType) - - // Measurements - m.Path("/measurements").Methods("GET").Name(Measurements) - m.Path("/measurements").Methods("POST").Name(CreateMeasurement) - m.Path("/measurements/{Id:.+}").Methods("GET").Name(Measurement) - m.Path("/measurements/{Id:.+}").Methods("PUT").Name(UpdateMeasurement) - m.Path("/measurements/{Id:.+}").Methods("DELETE").Name(DeleteMeasurement) - - // Subrouter for auth/security - s := m.PathPrefix("/{genus}").Subrouter() - s.Path("/species").Methods("GET").Name(SubrouterListSpecies) - s.Path("/strains").Methods("GET").Name(SubrouterListStrains) - s.Path("/measurements").Methods("GET").Name(SubrouterListMeasurements) - - // Misc - m.Path("/health").Methods("GET").Name(Health) - - return m -} diff --git a/router/routes.go b/router/routes.go deleted file mode 100644 index 0c38838..0000000 --- a/router/routes.go +++ /dev/null @@ -1,62 +0,0 @@ -package router - -const ( - User = "users:get" - CreateUser = "users:create" - Users = "users:list" - GetToken = "token:get" - - Genus = "genus:get" - CreateGenus = "genus:create" - Genera = "genus:list" - UpdateGenus = "genus:update" - DeleteGenus = "genus:delete" - - Species = "species:get" - CreateSpecies = "species:create" - SpeciesList = "species:list" - UpdateSpecies = "species:update" - DeleteSpecies = "species:delete" - - Strain = "strain:get" - CreateStrain = "strain:create" - Strains = "strain:list" - UpdateStrain = "strain:update" - DeleteStrain = "strain:delete" - - CharacteristicType = "characteristic_type:get" - CreateCharacteristicType = "characteristic_type:create" - CharacteristicTypes = "characteristic_type:list" - UpdateCharacteristicType = "characteristic_type:update" - DeleteCharacteristicType = "characteristic_type:delete" - - Characteristic = "characteristic:get" - CreateCharacteristic = "characteristic:create" - Characteristics = "characteristic:list" - UpdateCharacteristic = "characteristic:update" - DeleteCharacteristic = "characteristic:delete" - - TextMeasurementType = "text_measurement_type:get" - CreateTextMeasurementType = "text_measurement_type:create" - TextMeasurementTypes = "text_measurement_type:list" - UpdateTextMeasurementType = "text_measurement_type:update" - DeleteTextMeasurementType = "text_measurement_type:delete" - - UnitType = "unit_type:get" - CreateUnitType = "unit_type:create" - UnitTypes = "unit_type:list" - UpdateUnitType = "unit_type:update" - DeleteUnitType = "unit_type:delete" - - Measurement = "measurements:get" - CreateMeasurement = "measurements:create" - Measurements = "measurements:list" - UpdateMeasurement = "measurements:update" - DeleteMeasurement = "measurements:delete" - - SubrouterListSpecies = "subrouter_species:list" - SubrouterListStrains = "subrouter_strains:list" - SubrouterListMeasurements = "subrouter_measurements:list" - - Health = "health:get" -) diff --git a/shippable.yml b/shippable.yml deleted file mode 100644 index c666809..0000000 --- a/shippable.yml +++ /dev/null @@ -1,37 +0,0 @@ -language: go - -addons: - postgresql: 9.3 - -go: 1.4 - -env: - global: - - APP_NAME=bactdb - - CI_REPORT=shippable/testresults/junit.xml - - secure: TzuB7Rwi0KQH2MNim2iMKkvKttWPzcnxy9sItlpdQSAaqUmg3H0emChA3WOX/E6YvnaowUdexTOyuBoYKeFreAWD1rnNye+B67BpsAjrdizkqZ4q9NtoQQ4ZAzXv/k6ly0tTpQBMvDoMeyFsEpUDkX6rJHhuAIOdVdhk+zHPHLPuXkR3RrQsthkPWjN3lnLJXECaHSSrfL/ibJybJLtzBGrpK+AlDDRTI7p26VdtITxgRrybTSF19BnltqaompubiwZUCQoy1Zgk9yp2S7SyPZtHZwnoYHN5uWS0XVKndXF/n+B4rphUzBPXWBfaW+9KeDQvY8UEA/EkLtxmNNKPrw== - -before_install: - - export GOPATH=$SHIPPABLE_GOPATH - - source $HOME/.gvm/scripts/gvm - - gvm install go$SHIPPABLE_GO_VERSION - - gvm use go$SHIPPABLE_GO_VERSION - - go get github.com/t-yuki/gocover-cobertura - - go get golang.org/x/tools/cmd/cover - - go get -u github.com/jstemmer/go-junit-report - - which heroku || wget -qO- https://toolbelt.heroku.com/install-ubuntu.sh | sh - -before_script: - - psql -c 'CREATE DATABASE bactdbtest;' -U postgres - - mkdir -p shippable/testresults - - mkdir -p shippable/codecoverage - -script: - - ./test.sh | $GOPATH/bin/go-junit-report > shippable/testresults/junit.xml - - $GOPATH/bin/gocover-cobertura < coverage.txt > shippable/codecoverage/coverage.xml - -after_success: - - test -f ~/.ssh/id_rsa.heroku || ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.heroku && heroku keys:add ~/.ssh/id_rsa.heroku - - git remote -v | grep ^heroku || heroku git:remote --ssh-git --app $APP_NAME - - git push -f heroku $BRANCH:master - - heroku run bactdb migrate diff --git a/strains.go b/strains.go new file mode 100644 index 0000000..2f033e6 --- /dev/null +++ b/strains.go @@ -0,0 +1,157 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" +) + +var ErrStrainNotFound = errors.New("strain not found") + +func init() { + DB.AddTableWithName(StrainBase{}, "strains").SetKeys(true, "Id") +} + +// StrainBase is what the DB expects to see for inserts/updates +type StrainBase struct { + Id int64 `json:"id,omitempty"` + SpeciesId int64 `db:"species_id" json:"-"` + StrainName string `db:"strain_name" json:"strainName"` + StrainType string `db:"strain_type" json:"strainType"` + Etymology NullString `db:"etymology" json:"etymology"` + AccessionBanks string `db:"accession_banks" json:"accessionBanks"` + GenbankEmblDdb NullString `db:"genbank_embl_ddb" json:"genbankEmblDdb"` + IsolatedFrom NullString `db:"isolated_from" json:"isolatedFrom"` + CreatedAt time.Time `db:"created_at" json:"createdAt"` + UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` + DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` +} + +// Strain & StrainJSON(s) are what ember expects to see +type Strain struct { + *StrainBase + SpeciesName string `db:"species_name" json:"speciesName"` + Measurements NullSliceInt64 `db:"measurements" json:"measurements"` + TotalMeasurements int `db:"total_measurements" json:"totalMeasurements"` +} + +type StrainJSON struct { + Strain *Strain `json:"strain"` +} + +type StrainsJSON struct { + Strains []*Strain `json:"strains"` +} + +type StrainListOptions struct { + ListOptions + Genus string +} + +func serveStrainsList(w http.ResponseWriter, r *http.Request) { + var opt StrainListOptions + if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + opt.Genus = mux.Vars(r)["genus"] + + strains, err := dbGetStrains(&opt) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if strains == nil { + strains = []*Strain{} + } + data, err := json.Marshal(StrainsJSON{Strains: strains}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.Write(data) +} + +func serveStrain(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 + } + + strain, err := dbGetStrain(id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data, err := json.Marshal(StrainJSON{Strain: strain}) + 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 dbGetStrains(opt *StrainListOptions) ([]*Strain, error) { + if opt == nil { + return nil, errors.New("must provide options") + } + var vals []interface{} + + sql := `SELECT st.*, sp.species_name, array_agg(m.id) AS measurements, + COUNT(m) AS total_measurements + FROM strains st + 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 measurements m ON m.strain_id=st.id` + vals = append(vals, opt.Genus) + + if len(opt.Ids) != 0 { + var conds []string + s := "st.id IN (" + for i, id := range opt.Ids { + s = s + fmt.Sprintf("$%v,", i+2) // start param index at 2 + vals = append(vals, id) + } + s = s[:len(s)-1] + ")" + conds = append(conds, s) + sql += " WHERE (" + strings.Join(conds, ") AND (") + ")" + } + + sql += " GROUP BY st.id, sp.species_name;" + + var strains []*Strain + err := DBH.Select(&strains, sql, vals...) + if err != nil { + return nil, err + } + return strains, nil +} + +func dbGetStrain(id int64) (*Strain, error) { + var strain Strain + sql := `SELECT st.*, sp.species_name, array_agg(m.id) AS measurements, + COUNT(m) AS total_measurements + FROM strains st + INNER JOIN species sp ON sp.id=st.species_id + LEFT OUTER JOIN measurements m ON m.strain_id=st.id + WHERE st.id=$1 + GROUP BY st.id, sp.species_name;` + err := DBH.SelectOne(&strain, sql, id) + if err != nil { + return nil, err + } + if &strain == nil { + return nil, ErrStrainNotFound + } + return &strain, nil +} diff --git a/test.sh b/test.sh deleted file mode 100755 index 8dd0242..0000000 --- a/test.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -# FROM: https://github.com/getlantern/flashlight-build/blob/devel/testandcover.bash - -function die() { - echo $* - exit 1 -} - -export GOPATH=`pwd`:$GOPATH - -echo "mode: count" > coverage.txt - -ERROR="" - -for pkg in `cat testpackages.txt` -do - go test -v -covermode=count -coverprofile=profile_tmp.cov $pkg || ERROR="Error testing $pkg" - tail -n +2 profile_tmp.cov >> coverage.txt || die "Unable to append coverage for $pkg" -done - -if [ ! -z "$ERROR" ] -then - die "Encountered error, last error was: $ERROR" -fi diff --git a/testpackages.txt b/testpackages.txt deleted file mode 100644 index e72f979..0000000 --- a/testpackages.txt +++ /dev/null @@ -1,3 +0,0 @@ -github.com/thermokarst/bactdb/models -github.com/thermokarst/bactdb/datastore -github.com/thermokarst/bactdb/api diff --git a/models/types.go b/types.go similarity index 99% rename from models/types.go rename to types.go index ac356cd..ca16199 100644 --- a/models/types.go +++ b/types.go @@ -1,4 +1,4 @@ -package models +package main import ( "bytes" diff --git a/users.go b/users.go new file mode 100644 index 0000000..1622631 --- /dev/null +++ b/users.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "time" + + "github.com/dgrijalva/jwt-go" + "golang.org/x/crypto/bcrypt" +) + +var ( + ErrUserNotFound = errors.New("user not found") +) + +func init() { + DB.AddTableWithName(User{}, "users").SetKeys(true, "Id") +} + +// A User is a person that has administrative access to bactdb. +// Todo: add password +type User struct { + Id int64 `json:"id,omitempty"` + Username string `db:"username" json:"username"` + Password string `db:"password" json:"-"` + CreatedAt time.Time `db:"created_at" json:"createdAt"` + UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` + DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` +} + +type UserJSON struct { + User *User `json:"user"` +} + +type UsersJSON struct { + Users []*User `json:"users"` +} + +func (m *User) String() string { + return fmt.Sprintf("%v", *m) +} + +type UserSession struct { + Token string `json:"token"` + AccessLevel string `json:"access_level"` + Genus string `json:"genus"` +} + +func serveAuthenticateUser(w http.ResponseWriter, r *http.Request) { + username := r.FormValue("username") + password := r.FormValue("password") + + user_session, err := dbAuthenticate(username, password) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + 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 + } + user_session.Token = tokenString + data, err := json.Marshal(user_session) + 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 dbAuthenticate(username string, password string) (*UserSession, error) { + var users []User + var user_session UserSession + + if err := DBH.Select(&users, `SELECT * FROM users WHERE username=$1;`, username); err != nil { + return nil, err + } + if len(users) == 0 { + return nil, 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 +}