From 41574540d8f81b428ede080faa76df0d69d83fea Mon Sep 17 00:00:00 2001 From: Michael Tiller Date: Thu, 1 Dec 2022 16:06:56 -0500 Subject: [PATCH 1/3] feat: ability to fetch router from huma context This allows us to query the router for information. I've added a GetOperation method so we can query for information about a given operation. This is useful in hypermedia APIs because it allows us to link across different endpoints without having to hardwire paths. Instead, the cross-referencing is done via operation id. --- router.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++- router_test.go | 9 +++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/router.go b/router.go index 3295bab9..020b74b0 100644 --- a/router.go +++ b/router.go @@ -25,6 +25,9 @@ var connContextKey contextKey = "huma-request-conn" // has finished. var opIDContextKey contextKey = "huma-operation-id" +// routerContextKey is used to get the router associated with the API +var routerContextKey contextKey = "huma-router" + // GetConn gets the underlying `net.Conn` from a context. func GetConn(ctx context.Context) net.Conn { conn := ctx.Value(connContextKey) @@ -34,6 +37,15 @@ func GetConn(ctx context.Context) net.Conn { return nil } +// GetRouter gets the `*Router` handling API requests +func GetRouter(ctx context.Context) *Router { + router := ctx.Value(routerContextKey) + if router != nil { + return router.(*Router) + } + return nil +} + // Router is the entrypoint to your API. type Router struct { mux *chi.Mux @@ -243,6 +255,43 @@ func (r *Router) Resource(path string) *Resource { return res } +// GetOperation returns the path and +func (r *Router) GetOperation(id string) *OperationInfo { + // Loop over all router resources looking for the specified operation + for _, res := range r.resources { + result := getOperation(id, res) + if result != nil { + return result + } + } + return nil +} + +func getOperation(id string, res *Resource) *OperationInfo { + // First, search in this resource + for _, op := range res.operations { + if op.id == id { + return &OperationInfo{ + ID: op.id, + URITemplate: op.resource.path, + Summary: op.summary, + Tags: append([]string{}, op.resource.tags...), + } + } + } + // If we still haven't found anything, look in subresources + if res.subResources != nil { + for _, sub := range res.subResources { + result := getOperation(id, sub) + if result != nil { + return result + } + } + } + // If we get here, nothing in this part of the tree + return nil +} + // Middleware adds a new standard middleware to this router at the root, // so it will apply to all requests. Middleware can also be applied at the // resource level. @@ -532,7 +581,10 @@ func New(docs, version string) *Router { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { // Inject the operation info before other middleware so that the later // middleware will have access to it. - req = req.WithContext(context.WithValue(req.Context(), opIDContextKey, &OperationInfo{})) + reqContext := req.Context() + withOpenID := context.WithValue(reqContext, opIDContextKey, &OperationInfo{}) + withRouter := context.WithValue(withOpenID, routerContextKey, r) + req = req.WithContext(withRouter) next.ServeHTTP(w, req) diff --git a/router_test.go b/router_test.go index e23f90a9..836d4803 100644 --- a/router_test.go +++ b/router_test.go @@ -64,6 +64,15 @@ func TestStreamingInput(t *testing.T) { ctx.WriteHeader(http.StatusNoContent) }) + stream := r.GetOperation("stream") + assert.NotNil(t, stream) + assert.Equal(t, *stream, OperationInfo{ + ID: "stream", + URITemplate: "/stream", + Summary: "Stream test", + Tags: []string{}, + }) + w := httptest.NewRecorder() body := bytes.NewReader(make([]byte, 1024)) req, _ := http.NewRequest(http.MethodPost, "/stream", body) From 58fcaa506fbf4e39ea83cf3b2e5a9e8b5886bdff Mon Sep 17 00:00:00 2001 From: Michael M Tiller <46495651+mt35-rs@users.noreply.github.com> Date: Fri, 2 Dec 2022 14:45:30 -0500 Subject: [PATCH 2/3] Update router.go Co-authored-by: Daniel G. Taylor --- router.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/router.go b/router.go index 020b74b0..875fd0ff 100644 --- a/router.go +++ b/router.go @@ -582,8 +582,8 @@ func New(docs, version string) *Router { // Inject the operation info before other middleware so that the later // middleware will have access to it. reqContext := req.Context() - withOpenID := context.WithValue(reqContext, opIDContextKey, &OperationInfo{}) - withRouter := context.WithValue(withOpenID, routerContextKey, r) + withOpID := context.WithValue(reqContext, opIDContextKey, &OperationInfo{}) + withRouter := context.WithValue(withOpID, routerContextKey, r) req = req.WithContext(withRouter) next.ServeHTTP(w, req) From 26405ef6270739c8356d65ee22dcbfbf9ffe69ca Mon Sep 17 00:00:00 2001 From: Michael Tiller Date: Fri, 2 Dec 2022 14:47:32 -0500 Subject: [PATCH 3/3] docs: complete documentation for GetOperation --- router.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/router.go b/router.go index 875fd0ff..33953ea2 100644 --- a/router.go +++ b/router.go @@ -255,7 +255,9 @@ func (r *Router) Resource(path string) *Resource { return res } -// GetOperation returns the path and +// GetOperation returns an `OperationInfo` struct for the operation named by the +// `id` argument. The `OperationInfo` struct provides the URL template and a +// summary of the operation along with any tags associated with the operation. func (r *Router) GetOperation(id string) *OperationInfo { // Loop over all router resources looking for the specified operation for _, res := range r.resources {