From d413226d4dc0abde0343dd42191afcca5bd39081 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Tue, 2 Jun 2015 11:11:06 -0800 Subject: [PATCH] Refactor: generic handlers --- characteristics.go | 70 +++++---------------------- entities.go | 18 +++++++ handlers.go | 103 ++++++++++++++++++++++++++++++++++++--- helpers.go | 1 + measurements.go | 70 +++++---------------------- strains.go | 118 ++++++++++----------------------------------- 6 files changed, 166 insertions(+), 214 deletions(-) create mode 100644 entities.go diff --git a/characteristics.go b/characteristics.go index 3bc9605..873d93a 100644 --- a/characteristics.go +++ b/characteristics.go @@ -4,18 +4,16 @@ import ( "encoding/json" "errors" "fmt" - "net/http" - "strconv" "strings" "time" - - "github.com/gorilla/mux" ) func init() { DB.AddTableWithName(CharacteristicBase{}, "characteristics").SetKeys(true, "Id") } +type CharacteristicService struct{} + // A Characteristic is a lookup type type CharacteristicBase struct { Id int64 `json:"id,omitempty"` @@ -36,67 +34,25 @@ type Characteristic struct { CharacteristicTypeName string `db:"characteristic_type_name" json:"characteristicType"` } +type Characteristics []*Characteristic + type CharacteristicJSON struct { Characteristic *Characteristic `json:"characteristic"` } type CharacteristicsJSON struct { - Characteristics []*Characteristic `json:"characteristics"` + Characteristics *Characteristics `json:"characteristics"` } -type CharacteristicListOptions struct { - ListOptions - Genus string +func (c *Characteristic) marshal() ([]byte, error) { + return json.Marshal(&CharacteristicJSON{Characteristic: c}) } -func serveCharacteristicsList(w http.ResponseWriter, r *http.Request) { - var opt CharacteristicListOptions - if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - opt.Genus = mux.Vars(r)["genus"] - - characteristics, err := dbGetCharacteristics(&opt) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if characteristics == nil { - characteristics = []*Characteristic{} - } - data, err := json.Marshal(CharacteristicsJSON{Characteristics: characteristics}) - 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 (c *Characteristics) marshal() ([]byte, error) { + return json.Marshal(&CharacteristicsJSON{Characteristics: c}) } -func serveCharacteristic(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 - } - - characteristic, err := dbGetCharacteristic(id) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - data, err := json.Marshal(CharacteristicJSON{Characteristic: characteristic}) - 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 dbGetCharacteristics(opt *CharacteristicListOptions) ([]*Characteristic, error) { +func (c CharacteristicService) list(opt *ListOptions) (entity, error) { if opt == nil { return nil, errors.New("must provide options") } @@ -123,15 +79,15 @@ func dbGetCharacteristics(opt *CharacteristicListOptions) ([]*Characteristic, er sql += " GROUP BY c.id, ct.characteristic_type_name;" - var characteristics []*Characteristic + var characteristics Characteristics err := DBH.Select(&characteristics, sql, vals...) if err != nil { return nil, err } - return characteristics, nil + return &characteristics, nil } -func dbGetCharacteristic(id int64) (*Characteristic, error) { +func (c CharacteristicService) get(id int64, dummy string) (entity, error) { var characteristic Characteristic sql := `SELECT c.*, ct.characteristic_type_name, array_agg(m.id) AS measurements, array_agg(st.id) AS strains diff --git a/entities.go b/entities.go new file mode 100644 index 0000000..1d180a4 --- /dev/null +++ b/entities.go @@ -0,0 +1,18 @@ +package main + +type entity interface { + marshal() ([]byte, error) +} + +type getter interface { + get(int64, string) (entity, error) +} + +type lister interface { + list(*ListOptions) (entity, error) +} + +type updater interface { + update(int64, *entity, Claims) error + unmarshal([]byte) (entity, error) +} diff --git a/handlers.go b/handlers.go index 22a8404..77956c2 100644 --- a/handlers.go +++ b/handlers.go @@ -3,8 +3,10 @@ package main import ( "encoding/json" "errors" + "io/ioutil" "net/http" "os" + "strconv" "strings" "time" @@ -83,13 +85,13 @@ func Handler() http.Handler { } routes := []r{ - r{serveStrainsList, "GET", "/strains"}, - r{serveStrain, "GET", "/strains/{Id:.+}"}, - r{serveUpdateStrain, "PUT", "/strains/{Id:.+}"}, - r{serveMeasurementsList, "GET", "/measurements"}, - r{serveMeasurement, "GET", "/measurements/{Id:.+}"}, - r{serveCharacteristicsList, "GET", "/characteristics"}, - r{serveCharacteristic, "GET", "/characteristics/{Id:.+}"}, + r{handleLister(StrainService{}), "GET", "/strains"}, + r{handleGetter(StrainService{}), "GET", "/strains/{Id:.+}"}, + r{handleUpdater(StrainService{}), "PUT", "/strains/{Id:.+}"}, + r{handleLister(MeasurementService{}), "GET", "/measurements"}, + r{handleGetter(MeasurementService{}), "GET", "/measurements/{Id:.+}"}, + r{handleLister(CharacteristicService{}), "GET", "/characteristics"}, + r{handleGetter(CharacteristicService{}), "GET", "/characteristics/{Id:.+}"}, } for _, route := range routes { @@ -99,6 +101,93 @@ func Handler() http.Handler { return corsHandler(m) } +func handleGetter(g getter) http.HandlerFunc { + return func(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 + } + + e, err := g.get(id, mux.Vars(r)["genus"]) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data, err := e.marshal() + 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 handleLister(l lister) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var opt ListOptions + if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + opt.Genus = mux.Vars(r)["genus"] + + es, err := l.list(&opt) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + data, err := es.marshal() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.Write(data) + } +} + +func handleUpdater(u updater) http.HandlerFunc { + return func(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 + } + + bodyBytes, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + e, err := u.unmarshal(bodyBytes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + c := context.Get(r, "claims") + var claims Claims = c.(Claims) + + err = u.update(id, &e, claims) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data, err := e.marshal() + 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 tokenHandler(h http.Handler) http.Handler { token := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") diff --git a/helpers.go b/helpers.go index c2773e1..2be7d1e 100644 --- a/helpers.go +++ b/helpers.go @@ -11,6 +11,7 @@ type ListOptions struct { PerPage int `url:",omitempty" json:",omitempty"` Page int `url:",omitempty" json:",omitempty"` Ids []int `url:",omitempty" json:",omitempty" schema:"ids[]"` + Genus string } func (o ListOptions) PageOrDefault() int { diff --git a/measurements.go b/measurements.go index b63f254..c5d3289 100644 --- a/measurements.go +++ b/measurements.go @@ -4,12 +4,8 @@ import ( "encoding/json" "errors" "fmt" - "net/http" - "strconv" "strings" "time" - - "github.com/gorilla/mux" ) var ErrMeasurementNotFound = errors.New("measurement not found") @@ -18,6 +14,8 @@ func init() { DB.AddTableWithName(MeasurementBase{}, "measurements").SetKeys(true, "Id") } +type MeasurementService struct{} + // 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. @@ -48,67 +46,25 @@ type Measurement struct { TestMethod NullString `db:"test_method_name" json:"testMethod"` } +type Measurements []*Measurement + type MeasurementJSON struct { Measurement *Measurement `json:"measurement"` } type MeasurementsJSON struct { - Measurements []*Measurement `json:"measurements"` + Measurements *Measurements `json:"measurements"` } -type MeasurementListOptions struct { - ListOptions - Genus string +func (m *Measurement) marshal() ([]byte, error) { + return json.Marshal(&MeasurementJSON{Measurement: m}) } -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(MeasurementsJSON{Measurements: 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 (m *Measurements) marshal() ([]byte, error) { + return json.Marshal(&MeasurementsJSON{Measurements: m}) } -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, mux.Vars(r)["genus"]) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - data, err := json.Marshal(MeasurementJSON{Measurement: 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) { +func (m MeasurementService) list(opt *ListOptions) (entity, error) { if opt == nil { return nil, errors.New("must provide options") } @@ -142,15 +98,15 @@ func dbGetMeasurements(opt *MeasurementListOptions) ([]*Measurement, error) { sql += ";" - var measurements []*Measurement + var measurements Measurements err := DBH.Select(&measurements, sql, vals...) if err != nil { return nil, err } - return measurements, nil + return &measurements, nil } -func dbGetMeasurement(id int64, genus string) (*Measurement, error) { +func (m MeasurementService) get(id int64, genus string) (entity, error) { var measurement Measurement sql := `SELECT m.*, c.characteristic_name, t.text_measurement_name AS text_measurement_type_name, diff --git a/strains.go b/strains.go index 131916f..643cce7 100644 --- a/strains.go +++ b/strains.go @@ -5,12 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "net/http" - "strconv" "strings" - - "github.com/gorilla/context" - "github.com/gorilla/mux" ) var ( @@ -22,6 +17,8 @@ func init() { DB.AddTableWithName(StrainBase{}, "strains").SetKeys(true, "Id") } +type StrainService struct{} + // StrainBase is what the DB expects to see for inserts/updates type StrainBase struct { Id int64 `db:"id" json:"id"` @@ -45,105 +42,34 @@ type Strain struct { *StrainBase SpeciesName string `db:"species_name" json:"speciesName"` Measurements NullSliceInt64 `db:"measurements" json:"measurements"` - TotalMeasurements int `db:"total_measurements" json:"totalMeasurements"` + TotalMeasurements int64 `db:"total_measurements" json:"totalMeasurements"` } +type Strains []*Strain + type StrainJSON struct { Strain *Strain `json:"strain"` } type StrainsJSON struct { - Strains []*Strain `json:"strains"` + Strains *Strains `json:"strains"` } -type StrainListOptions struct { - ListOptions - Genus string +func (s *Strain) marshal() ([]byte, error) { + return json.Marshal(&StrainJSON{Strain: s}) } -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 (s *Strains) marshal() ([]byte, error) { + return json.Marshal(&StrainsJSON{Strains: s}) } -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, mux.Vars(r)["genus"]) - 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 (s StrainService) unmarshal(b []byte) (entity, error) { + var sj StrainJSON + err := json.Unmarshal(b, &sj) + return sj.Strain, err } -func serveUpdateStrain(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 - } - - var strainjson StrainJSON - err = json.NewDecoder(r.Body).Decode(&strainjson) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - c := context.Get(r, "claims") - var claims Claims = c.(Claims) - strainjson.Strain.UpdatedBy = claims.Sub - strainjson.Strain.UpdatedAt = currentTime() - strainjson.Strain.Id = id - - err = dbUpdateStrain(strainjson.Strain) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - data, err := json.Marshal(StrainJSON{Strain: strainjson.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) { +func (s StrainService) list(opt *ListOptions) (entity, error) { if opt == nil { return nil, errors.New("must provide options") } @@ -171,15 +97,15 @@ func dbGetStrains(opt *StrainListOptions) ([]*Strain, error) { sql += " GROUP BY st.id, sp.species_name;" - var strains []*Strain + var strains Strains err := DBH.Select(&strains, sql, vals...) if err != nil { return nil, err } - return strains, nil + return &strains, nil } -func dbGetStrain(id int64, genus string) (*Strain, error) { +func (s StrainService) get(id int64, genus string) (entity, error) { var strain Strain q := `SELECT st.*, sp.species_name, array_agg(m.id) AS measurements, COUNT(m) AS total_measurements @@ -198,13 +124,19 @@ func dbGetStrain(id int64, genus string) (*Strain, error) { return &strain, nil } -func dbUpdateStrain(strain *Strain) error { +func (s StrainService) update(id int64, e *entity, claims Claims) error { + strain := (*e).(*Strain) + strain.UpdatedBy = claims.Sub + strain.UpdatedAt = currentTime() + strain.Id = id + var species_id struct{ Id int64 } q := `SELECT id FROM species WHERE species_name = $1;` if err := DBH.SelectOne(&species_id, q, strain.SpeciesName); err != nil { return err } strain.StrainBase.SpeciesId = species_id.Id + count, err := DBH.Update(strain.StrainBase) if err != nil { return err