Compare commits
3 commits
6e1640e4b4
...
759c274950
Author | SHA1 | Date | |
---|---|---|---|
759c274950 | |||
cecbeb78fe | |||
e97c9ced0e |
6 changed files with 80 additions and 60 deletions
|
@ -1,13 +1,13 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/render"
|
||||||
"github.com/go-viper/mapstructure/v2"
|
"github.com/go-viper/mapstructure/v2"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
@ -29,57 +29,81 @@ func New() API {
|
||||||
func (a *api) Subrouter() http.Handler {
|
func (a *api) Subrouter() http.Handler {
|
||||||
r := chi.NewMux()
|
r := chi.NewMux()
|
||||||
|
|
||||||
r.Mount("/talkgroup", new(talkgroupAPI).routes())
|
r.Mount("/talkgroup", new(talkgroupAPI).Subrouter())
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
type errResponse struct {
|
type errResponse struct {
|
||||||
text string
|
Err error `json:"-"`
|
||||||
code int
|
Code int `json:"-"`
|
||||||
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusMapping = map[error]errResponse{
|
func (e *errResponse) Render(w http.ResponseWriter, r *http.Request) error {
|
||||||
talkgroups.ErrNotFound: {talkgroups.ErrNotFound.Error(), http.StatusNotFound},
|
switch e.Code {
|
||||||
pgx.ErrNoRows: {"no such record", http.StatusNotFound},
|
case http.StatusNotFound:
|
||||||
|
case http.StatusBadRequest:
|
||||||
|
default:
|
||||||
|
log.Error().Str("path", r.URL.Path).Err(e.Err).Int("code", e.Code).Str("msg", e.Error).Msg("request failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpCode(err error) (string, int) {
|
render.Status(r, e.Code)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func badRequest(err error) render.Renderer {
|
||||||
|
return &errResponse{
|
||||||
|
Err: err,
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
Error: "Bad request",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordNotFound(err error) render.Renderer {
|
||||||
|
return &errResponse{
|
||||||
|
Err: err,
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
Error: "Record not found",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func internalError(err error) render.Renderer {
|
||||||
|
return &errResponse{
|
||||||
|
Err: err,
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
Error: "Internal server error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type errResponder func(error) render.Renderer
|
||||||
|
|
||||||
|
var statusMapping = map[error]errResponder{
|
||||||
|
talkgroups.ErrNotFound: recordNotFound,
|
||||||
|
pgx.ErrNoRows: recordNotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoError(err error) render.Renderer {
|
||||||
c, ok := statusMapping[err]
|
c, ok := statusMapping[err]
|
||||||
if ok {
|
if ok {
|
||||||
return c.text, c.code
|
c(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for e, c := range statusMapping { // check if err wraps an error we know about
|
for e, c := range statusMapping { // check if err wraps an error we know about
|
||||||
if errors.Is(err, e) {
|
if errors.Is(err, e) {
|
||||||
return c.text, c.code
|
return c(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err.Error(), http.StatusInternalServerError
|
return internalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeResponse(w http.ResponseWriter, r *http.Request, data interface{}, err error) {
|
func wErr(w http.ResponseWriter, r *http.Request, v render.Renderer) {
|
||||||
|
err := render.Render(w, r, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Str("path", r.URL.Path).Err(err).Msg("request failed")
|
log.Error().Err(err).Msg("wErr render error")
|
||||||
text, code := httpCode(err)
|
|
||||||
http.Error(w, text, code)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
enc := json.NewEncoder(w)
|
|
||||||
err = enc.Encode(data)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Str("path", r.URL.Path).Err(err).Msg("response marshal failed")
|
|
||||||
text, code := httpCode(err)
|
|
||||||
http.Error(w, text, code)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func reqErr(w http.ResponseWriter, err error, code int) {
|
|
||||||
http.Error(w, err.Error(), code)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeParams(d interface{}, r *http.Request) error {
|
func decodeParams(d interface{}, r *http.Request) error {
|
||||||
|
@ -103,6 +127,6 @@ func decodeParams(d interface{}, r *http.Request) error {
|
||||||
return dec.Decode(m)
|
return dec.Decode(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func badReq(w http.ResponseWriter, err error) {
|
func respond(w http.ResponseWriter, r *http.Request, v interface{}) {
|
||||||
reqErr(w, err, http.StatusBadRequest)
|
render.DefaultResponder(w, r, v)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/forms"
|
"dynatron.me/x/stillbox/internal/forms"
|
||||||
|
@ -14,7 +13,7 @@ import (
|
||||||
type talkgroupAPI struct {
|
type talkgroupAPI struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tga *talkgroupAPI) routes() http.Handler {
|
func (tga *talkgroupAPI) Subrouter() http.Handler {
|
||||||
r := chi.NewMux()
|
r := chi.NewMux()
|
||||||
|
|
||||||
r.Get("/{system:\\d+}/{id:\\d+}", tga.talkgroup)
|
r.Get("/{system:\\d+}/{id:\\d+}", tga.talkgroup)
|
||||||
|
@ -57,7 +56,7 @@ func (tga *talkgroupAPI) talkgroup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
err := decodeParams(&p, r)
|
err := decodeParams(&p, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
badReq(w, err)
|
wErr(w, r, badRequest(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,14 +70,19 @@ func (tga *talkgroupAPI) talkgroup(w http.ResponseWriter, r *http.Request) {
|
||||||
res, err = tgs.TGs(ctx, nil)
|
res, err = tgs.TGs(ctx, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeResponse(w, r, res, err)
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(w, r, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tga *talkgroupAPI) putTalkgroup(w http.ResponseWriter, r *http.Request) {
|
func (tga *talkgroupAPI) putTalkgroup(w http.ResponseWriter, r *http.Request) {
|
||||||
var id tgParams
|
var id tgParams
|
||||||
err := decodeParams(&id, r)
|
err := decodeParams(&id, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
badReq(w, err)
|
wErr(w, r, badRequest(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,19 +93,16 @@ func (tga *talkgroupAPI) putTalkgroup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
err = forms.Unmarshal(r, &input, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
err = forms.Unmarshal(r, &input, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeResponse(w, r, nil, err)
|
wErr(w, r, badRequest(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
input.ID = id.ToID().Pack()
|
input.ID = id.ToID().Pack()
|
||||||
|
|
||||||
record, err := tgs.UpdateTG(ctx, input)
|
record, err := tgs.UpdateTG(ctx, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeResponse(w, r, nil, err)
|
wErr(w, r, autoError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(record)
|
respond(w, r, record)
|
||||||
if err != nil {
|
|
||||||
writeResponse(w, r, nil, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,13 +56,13 @@ func NewClient(ctx context.Context, conf config.DB) (*DB, error) {
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type DBCtxKey string
|
type dBCtxKey string
|
||||||
|
|
||||||
const DBCtxKeyValue DBCtxKey = "dbctx"
|
const DBCtxKey dBCtxKey = "dbctx"
|
||||||
|
|
||||||
// FromCtx returns the database handle from the provided Context.
|
// FromCtx returns the database handle from the provided Context.
|
||||||
func FromCtx(ctx context.Context) *DB {
|
func FromCtx(ctx context.Context) *DB {
|
||||||
c, ok := ctx.Value(DBCtxKeyValue).(*DB)
|
c, ok := ctx.Value(DBCtxKey).(*DB)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("no DB in context")
|
panic("no DB in context")
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ func FromCtx(ctx context.Context) *DB {
|
||||||
|
|
||||||
// CtxWithDB returns a Context with the provided database handle.
|
// CtxWithDB returns a Context with the provided database handle.
|
||||||
func CtxWithDB(ctx context.Context, conn *DB) context.Context {
|
func CtxWithDB(ctx context.Context, conn *DB) context.Context {
|
||||||
return context.WithValue(ctx, DBCtxKeyValue, conn)
|
return context.WithValue(ctx, DBCtxKey, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsNoRows is a convenience function that returns whether a returned error is a database
|
// IsNoRows is a convenience function that returns whether a returned error is a database
|
||||||
|
|
|
@ -4,14 +4,9 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http/pprof"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) installPprof() {
|
func (s *Server) installPprof() {
|
||||||
r := s.r
|
s.r.Mount("/debug", middleware.Profiler())
|
||||||
r.HandleFunc("/debug/pprof/", pprof.Index)
|
|
||||||
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
|
||||||
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
|
||||||
r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
|
||||||
r.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,8 @@ func (s *Server) setupRoutes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
r := s.r
|
r := s.r
|
||||||
r.Use(middleware.WithValue(database.DBCtxKeyValue, s.db))
|
r.Use(middleware.WithValue(database.DBCtxKey, s.db))
|
||||||
r.Use(middleware.WithValue(talkgroups.StoreCtxKeyValue, s.tgs))
|
r.Use(middleware.WithValue(talkgroups.StoreCtxKey, s.tgs))
|
||||||
|
|
||||||
s.installPprof()
|
s.installPprof()
|
||||||
|
|
||||||
|
|
|
@ -57,16 +57,16 @@ type Store interface {
|
||||||
HUP(*config.Config)
|
HUP(*config.Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CtxStoreKey string
|
type storeCtxKey string
|
||||||
|
|
||||||
const StoreCtxKeyValue CtxStoreKey = "store"
|
const StoreCtxKey storeCtxKey = "store"
|
||||||
|
|
||||||
func CtxWithStore(ctx context.Context, s Store) context.Context {
|
func CtxWithStore(ctx context.Context, s Store) context.Context {
|
||||||
return context.WithValue(ctx, StoreCtxKeyValue, s)
|
return context.WithValue(ctx, StoreCtxKey, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func StoreFrom(ctx context.Context) Store {
|
func StoreFrom(ctx context.Context) Store {
|
||||||
s, ok := ctx.Value(StoreCtxKeyValue).(Store)
|
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||||
if !ok {
|
if !ok {
|
||||||
return NewCache()
|
return NewCache()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue