From 857003abffe34607694cdd8dbce2ba55f8c793c1 Mon Sep 17 00:00:00 2001 From: Logan Garrett Date: Wed, 1 Jun 2022 11:32:37 -0700 Subject: [PATCH 1/4] chore: make the path for static openapi routes configurable --- router.go | 67 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/router.go b/router.go index 9532e7d3..f6fc411d 100644 --- a/router.go +++ b/router.go @@ -49,10 +49,13 @@ type Router struct { autoConfig *AutoConfig - // Documentation handler function - docsPrefix string - docsHandler http.Handler - docsAreSetup bool + // Documentation, OpenAPI spec, and schemas routing handlers + docsPrefix string + docsSuffix string + schemasSuffix string + specSuffix string + docsHandler http.Handler + docsAreSetup bool // Tracks the currently running server for graceful shutdown. server *http.Server @@ -215,17 +218,48 @@ func (r *Router) Middleware(middlewares ...func(next http.Handler) http.Handler) r.mux.Use(middlewares...) } +// DocsPath returns the server path to the OpenAPI docs. +func (r *Router) DocsPath() string { + return fmt.Sprintf("%s/%s", r.docsPrefix, r.docsSuffix) +} + +// SchemasPath returns the server path to the OpenAPI Schemas. +func (r *Router) SchemasPath() string { + return fmt.Sprintf("%s/%s", r.docsPrefix, r.schemasSuffix) +} + // OpenAPIPath returns the server path to the OpenAPI JSON. func (r *Router) OpenAPIPath() string { - return r.docsPrefix + "/openapi.json" + return fmt.Sprintf("%s/%s.json", r.docsPrefix, r.specSuffix) } -// DocsPrefix sets the path prefix for where the OpenAPI JSON and documentation -// are hosted. +// DocsPrefix sets the path prefix for where the OpenAPI JSON, schemas, +// and documentation are hosted. func (r *Router) DocsPrefix(path string) { r.docsPrefix = path } +// DocsSuffix sets the final path suffix for where the OpenAPI documentation +// is hosted. When not specified, the default value of `docs` is appended to the +// DocsPrefix. +func (r *Router) DocsSuffix(suffix string) { + r.docsSuffix = suffix +} + +// SchemasSuffix sets the final path suffix for where the OpenAPI schemas +// are hosted. When not specified, the default value of `schemas` is appended +// to the DocsPrefix. +func (r *Router) SchemasSuffix(suffix string) { + r.specSuffix = suffix +} + +// SpecSuffix sets the final path suffix for where the OpenAPI spec is hosted. +// When not specified, the default value of `openapi` is appended to the +// DocsPrefix. +func (r *Router) SpecSuffix(suffix string) { + r.specSuffix = suffix +} + // DocsHandler sets the http.Handler to render documentation. It defaults to // using RapiDoc. func (r *Router) DocsHandler(handler http.Handler) { @@ -281,8 +315,8 @@ func (r *Router) setupDocs() { }) } - if !r.mux.Match(chi.NewRouteContext(), http.MethodGet, r.docsPrefix+"/schemas/{schema-id}.json") { - r.mux.Get(r.docsPrefix+"/schemas/{schema-id}.json", func(w http.ResponseWriter, req *http.Request) { + if !r.mux.Match(chi.NewRouteContext(), http.MethodGet, r.SchemasPath()+"/{schema-id}.json") { + r.mux.Get(r.SchemasPath()+"/{schema-id}.json", func(w http.ResponseWriter, req *http.Request) { id := chi.URLParam(req, "schema-id") schema := schemas[id] if schema == nil { @@ -295,8 +329,8 @@ func (r *Router) setupDocs() { }) } - if !r.mux.Match(chi.NewRouteContext(), http.MethodGet, r.docsPrefix+"/docs") { - r.mux.Get(r.docsPrefix+"/docs", r.docsHandler.ServeHTTP) + if !r.mux.Match(chi.NewRouteContext(), http.MethodGet, r.DocsPath()) { + r.mux.Get(r.DocsPath(), r.docsHandler.ServeHTTP) } r.docsAreSetup = true @@ -411,6 +445,12 @@ func (r *Router) DisableSchemaProperty() { r.disableSchemaProperty = true } +const ( + DefaultDocsSuffix = "docs" + DefaultSchemasSuffix = "schemas" + DefaultSpecSuffix = "openapi" +) + // New creates a new Huma router to which you can attach resources, // operations, middleware, etc. func New(docs, version string) *Router { @@ -427,6 +467,9 @@ func New(docs, version string) *Router { security: []map[string][]string{}, defaultBodyReadTimeout: 15 * time.Second, defaultServerIdleTimeout: 15 * time.Second, + docsSuffix: DefaultDocsSuffix, + schemasSuffix: DefaultSchemasSuffix, + specSuffix: DefaultSpecSuffix, } r.docsHandler = RapiDocHandler(r) @@ -456,7 +499,7 @@ func New(docs, version string) *Router { if link != "" { link += ", " } - link += `<` + r.OpenAPIPath() + `>; rel="service-desc", <` + r.docsPrefix + `/docs>; rel="service-doc"` + link += `<` + r.OpenAPIPath() + `>; rel="service-desc", <` + r.DocsPath() + `>; rel="service-doc"` w.Header().Set("link", link) } }) From 1cde5a821e59a6a91a01c0a7086d7b8a131ed531 Mon Sep 17 00:00:00 2001 From: Logan Garrett Date: Wed, 1 Jun 2022 11:34:50 -0700 Subject: [PATCH 2/4] fix: bad copy/paste --- router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router.go b/router.go index f6fc411d..59401b3c 100644 --- a/router.go +++ b/router.go @@ -250,7 +250,7 @@ func (r *Router) DocsSuffix(suffix string) { // are hosted. When not specified, the default value of `schemas` is appended // to the DocsPrefix. func (r *Router) SchemasSuffix(suffix string) { - r.specSuffix = suffix + r.schemasSuffix = suffix } // SpecSuffix sets the final path suffix for where the OpenAPI spec is hosted. From f35018cc996792fcf596e248925ac18d66519be3 Mon Sep 17 00:00:00 2001 From: Logan Garrett Date: Wed, 1 Jun 2022 13:39:26 -0700 Subject: [PATCH 3/4] chore: update README with new configuration options --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 48a1b258..bad440f3 100644 --- a/README.md +++ b/README.md @@ -878,13 +878,21 @@ app.DocsHandler(huma.ReDocHandler(app.Router)) ## OpenAPI -By default, the generated OpenAPI and autogenerated documentation are served in the root at `/openapi.json` and `/docs` respectively. This default path can be modified: +By default, the generated OpenAPI spec, schemas, and autogenerated documentation are served in the root at `/openapi.json`, `/schemas`, and `/docs` respectively. The default prefix for all, and the suffix for each individual route can be modified: ```go -// Serve `/public/openapi.json and `/public/docs`: +// Serve `/public/openapi.json`, `/public/schemas`, and `/public/docs`: app.DocsPrefix("/public") ``` +```go +// Serve `/internal/app/myService/model/openapi.json`, `/internal/app/myService/model/schemas`, and `/internal/app/myService/documentation`: +app.DocsPrefix("/internal/app/myService") +app.DocsSuffix("documentation") +app.SchemasSuffix("model/schemas") +app.SpecSuffix("model/openapi") +``` + ### Custom OpenAPI Fields Use the OpenAPI hook for OpenAPI customization. It gives you a `*gabs.Container` instance that represents the root of the OpenAPI document. @@ -902,7 +910,7 @@ app.OpenAPIHook(modify) ### JSON Schema -Each resource operation also returns a `describedby` HTTP link relation which references a JSON-Schema file. These schemas re-use the `DocsPrefix` described above and default to the server root. For example: +Each resource operation also returns a `describedby` HTTP link relation which references a JSON-Schema file. These schemas re-use the `DocsPrefix` and `SchemasSuffix` described above and default to the server root. For example: ```http Link: ; rel="describedby" From 7c1f1d9c1a7e27f3ea2e44814e07241e0100c667 Mon Sep 17 00:00:00 2001 From: Logan Garrett Date: Wed, 1 Jun 2022 15:06:24 -0700 Subject: [PATCH 4/4] fix: use configured static routes in links --- context.go | 8 +++++--- operation.go | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/context.go b/context.go index 224c618d..62806624 100644 --- a/context.go +++ b/context.go @@ -81,7 +81,9 @@ type hcontext struct { errorCode int op *Operation closed bool - docsPrefix string + docsPath string + schemasPath string + specPath string urlPrefix string disableSchemaProperty bool } @@ -310,7 +312,7 @@ func (c *hcontext) writeModel(ct string, status int, model interface{}) { if link != "" { link += ", " } - link += "<" + c.docsPrefix + "/schemas/" + id + ".json>; rel=\"describedby\"" + link += "<" + c.schemasPath + "/" + id + ".json>; rel=\"describedby\"" c.Header().Set("Link", link) if modelType.Kind() == reflect.Ptr { @@ -320,7 +322,7 @@ func (c *hcontext) writeModel(ct string, status int, model interface{}) { tmp := map[string]interface{}{} shallowStructToMap(reflect.ValueOf(model), tmp) if tmp["$schema"] == nil { - tmp["$schema"] = c.URLPrefix() + c.docsPrefix + "/schemas/" + id + ".json" + tmp["$schema"] = c.URLPrefix() + c.schemasPath + "/" + id + ".json" } model = tmp } diff --git a/operation.go b/operation.go index f41e2d03..eda6601b 100644 --- a/operation.go +++ b/operation.go @@ -276,7 +276,9 @@ func (o *Operation) Run(handler interface{}) { ResponseWriter: w, r: r, op: o, - docsPrefix: o.resource.router.docsPrefix, + docsPath: o.resource.router.DocsPath(), + schemasPath: o.resource.router.SchemasPath(), + specPath: o.resource.router.OpenAPIPath(), urlPrefix: o.resource.router.urlPrefix, disableSchemaProperty: o.resource.router.disableSchemaProperty, errorCode: http.StatusBadRequest,