diff --git a/api/handler.go b/api/handler.go index a01506b..384740a 100644 --- a/api/handler.go +++ b/api/handler.go @@ -29,6 +29,8 @@ func Handler() *mux.Router { m.Get(router.UpdateGenus).Handler(handler(serveUpdateGenus)) m.Get(router.DeleteGenus).Handler(handler(serveDeleteGenus)) + m.Get(router.Species).Handler(handler(serveSpecies)) + return m } diff --git a/api/species.go b/api/species.go new file mode 100644 index 0000000..c8e127c --- /dev/null +++ b/api/species.go @@ -0,0 +1,22 @@ +package api + +import ( + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +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, species) +} diff --git a/api/species_test.go b/api/species_test.go new file mode 100644 index 0000000..5d1bf87 --- /dev/null +++ b/api/species_test.go @@ -0,0 +1,34 @@ +package api + +import ( + "testing" + + "github.com/thermokarst/bactdb/models" +) + +func TestSpecies_Get(t *testing.T) { + setup() + + want := &models.Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"} + + 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 + } + + _, 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) + // } +} diff --git a/datastore/datastore.go b/datastore/datastore.go index 43faafd..1919e3a 100644 --- a/datastore/datastore.go +++ b/datastore/datastore.go @@ -9,9 +9,10 @@ import ( // A datastore access point (in PostgreSQL) type Datastore struct { - Users models.UsersService - Genera models.GeneraService - dbh modl.SqlExecutor + Users models.UsersService + Genera models.GeneraService + Species models.SpeciesService + dbh modl.SqlExecutor } var ( @@ -29,12 +30,14 @@ func NewDatastore(dbh modl.SqlExecutor) *Datastore { d := &Datastore{dbh: dbh} d.Users = &usersStore{d} d.Genera = &generaStore{d} + d.Species = &speciesStore{d} return d } func NewMockDatastore() *Datastore { return &Datastore{ - Users: &models.MockUsersService{}, - Genera: &models.MockGeneraService{}, + Users: &models.MockUsersService{}, + Genera: &models.MockGeneraService{}, + Species: &models.MockSpeciesService{}, } } diff --git a/datastore/migrations/00003_AddSpecies_down.sql b/datastore/migrations/00003_AddSpecies_down.sql new file mode 100644 index 0000000..3ce7db5 --- /dev/null +++ b/datastore/migrations/00003_AddSpecies_down.sql @@ -0,0 +1,5 @@ +-- bactdb +-- Matthew R Dillon + +DROP TABLE species; + diff --git a/datastore/migrations/00003_AddSpecies_up.sql b/datastore/migrations/00003_AddSpecies_up.sql new file mode 100644 index 0000000..d290de1 --- /dev/null +++ b/datastore/migrations/00003_AddSpecies_up.sql @@ -0,0 +1,15 @@ +-- bactdb +-- Matthew Dillon + +CREATE TABLE species ( + id BIGSERIAL NOT NULL, + genusid BIGINT, + speciesname CHARACTER VARYING(100), + + createdat TIMESTAMP WITH TIME ZONE, + updatedat TIMESTAMP WITH TIME ZONE, + deletedat TIMESTAMP WITH TIME ZONE, + + CONSTRAINT species_pkey PRIMARY KEY (id), + FOREIGN KEY (genusid) REFERENCES genera(id) +); diff --git a/datastore/species.go b/datastore/species.go new file mode 100644 index 0000000..a7f3c17 --- /dev/null +++ b/datastore/species.go @@ -0,0 +1,22 @@ +package datastore + +import "github.com/thermokarst/bactdb/models" + +func init() { + DB.AddTableWithName(models.Species{}, "species").SetKeys(true, "Id") +} + +type speciesStore struct { + *Datastore +} + +func (s *speciesStore) Get(id int64) (*models.Species, error) { + var species []*models.Species + if err := s.dbh.Select(&species, `SELECT * FROM species WHERE id=$1;`, id); err != nil { + return nil, err + } + if len(species) == 0 { + return nil, models.ErrSpeciesNotFound + } + return species[0], nil +} diff --git a/datastore/species_test.go b/datastore/species_test.go new file mode 100644 index 0000000..915d522 --- /dev/null +++ b/datastore/species_test.go @@ -0,0 +1,37 @@ +package datastore + +import ( + "reflect" + "testing" + + "github.com/thermokarst/bactdb/models" +) + +func TestSpeciesStore_Get_db(t *testing.T) { + tx, _ := DB.Begin() + defer tx.Rollback() + + // Test on a clean database + tx.Exec(`DELETE FROM species;`) + + wantGenus := &models.Genus{GenusName: "Test Genus"} + if err := tx.Insert(wantGenus); err != nil { + t.Fatal(err) + } + + want := &models.Species{Id: 1, GenusId: wantGenus.Id, SpeciesName: "Test Species"} + if err := tx.Insert(want); err != nil { + t.Fatal(err) + } + + d := NewDatastore(tx) + species, err := d.Species.Get(1) + if err != nil { + t.Fatal(err) + } + + normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) + if !reflect.DeepEqual(species, want) { + t.Errorf("got species %+v, want %+v", species, want) + } +} diff --git a/models/client.go b/models/client.go index bb321e7..b94f3f1 100644 --- a/models/client.go +++ b/models/client.go @@ -16,8 +16,9 @@ import ( // A Client communicates with bactdb's HTTP API. type Client struct { - Users UsersService - Genera GeneraService + Users UsersService + Genera GeneraService + Species SpeciesService // BaseURL for HTTP requests to bactdb's API. BaseURL *url.URL @@ -47,6 +48,7 @@ func NewClient(httpClient *http.Client) *Client { } c.Users = &usersService{c} c.Genera = &generaService{c} + c.Species = &speciesService{c} return c } diff --git a/models/species.go b/models/species.go new file mode 100644 index 0000000..336c474 --- /dev/null +++ b/models/species.go @@ -0,0 +1,69 @@ +package models + +import ( + "errors" + "strconv" + "time" + + "github.com/thermokarst/bactdb/router" +) + +// A Species is a high-level classifier in bactdb. +type Species struct { + Id int64 `json:"id,omitempty"` + GenusId int64 `json:"genus_id"` + SpeciesName string `json:"species_name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt time.Time `json:"deleted_at"` +} + +// SpeciesService interacts with the species-related endpoints in bactdb's API. +type SpeciesService interface { + // Get a species + Get(id int64) (*Species, 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 *Species + _, err = s.client.Do(req, &species) + if err != nil { + return nil, err + } + + return species, nil +} + +type MockSpeciesService struct { + Get_ func(id int64) (*Species, error) +} + +var _ SpeciesService = &MockSpeciesService{} + +func (s *MockSpeciesService) Get(id int64) (*Species, error) { + if s.Get_ == nil { + return nil, nil + } + return s.Get_(id) +} diff --git a/models/species_test.go b/models/species_test.go new file mode 100644 index 0000000..e080179 --- /dev/null +++ b/models/species_test.go @@ -0,0 +1,39 @@ +package models + +import ( + "net/http" + "reflect" + "testing" + + "github.com/thermokarst/bactdb/router" +) + +func TestSpeciesService_Get(t *testing.T) { + setup() + defer teardown() + + want := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"} + + 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, want) + }) + + species, err := client.Species.Get(1) + 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) + } +} diff --git a/router/api.go b/router/api.go index 1df34ee..229bddc 100644 --- a/router/api.go +++ b/router/api.go @@ -16,5 +16,9 @@ func API() *mux.Router { 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/{Id:.+}").Methods("GET").Name(Species) + return m } diff --git a/router/routes.go b/router/routes.go index e3bb7bb..9c4ed37 100644 --- a/router/routes.go +++ b/router/routes.go @@ -10,4 +10,6 @@ const ( Genera = "genera" UpdateGenus = "genus:update" DeleteGenus = "genus:delete" + + Species = "species" )