Refactor: generic handlers
This commit is contained in:
parent
58843133f5
commit
d413226d4d
6 changed files with 166 additions and 214 deletions
|
@ -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
18
entities.go
Normal 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)
|
||||
}
|
103
handlers.go
103
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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
118
strains.go
118
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
|
||||
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
|
||||
|
|
Reference in a new issue