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"
"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
func (c *Characteristics) marshal() ([]byte, error) {
return json.Marshal(&CharacteristicsJSON{Characteristics: c})
}
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) {
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

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

View file

@ -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 {

View file

@ -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
func (m *Measurements) marshal() ([]byte, error) {
return json.Marshal(&MeasurementsJSON{Measurements: m})
}
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) {
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,

View file

@ -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
func (s *Strains) marshal() ([]byte, error) {
return json.Marshal(&StrainsJSON{Strains: s})
}
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 StrainService) unmarshal(b []byte) (entity, error) {
var sj StrainJSON
err := json.Unmarshal(b, &sj)
return sj.Strain, err
}
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 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