Refactor: generic handlers

This commit is contained in:
Matthew Dillon 2015-06-02 11:11:06 -08:00
parent 58843133f5
commit d413226d4d
6 changed files with 166 additions and 214 deletions

View file

@ -4,18 +4,16 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http"
"strconv"
"strings" "strings"
"time" "time"
"github.com/gorilla/mux"
) )
func init() { func init() {
DB.AddTableWithName(CharacteristicBase{}, "characteristics").SetKeys(true, "Id") DB.AddTableWithName(CharacteristicBase{}, "characteristics").SetKeys(true, "Id")
} }
type CharacteristicService struct{}
// A Characteristic is a lookup type // A Characteristic is a lookup type
type CharacteristicBase struct { type CharacteristicBase struct {
Id int64 `json:"id,omitempty"` Id int64 `json:"id,omitempty"`
@ -36,67 +34,25 @@ type Characteristic struct {
CharacteristicTypeName string `db:"characteristic_type_name" json:"characteristicType"` CharacteristicTypeName string `db:"characteristic_type_name" json:"characteristicType"`
} }
type Characteristics []*Characteristic
type CharacteristicJSON struct { type CharacteristicJSON struct {
Characteristic *Characteristic `json:"characteristic"` Characteristic *Characteristic `json:"characteristic"`
} }
type CharacteristicsJSON struct { type CharacteristicsJSON struct {
Characteristics []*Characteristic `json:"characteristics"` Characteristics *Characteristics `json:"characteristics"`
} }
type CharacteristicListOptions struct { func (c *Characteristic) marshal() ([]byte, error) {
ListOptions return json.Marshal(&CharacteristicJSON{Characteristic: c})
Genus string
} }
func serveCharacteristicsList(w http.ResponseWriter, r *http.Request) { func (c *Characteristics) marshal() ([]byte, error) {
var opt CharacteristicListOptions return json.Marshal(&CharacteristicsJSON{Characteristics: c})
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 serveCharacteristic(w http.ResponseWriter, r *http.Request) { func (c CharacteristicService) list(opt *ListOptions) (entity, error) {
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) {
if opt == nil { if opt == nil {
return nil, errors.New("must provide options") 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;" sql += " GROUP BY c.id, ct.characteristic_type_name;"
var characteristics []*Characteristic var characteristics Characteristics
err := DBH.Select(&characteristics, sql, vals...) err := DBH.Select(&characteristics, sql, vals...)
if err != nil { if err != nil {
return nil, err 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 var characteristic Characteristic
sql := `SELECT c.*, ct.characteristic_type_name, sql := `SELECT c.*, ct.characteristic_type_name,
array_agg(m.id) AS measurements, array_agg(st.id) AS strains array_agg(m.id) AS measurements, array_agg(st.id) AS strains

18
entities.go Normal file
View file

@ -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)
}

View file

@ -3,8 +3,10 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
@ -83,13 +85,13 @@ func Handler() http.Handler {
} }
routes := []r{ routes := []r{
r{serveStrainsList, "GET", "/strains"}, r{handleLister(StrainService{}), "GET", "/strains"},
r{serveStrain, "GET", "/strains/{Id:.+}"}, r{handleGetter(StrainService{}), "GET", "/strains/{Id:.+}"},
r{serveUpdateStrain, "PUT", "/strains/{Id:.+}"}, r{handleUpdater(StrainService{}), "PUT", "/strains/{Id:.+}"},
r{serveMeasurementsList, "GET", "/measurements"}, r{handleLister(MeasurementService{}), "GET", "/measurements"},
r{serveMeasurement, "GET", "/measurements/{Id:.+}"}, r{handleGetter(MeasurementService{}), "GET", "/measurements/{Id:.+}"},
r{serveCharacteristicsList, "GET", "/characteristics"}, r{handleLister(CharacteristicService{}), "GET", "/characteristics"},
r{serveCharacteristic, "GET", "/characteristics/{Id:.+}"}, r{handleGetter(CharacteristicService{}), "GET", "/characteristics/{Id:.+}"},
} }
for _, route := range routes { for _, route := range routes {
@ -99,6 +101,93 @@ func Handler() http.Handler {
return corsHandler(m) 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 { func tokenHandler(h http.Handler) http.Handler {
token := func(w http.ResponseWriter, r *http.Request) { token := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.Header().Set("Content-Type", "application/json; charset=UTF-8")

View file

@ -11,6 +11,7 @@ type ListOptions struct {
PerPage int `url:",omitempty" json:",omitempty"` PerPage int `url:",omitempty" json:",omitempty"`
Page int `url:",omitempty" json:",omitempty"` Page int `url:",omitempty" json:",omitempty"`
Ids []int `url:",omitempty" json:",omitempty" schema:"ids[]"` Ids []int `url:",omitempty" json:",omitempty" schema:"ids[]"`
Genus string
} }
func (o ListOptions) PageOrDefault() int { func (o ListOptions) PageOrDefault() int {

View file

@ -4,12 +4,8 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http"
"strconv"
"strings" "strings"
"time" "time"
"github.com/gorilla/mux"
) )
var ErrMeasurementNotFound = errors.New("measurement not found") var ErrMeasurementNotFound = errors.New("measurement not found")
@ -18,6 +14,8 @@ func init() {
DB.AddTableWithName(MeasurementBase{}, "measurements").SetKeys(true, "Id") DB.AddTableWithName(MeasurementBase{}, "measurements").SetKeys(true, "Id")
} }
type MeasurementService struct{}
// There are three types of supported measurements: fixed-test, free-text, // There are three types of supported measurements: fixed-test, free-text,
// & numerical. The table has a constraint that will allow one or the other // & numerical. The table has a constraint that will allow one or the other
// for a particular combination of strain & characteristic, but not both. // 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"` TestMethod NullString `db:"test_method_name" json:"testMethod"`
} }
type Measurements []*Measurement
type MeasurementJSON struct { type MeasurementJSON struct {
Measurement *Measurement `json:"measurement"` Measurement *Measurement `json:"measurement"`
} }
type MeasurementsJSON struct { type MeasurementsJSON struct {
Measurements []*Measurement `json:"measurements"` Measurements *Measurements `json:"measurements"`
} }
type MeasurementListOptions struct { func (m *Measurement) marshal() ([]byte, error) {
ListOptions return json.Marshal(&MeasurementJSON{Measurement: m})
Genus string
} }
func serveMeasurementsList(w http.ResponseWriter, r *http.Request) { func (m *Measurements) marshal() ([]byte, error) {
var opt MeasurementListOptions return json.Marshal(&MeasurementsJSON{Measurements: m})
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 serveMeasurement(w http.ResponseWriter, r *http.Request) { func (m MeasurementService) list(opt *ListOptions) (entity, error) {
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) {
if opt == nil { if opt == nil {
return nil, errors.New("must provide options") return nil, errors.New("must provide options")
} }
@ -142,15 +98,15 @@ func dbGetMeasurements(opt *MeasurementListOptions) ([]*Measurement, error) {
sql += ";" sql += ";"
var measurements []*Measurement var measurements Measurements
err := DBH.Select(&measurements, sql, vals...) err := DBH.Select(&measurements, sql, vals...)
if err != nil { if err != nil {
return nil, err 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 var measurement Measurement
sql := `SELECT m.*, c.characteristic_name, sql := `SELECT m.*, c.characteristic_name,
t.text_measurement_name AS text_measurement_type_name, t.text_measurement_name AS text_measurement_type_name,

View file

@ -5,12 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http"
"strconv"
"strings" "strings"
"github.com/gorilla/context"
"github.com/gorilla/mux"
) )
var ( var (
@ -22,6 +17,8 @@ func init() {
DB.AddTableWithName(StrainBase{}, "strains").SetKeys(true, "Id") DB.AddTableWithName(StrainBase{}, "strains").SetKeys(true, "Id")
} }
type StrainService struct{}
// StrainBase is what the DB expects to see for inserts/updates // StrainBase is what the DB expects to see for inserts/updates
type StrainBase struct { type StrainBase struct {
Id int64 `db:"id" json:"id"` Id int64 `db:"id" json:"id"`
@ -45,105 +42,34 @@ type Strain struct {
*StrainBase *StrainBase
SpeciesName string `db:"species_name" json:"speciesName"` SpeciesName string `db:"species_name" json:"speciesName"`
Measurements NullSliceInt64 `db:"measurements" json:"measurements"` 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 { type StrainJSON struct {
Strain *Strain `json:"strain"` Strain *Strain `json:"strain"`
} }
type StrainsJSON struct { type StrainsJSON struct {
Strains []*Strain `json:"strains"` Strains *Strains `json:"strains"`
} }
type StrainListOptions struct { func (s *Strain) marshal() ([]byte, error) {
ListOptions return json.Marshal(&StrainJSON{Strain: s})
Genus string
} }
func serveStrainsList(w http.ResponseWriter, r *http.Request) { func (s *Strains) marshal() ([]byte, error) {
var opt StrainListOptions return json.Marshal(&StrainsJSON{Strains: s})
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 serveStrain(w http.ResponseWriter, r *http.Request) { func (s StrainService) unmarshal(b []byte) (entity, error) {
id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0) var sj StrainJSON
if err != nil { err := json.Unmarshal(b, &sj)
http.Error(w, err.Error(), http.StatusInternalServerError) return sj.Strain, err
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 serveUpdateStrain(w http.ResponseWriter, r *http.Request) { func (s StrainService) list(opt *ListOptions) (entity, error) {
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) {
if opt == nil { if opt == nil {
return nil, errors.New("must provide options") 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;" sql += " GROUP BY st.id, sp.species_name;"
var strains []*Strain var strains Strains
err := DBH.Select(&strains, sql, vals...) err := DBH.Select(&strains, sql, vals...)
if err != nil { if err != nil {
return nil, err 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 var strain Strain
q := `SELECT st.*, sp.species_name, array_agg(m.id) AS measurements, q := `SELECT st.*, sp.species_name, array_agg(m.id) AS measurements,
COUNT(m) AS total_measurements COUNT(m) AS total_measurements
@ -198,13 +124,19 @@ func dbGetStrain(id int64, genus string) (*Strain, error) {
return &strain, nil 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 } var species_id struct{ Id int64 }
q := `SELECT id FROM species WHERE species_name = $1;` q := `SELECT id FROM species WHERE species_name = $1;`
if err := DBH.SelectOne(&species_id, q, strain.SpeciesName); err != nil { if err := DBH.SelectOne(&species_id, q, strain.SpeciesName); err != nil {
return err return err
} }
strain.StrainBase.SpeciesId = species_id.Id strain.StrainBase.SpeciesId = species_id.Id
count, err := DBH.Update(strain.StrainBase) count, err := DBH.Update(strain.StrainBase)
if err != nil { if err != nil {
return err return err