From 6c118f47f7cd4bb086c5081bc798ee854492b9c0 Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Tue, 28 Oct 2014 16:14:14 -0800
Subject: [PATCH] Get a strain.

---
 api/handler.go            |  2 ++
 api/strains.go            | 22 ++++++++++++
 api/strains_test.go       | 42 ++++++++++++++++++++++
 datastore/datastore.go    |  3 ++
 datastore/strains.go      | 22 ++++++++++++
 datastore/strains_test.go | 47 ++++++++++++++++++++++++
 models/client.go          |  2 ++
 models/strains.go         | 76 +++++++++++++++++++++++++++++++++++++++
 models/strains_test.go    | 46 ++++++++++++++++++++++++
 router/api.go             |  3 ++
 router/routes.go          |  2 ++
 11 files changed, 267 insertions(+)
 create mode 100644 api/strains.go
 create mode 100644 api/strains_test.go
 create mode 100644 datastore/strains.go
 create mode 100644 datastore/strains_test.go
 create mode 100644 models/strains.go
 create mode 100644 models/strains_test.go

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"
 )