introduce experimental adapter pattern for hdlrs

This commit is contained in:
Hayden 2023-03-04 22:41:12 -09:00
parent b82fbb01f5
commit b0a9c510ad
No known key found for this signature in database
GPG key ID: 17CF79474E257545
5 changed files with 153 additions and 2 deletions

View file

@ -11,6 +11,7 @@ require (
github.com/go-playground/validator/v10 v10.11.2
github.com/gocarina/gocsv v0.0.0-20230219202803-bcce7dc8d0bb
github.com/google/uuid v1.3.0
github.com/gorilla/schema v1.2.0
github.com/mattn/go-sqlite3 v1.14.16
github.com/rs/zerolog v1.29.0
github.com/stretchr/testify v1.8.2

View file

@ -398,6 +398,8 @@ github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqE
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.15.3/go.mod h1:/g/qgcoBcEXALCNZgRRisyTW0nY86++L0KbeAMXYCeY=

View file

@ -5,7 +5,8 @@ import (
"errors"
)
type UnauthorizedError struct{}
type UnauthorizedError struct {
}
func (err *UnauthorizedError) Error() string {
return "unauthorized"
@ -28,7 +29,7 @@ func (err *InvalidRouteKeyError) Error() string {
return "invalid route key: " + err.key
}
func NewInvalidRouteKeyError(key string) error {
func NewRouteKeyError(key string) error {
return &InvalidRouteKeyError{key}
}

View file

@ -0,0 +1,95 @@
// Package adapters provides functions to adapt functions to the server.Handler interface.
package adapters
import (
"context"
"net/http"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/pkgs/server"
)
type AdapterFunc[T any, Y any] func(context.Context, T) (Y, error)
type IDFunc[T any, Y any] func(context.Context, uuid.UUID, T) (Y, error)
// Query is a server.Handler that decodes a query from the request and calls the provided function.
func Query[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
q, err := decodeQuery[T](r)
if err != nil {
return err
}
res, err := f(r.Context(), q)
if err != nil {
return err
}
return server.Respond(w, ok, res)
}
}
// QueryID is a server.Handler that decodes a query and an ID from the request and calls the provided function.
func QueryID[T any, Y any](param string, f IDFunc[T, Y], ok int) server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ID, err := routeUUID(r, param)
if err != nil {
return err
}
q, err := decodeQuery[T](r)
if err != nil {
return err
}
res, err := f(r.Context(), ID, q)
if err != nil {
return err
}
return server.Respond(w, ok, res)
}
}
// Action is a function that adapts a function to the server.Handler interface.
// It decodes the request body into a value of type T and passes it to the function f.
// The function f is expected to return a value of type Y and an error.
//
// Note: Action differs from Query in that it decodes the request body.
func Action[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
v, err := decode[T](r)
if err != nil {
return err
}
res, err := f(r.Context(), v)
if err != nil {
return err
}
return server.Respond(w, ok, res)
}
}
// ActionID functions the same as Action, but it also decodes a UUID from the URL path.
func ActionID[T any, Y any](param string, f IDFunc[T, Y], ok int) server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ID, err := routeUUID(r, param)
if err != nil {
return err
}
v, err := decode[T](r)
if err != nil {
return err
}
res, err := f(r.Context(), ID, v)
if err != nil {
return err
}
return server.Respond(w, ok, res)
}
}

View file

@ -0,0 +1,52 @@
package adapters
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/gorilla/schema"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server"
)
var queryDecoder = schema.NewDecoder()
func decodeQuery[T any](r *http.Request) (T, error) {
var v T
err := queryDecoder.Decode(&v, r.URL.Query())
if err != nil {
return v, err
}
err = validate.Check(v)
if err != nil {
return v, err
}
return v, nil
}
func decode[T any](r *http.Request) (T, error) {
var v T
err := server.Decode(r, &v)
if err != nil {
return v, err
}
err = validate.Check(v)
if err != nil {
return v, err
}
return v, nil
}
func routeUUID(r *http.Request, key string) (uuid.UUID, error) {
ID, err := uuid.Parse(chi.URLParam(r, key))
if err != nil {
return uuid.Nil, validate.NewRouteKeyError(key)
}
return ID, nil
}