From ca98dd16a8edafe3eaeefc857e2b0ab123224e60 Mon Sep 17 00:00:00 2001 From: diamondburned Date: Wed, 20 Dec 2023 19:45:20 -0800 Subject: [PATCH] Introduce v2, add hrt.Handler.Introspect and hrt.Router --- chi.go | 136 +++++++++++++++++++++++++++++++++++++++ decoder.go | 33 +++++----- go.mod | 2 +- hrt.go | 41 +++++++++++- hrt_example_get_test.go | 7 +- hrt_example_post_test.go | 8 +-- hrt_test.go | 35 ++++++++++ 7 files changed, 234 insertions(+), 28 deletions(-) create mode 100644 chi.go create mode 100644 hrt_test.go diff --git a/chi.go b/chi.go new file mode 100644 index 0000000..8b46484 --- /dev/null +++ b/chi.go @@ -0,0 +1,136 @@ +package hrt + +import ( + "net/http" + + "github.com/go-chi/chi/v5" +) + +// Router redefines [chi.Router] to modify all method-routing functions to +// accept an [http.Handler] instead of a [http.HandlerFunc]. +type Router interface { + http.Handler + chi.Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern“ string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.Handler) + Delete(pattern string, h http.Handler) + Get(pattern string, h http.Handler) + Head(pattern string, h http.Handler) + Options(pattern string, h http.Handler) + Patch(pattern string, h http.Handler) + Post(pattern string, h http.Handler) + Put(pattern string, h http.Handler) + Trace(pattern string, h http.Handler) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// NewRouter creates a [chi.Router] wrapper that turns all method-routing +// functions to take a regular [http.Handler] instead of an [http.HandlerFunc]. +// This allows [hrt.Wrap] to function properly. This router also has the given +// opts injected into its context, so there is no need to call [hrt.Use]. +func NewRouter(opts Opts) Router { + r := router{chi.NewRouter()} + r.Use(Use(opts)) + return r +} + +// NewPlainRouter is like [NewRouter] but does not inject any options into the +// context. +func NewPlainRouter() Router { + return router{chi.NewRouter()} +} + +// WrapRouter wraps a [chi.Router] to turn all method-routing functions to take +// a regular [http.Handler] instead of an [http.HandlerFunc]. This allows +// [hrt.Wrap] to function properly. +func WrapRouter(r chi.Router) Router { + return router{r} +} + +type router struct{ chi.Router } + +func (r router) With(middlewares ...func(http.Handler) http.Handler) Router { + return router{r.Router.With(middlewares...)} +} + +func (r router) Group(fn func(r Router)) Router { + return router{r.Router.Group(func(r chi.Router) { + fn(router{r}) + })} +} + +func (r router) Route(pattern string, fn func(r Router)) Router { + return router{r.Router.Route(pattern, func(r chi.Router) { + fn(router{r}) + })} +} + +func (r router) Connect(pattern string, h http.Handler) { + r.Router.Method("connect", pattern, h) +} + +func (r router) Delete(pattern string, h http.Handler) { + r.Router.Method("delete", pattern, h) +} + +func (r router) Get(pattern string, h http.Handler) { + r.Router.Method("get", pattern, h) +} + +func (r router) Head(pattern string, h http.Handler) { + r.Router.Method("head", pattern, h) +} + +func (r router) Options(pattern string, h http.Handler) { + r.Router.Method("options", pattern, h) +} + +func (r router) Patch(pattern string, h http.Handler) { + r.Router.Method("patch", pattern, h) +} + +func (r router) Post(pattern string, h http.Handler) { + r.Router.Method("post", pattern, h) +} + +func (r router) Put(pattern string, h http.Handler) { + r.Router.Method("put", pattern, h) +} + +func (r router) Trace(pattern string, h http.Handler) { + r.Router.Method("trace", pattern, h) +} diff --git a/decoder.go b/decoder.go index 8d3fc81..b448d06 100644 --- a/decoder.go +++ b/decoder.go @@ -6,9 +6,9 @@ import ( "reflect" "strings" - "libdb.so/hrt/internal/rfutil" "github.com/go-chi/chi/v5" "github.com/pkg/errors" + "libdb.so/hrt/v2/internal/rfutil" ) // Decoder describes a decoder that decodes the request type. @@ -40,14 +40,14 @@ func (e MethodDecoder) Decode(r *http.Request, v any) error { // // The following tags are supported: // -// - `url` - uses chi.URLParam to decode the value. -// - `form` - uses r.FormValue to decode the value. -// - `query` - similar to `form`. -// - `schema` - similar to `form`, exists for compatibility with gorilla/schema. -// - `json` - uses either chi.URLParam or r.FormValue to decode the value. -// If the value is provided within the form, then it is unmarshaled as JSON -// into the field unless the type is a string. If the value is provided within -// the URL, then it is unmarshaled as a primitive value. +// - `url` - uses chi.URLParam to decode the value. +// - `form` - uses r.FormValue to decode the value. +// - `query` - similar to `form`. +// - `schema` - similar to `form`, exists for compatibility with gorilla/schema. +// - `json` - uses either chi.URLParam or r.FormValue to decode the value. +// If the value is provided within the form, then it is unmarshaled as JSON +// into the field unless the type is a string. If the value is provided within +// the URL, then it is unmarshaled as a primitive value. // // If a struct field has no tag, it is assumed to be the same as the field name. // If a struct field has a tag, then only that tag is used. @@ -56,14 +56,13 @@ func (e MethodDecoder) Decode(r *http.Request, v any) error { // // The following Go type would be decoded to have 2 URL parameters: // -// type Data struct { -// ID string -// Num int `url:"num"` -// Nested struct { -// ID string -// } -// } -// +// type Data struct { +// ID string +// Num int `url:"num"` +// Nested struct { +// ID string +// } +// } var URLDecoder Decoder = urlDecoder{} type urlDecoder struct{} diff --git a/go.mod b/go.mod index b8d301a..49b7ef8 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module libdb.so/hrt +module libdb.so/hrt/v2 go 1.18 diff --git a/hrt.go b/hrt.go index 61e5902..895ec50 100644 --- a/hrt.go +++ b/hrt.go @@ -70,8 +70,8 @@ type Handler[RequestT, ResponseT any] func(ctx context.Context, req RequestT) (R // Wrap wraps a handler into a http.Handler. It exists because Go's type // inference doesn't work well with the Handler type. -func Wrap[RequestT, ResponseT any](f func(ctx context.Context, req RequestT) (ResponseT, error)) http.HandlerFunc { - return Handler[RequestT, ResponseT](f).ServeHTTP +func Wrap[RequestT, ResponseT any](f func(ctx context.Context, req RequestT) (ResponseT, error)) http.Handler { + return Handler[RequestT, ResponseT](f) } // ServeHTTP implements the http.Handler interface. @@ -131,3 +131,40 @@ func decodeRequest[RequestT any](r *http.Request, opts Opts) (RequestT, error) { return *v.(*RequestT), nil } + +// HandlerIntrospection is a struct that contains information about a handler. +// This is primarily used for documentation. +type HandlerIntrospection struct { + // FuncType is the type of the function. + FuncType reflect.Type + // RequestType is the type of the request parameter. + RequestType reflect.Type + // ResponseType is the type of the response parameter. + ResponseType reflect.Type +} + +// TryIntrospectingHandler checks if h is an hrt.Handler and returns its +// introspection if it is, otherwise it returns false. +func TryIntrospectingHandler(h http.Handler) (HandlerIntrospection, bool) { + type introspector interface { + Introspect() HandlerIntrospection + } + var _ introspector = Handler[None, None](nil) + + if h, ok := h.(introspector); ok { + return h.Introspect(), true + } + return HandlerIntrospection{}, false +} + +// Introspect returns information about the handler. +func (h Handler[RequestT, ResponseT]) Introspect() HandlerIntrospection { + var req RequestT + var resp ResponseT + + return HandlerIntrospection{ + FuncType: reflect.TypeOf(h), + RequestType: reflect.TypeOf(req), + ResponseType: reflect.TypeOf(resp), + } +} diff --git a/hrt_example_get_test.go b/hrt_example_get_test.go index 99717ff..35cd0c9 100644 --- a/hrt_example_get_test.go +++ b/hrt_example_get_test.go @@ -6,10 +6,9 @@ import ( "net/url" "strings" - "libdb.so/hrt" - "libdb.so/hrt/internal/ht" - "github.com/go-chi/chi/v5" "github.com/pkg/errors" + "libdb.so/hrt/v2" + "libdb.so/hrt/v2/internal/ht" ) // EchoRequest is a simple request type that echoes the request. @@ -35,7 +34,7 @@ func handleEcho(ctx context.Context, req EchoRequest) (EchoResponse, error) { } func Example_get() { - r := chi.NewRouter() + r := hrt.NewRouter() r.Use(hrt.Use(hrt.DefaultOpts)) r.Get("/echo", hrt.Wrap(handleEcho)) diff --git a/hrt_example_post_test.go b/hrt_example_post_test.go index 7c7dec4..d4b4d1f 100644 --- a/hrt_example_post_test.go +++ b/hrt_example_post_test.go @@ -5,10 +5,10 @@ import ( "fmt" "sync" - "libdb.so/hrt" - "libdb.so/hrt/internal/ht" "github.com/go-chi/chi/v5" "github.com/pkg/errors" + "libdb.so/hrt/v2" + "libdb.so/hrt/v2/internal/ht" ) // User is a simple user type. @@ -77,8 +77,8 @@ func Example_post() { r := chi.NewRouter() r.Use(hrt.Use(hrt.DefaultOpts)) r.Route("/users", func(r chi.Router) { - r.Get("/{id}", hrt.Wrap(handleGetUser)) - r.Post("/", hrt.Wrap(handleCreateUser)) + r.Method("get", "/{id}", hrt.Wrap(handleGetUser)) + r.Method("post", "/", hrt.Wrap(handleCreateUser)) }) srv := ht.NewServer(r) diff --git a/hrt_test.go b/hrt_test.go new file mode 100644 index 0000000..d70a797 --- /dev/null +++ b/hrt_test.go @@ -0,0 +1,35 @@ +package hrt + +import ( + "context" + "strings" + "testing" + + "github.com/pkg/errors" +) + +func TestHandler_Introspect(t *testing.T) { + handler := Wrap(func(ctx context.Context, req echoRequest) (echoResponse, error) { + return echoResponse{What: req.What}, nil + }) + introspection, ok := TryIntrospectingHandler(handler) + if !ok { + t.Fatal("hrt.Handler is not introspectable") + } + t.Log(introspection) +} + +type echoRequest struct { + What string `query:"what"` +} + +func (r echoRequest) Validate() error { + if !strings.HasSuffix(r.What, "!") { + return errors.New("enthusiasm required") + } + return nil +} + +type echoResponse struct { + What string `json:"what"` +}