Skip to content

Commit

Permalink
Introduce v2, add hrt.Handler.Introspect and hrt.Router
Browse files Browse the repository at this point in the history
  • Loading branch information
diamondburned committed Jun 19, 2024
1 parent 9ceeca8 commit ca98dd1
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 28 deletions.
136 changes: 136 additions & 0 deletions chi.go
Original file line number Diff line number Diff line change
@@ -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)
}
33 changes: 16 additions & 17 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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{}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module libdb.so/hrt
module libdb.so/hrt/v2

go 1.18

Expand Down
41 changes: 39 additions & 2 deletions hrt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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),
}
}
7 changes: 3 additions & 4 deletions hrt_example_get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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))

Expand Down
8 changes: 4 additions & 4 deletions hrt_example_post_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions hrt_test.go
Original file line number Diff line number Diff line change
@@ -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"`
}

0 comments on commit ca98dd1

Please sign in to comment.