diff --git a/api/handler.go b/api/handler.go index fd686c6..b2ed52a 100644 --- a/api/handler.go +++ b/api/handler.go @@ -35,6 +35,8 @@ func Handler() *mux.Router { m.Get(router.UpdateSpecies).Handler(handler(serveUpdateSpecies)) m.Get(router.DeleteSpecies).Handler(handler(serveDeleteSpecies)) + m.Get(router.Strain).Handler(handler(serveStrain)) + return m } diff --git a/api/strains.go b/api/strains.go new file mode 100644 index 0000000..13f3158 --- /dev/null +++ b/api/strains.go @@ -0,0 +1,22 @@ +package api + +import ( + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +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, strain) +} diff --git a/api/strains_test.go b/api/strains_test.go new file mode 100644 index 0000000..eb90110 --- /dev/null +++ b/api/strains_test.go @@ -0,0 +1,42 @@ +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) + } +} diff --git a/datastore/datastore.go b/datastore/datastore.go index 1919e3a..b6de014 100644 --- a/datastore/datastore.go +++ b/datastore/datastore.go @@ -12,6 +12,7 @@ type Datastore struct { Users models.UsersService Genera models.GeneraService Species models.SpeciesService + Strains models.StrainsService dbh modl.SqlExecutor } @@ -31,6 +32,7 @@ func NewDatastore(dbh modl.SqlExecutor) *Datastore { d.Users = &usersStore{d} d.Genera = &generaStore{d} d.Species = &speciesStore{d} + d.Strains = &strainsStore{d} return d } @@ -39,5 +41,6 @@ func NewMockDatastore() *Datastore { Users: &models.MockUsersService{}, Genera: &models.MockGeneraService{}, Species: &models.MockSpeciesService{}, + Strains: &models.MockStrainsService{}, } } diff --git a/datastore/strains.go b/datastore/strains.go new file mode 100644 index 0000000..367fbb0 --- /dev/null +++ b/datastore/strains.go @@ -0,0 +1,22 @@ +package datastore + +import "github.com/thermokarst/bactdb/models" + +func init() { + DB.AddTableWithName(models.Strain{}, "strains").SetKeys(true, "Id") +} + +type strainsStore struct { + *Datastore +} + +func (s *strainsStore) Get(id int64) (*models.Strain, error) { + var strain []*models.Strain + if err := s.dbh.Select(&strain, `SELECT * FROM strains WHERE id=$1;`, id); err != nil { + return nil, err + } + if len(strain) == 0 { + return nil, models.ErrStrainNotFound + } + return strain[0], nil +} diff --git a/datastore/strains_test.go b/datastore/strains_test.go new file mode 100644 index 0000000..2b45c5a --- /dev/null +++ b/datastore/strains_test.go @@ -0,0 +1,47 @@ +package datastore + +import ( + "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;`) + strain := newStrain(t, tx) + if err := tx.Insert(strain); err != nil { + t.Fatal(err) + } + return strain +} + +func newStrain(t *testing.T, tx *modl.Transaction) *models.Strain { + // we want to create and insert a species (and genus) record too + species := insertSpecies(t, tx) + return &models.Strain{SpeciesId: species.Id, StrainName: "Test Strain", + StrainType: "Test Type", Etymology: "Test Etymology", + AccessionBanks: "Test Bank", GenbankEmblDdb: "Test Genbank"} +} + +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) + + if !reflect.DeepEqual(strain, want) { + t.Errorf("got strain %+v, want %+v", strain, want) + } +} diff --git a/models/client.go b/models/client.go index b94f3f1..4811b53 100644 --- a/models/client.go +++ b/models/client.go @@ -19,6 +19,7 @@ type Client struct { Users UsersService Genera GeneraService Species SpeciesService + Strains StrainsService // BaseURL for HTTP requests to bactdb's API. BaseURL *url.URL @@ -49,6 +50,7 @@ func NewClient(httpClient *http.Client) *Client { c.Users = &usersService{c} c.Genera = &generaService{c} c.Species = &speciesService{c} + c.Strains = &strainsService{c} return c } diff --git a/models/strains.go b/models/strains.go new file mode 100644 index 0000000..3e5dd7c --- /dev/null +++ b/models/strains.go @@ -0,0 +1,76 @@ +package models + +import ( + "errors" + "strconv" + "time" + + "github.com/thermokarst/bactdb/router" +) + +// A Strain is a subclass of species +type Strain struct { + Id int64 `json:"id,omitempty"` + SpeciesId int64 `db:"species_id" json:"species_id"` + StrainName string `db:"strain_name" json:"strain_name"` + StrainType string `db:"strain_type" json:"strain_type"` + Etymology string `db:"etymology" json:"etymology"` + AccessionBanks string `db:"accession_banks" json:"accession_banks"` + GenbankEmblDdb string `db:"genbank_embl_ddb" json:"genbank_eml_ddb"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + DeletedAt time.Time `db:"deleted_at" json:"deleted_at"` +} + +func NewStrain() *Strain { + return &Strain{StrainName: "Test Strain", StrainType: "Test Type", Etymology: "Test Etymology", AccessionBanks: "Test Accession", GenbankEmblDdb: "Test Genbank"} +} + +// StrainService interacts with the strain-related endpoints in bactdb's API +type StrainsService interface { + // Get a strain + Get(id int64) (*Strain, 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 *Strain + _, err = s.client.Do(req, &strain) + if err != nil { + return nil, err + } + + return strain, nil +} + +type MockStrainsService struct { + Get_ func(id int64) (*Strain, error) +} + +var _ StrainsService = &MockStrainsService{} + +func (s *MockStrainsService) Get(id int64) (*Strain, error) { + if s.Get_ == nil { + return nil, nil + } + return s.Get_(id) +} diff --git a/models/strains_test.go b/models/strains_test.go new file mode 100644 index 0000000..0dd5275 --- /dev/null +++ b/models/strains_test.go @@ -0,0 +1,46 @@ +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, 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) + } +} diff --git a/router/api.go b/router/api.go index a7731ab..74f9e64 100644 --- a/router/api.go +++ b/router/api.go @@ -24,5 +24,8 @@ func API() *mux.Router { m.Path("/species/{Id:.+}").Methods("PUT").Name(UpdateSpecies) m.Path("/species/{Id:.+}").Methods("DELETE").Name(DeleteSpecies) + // Strains + m.Path("/strains/{Id:.+}").Methods("GET").Name(Strain) + return m } diff --git a/router/routes.go b/router/routes.go index a21e77f..b3d64b7 100644 --- a/router/routes.go +++ b/router/routes.go @@ -16,4 +16,6 @@ const ( SpeciesList = "species:list" UpdateSpecies = "species:update" DeleteSpecies = "species:delete" + + Strain = "strain:get" )