Species Read - Order of ops:
router/routes.go router/api.go models/species_test.go models/species.go models/client.go datastore/migrations/addspecies.sql datastore/migrations/dropspecies.sql datastore/species_test.go datastore/species.go datastore/datastore.go api/species_test.go api/species.go api/handler.go
This commit is contained in:
		
							parent
							
								
									6fe6d5d189
								
							
						
					
					
						commit
						830a8805c9
					
				
					 13 changed files with 263 additions and 7 deletions
				
			
		|  | @ -29,6 +29,8 @@ func Handler() *mux.Router { | ||||||
| 	m.Get(router.UpdateGenus).Handler(handler(serveUpdateGenus)) | 	m.Get(router.UpdateGenus).Handler(handler(serveUpdateGenus)) | ||||||
| 	m.Get(router.DeleteGenus).Handler(handler(serveDeleteGenus)) | 	m.Get(router.DeleteGenus).Handler(handler(serveDeleteGenus)) | ||||||
| 
 | 
 | ||||||
|  | 	m.Get(router.Species).Handler(handler(serveSpecies)) | ||||||
|  | 
 | ||||||
| 	return m | 	return m | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										22
									
								
								api/species.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								api/species.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								api/species_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								api/species_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | 	// } | ||||||
|  | } | ||||||
|  | @ -11,6 +11,7 @@ import ( | ||||||
| type Datastore struct { | type Datastore struct { | ||||||
| 	Users   models.UsersService | 	Users   models.UsersService | ||||||
| 	Genera  models.GeneraService | 	Genera  models.GeneraService | ||||||
|  | 	Species models.SpeciesService | ||||||
| 	dbh     modl.SqlExecutor | 	dbh     modl.SqlExecutor | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -29,6 +30,7 @@ func NewDatastore(dbh modl.SqlExecutor) *Datastore { | ||||||
| 	d := &Datastore{dbh: dbh} | 	d := &Datastore{dbh: dbh} | ||||||
| 	d.Users = &usersStore{d} | 	d.Users = &usersStore{d} | ||||||
| 	d.Genera = &generaStore{d} | 	d.Genera = &generaStore{d} | ||||||
|  | 	d.Species = &speciesStore{d} | ||||||
| 	return d | 	return d | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -36,5 +38,6 @@ func NewMockDatastore() *Datastore { | ||||||
| 	return &Datastore{ | 	return &Datastore{ | ||||||
| 		Users:   &models.MockUsersService{}, | 		Users:   &models.MockUsersService{}, | ||||||
| 		Genera:  &models.MockGeneraService{}, | 		Genera:  &models.MockGeneraService{}, | ||||||
|  | 		Species: &models.MockSpeciesService{}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								datastore/migrations/00003_AddSpecies_down.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								datastore/migrations/00003_AddSpecies_down.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | -- bactdb | ||||||
|  | -- Matthew R Dillon | ||||||
|  | 
 | ||||||
|  | DROP TABLE species; | ||||||
|  | 
 | ||||||
							
								
								
									
										15
									
								
								datastore/migrations/00003_AddSpecies_up.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								datastore/migrations/00003_AddSpecies_up.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | ); | ||||||
							
								
								
									
										22
									
								
								datastore/species.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								datastore/species.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								datastore/species_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								datastore/species_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -18,6 +18,7 @@ import ( | ||||||
| type Client struct { | type Client struct { | ||||||
| 	Users   UsersService | 	Users   UsersService | ||||||
| 	Genera  GeneraService | 	Genera  GeneraService | ||||||
|  | 	Species SpeciesService | ||||||
| 
 | 
 | ||||||
| 	// BaseURL for HTTP requests to bactdb's API. | 	// BaseURL for HTTP requests to bactdb's API. | ||||||
| 	BaseURL *url.URL | 	BaseURL *url.URL | ||||||
|  | @ -47,6 +48,7 @@ func NewClient(httpClient *http.Client) *Client { | ||||||
| 	} | 	} | ||||||
| 	c.Users = &usersService{c} | 	c.Users = &usersService{c} | ||||||
| 	c.Genera = &generaService{c} | 	c.Genera = &generaService{c} | ||||||
|  | 	c.Species = &speciesService{c} | ||||||
| 	return c | 	return c | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										69
									
								
								models/species.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								models/species.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								models/species_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								models/species_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -16,5 +16,9 @@ func API() *mux.Router { | ||||||
| 	m.Path("/genera/{Id:.+}").Methods("GET").Name(Genus) | 	m.Path("/genera/{Id:.+}").Methods("GET").Name(Genus) | ||||||
| 	m.Path("/genera/{Id:.+}").Methods("PUT").Name(UpdateGenus) | 	m.Path("/genera/{Id:.+}").Methods("PUT").Name(UpdateGenus) | ||||||
| 	m.Path("/genera/{Id:.+}").Methods("DELETE").Name(DeleteGenus) | 	m.Path("/genera/{Id:.+}").Methods("DELETE").Name(DeleteGenus) | ||||||
|  | 
 | ||||||
|  | 	// Species | ||||||
|  | 	m.Path("/species/{Id:.+}").Methods("GET").Name(Species) | ||||||
|  | 
 | ||||||
| 	return m | 	return m | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,4 +10,6 @@ const ( | ||||||
| 	Genera      = "genera" | 	Genera      = "genera" | ||||||
| 	UpdateGenus = "genus:update" | 	UpdateGenus = "genus:update" | ||||||
| 	DeleteGenus = "genus:delete" | 	DeleteGenus = "genus:delete" | ||||||
|  | 
 | ||||||
|  | 	Species = "species" | ||||||
| ) | ) | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Matthew Dillon
						Matthew Dillon