diff --git a/backend/go.mod b/backend/go.mod index b6cc3d7..b55bc56 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -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 diff --git a/backend/go.sum b/backend/go.sum index 25b17e4..dc4f3d6 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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= diff --git a/backend/internal/sys/validate/errors.go b/backend/internal/sys/validate/errors.go index b5c101a..2338785 100644 --- a/backend/internal/sys/validate/errors.go +++ b/backend/internal/sys/validate/errors.go @@ -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} } diff --git a/backend/internal/web/adapters/adapters.go b/backend/internal/web/adapters/adapters.go new file mode 100644 index 0000000..bd9e30c --- /dev/null +++ b/backend/internal/web/adapters/adapters.go @@ -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) + } +} diff --git a/backend/internal/web/adapters/decoders.go b/backend/internal/web/adapters/decoders.go new file mode 100644 index 0000000..c88fc21 --- /dev/null +++ b/backend/internal/web/adapters/decoders.go @@ -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 +}