From 8ebcc2daa62c8f5eeb4ad423981fc509545e0548 Mon Sep 17 00:00:00 2001 From: David Weiser Date: Fri, 14 Aug 2020 14:49:56 -0700 Subject: [PATCH 1/3] add ability to prefix routes of docs --- go.mod | 1 + options.go | 7 +++++++ router.go | 8 +++++--- router_test.go | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 074084a8..f1b8fcb9 100644 --- a/go.mod +++ b/go.mod @@ -22,4 +22,5 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 go.uber.org/zap v1.10.0 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/yaml.v2 v2.2.8 ) diff --git a/options.go b/options.go index dc4eb69a..a9e0ab34 100644 --- a/options.go +++ b/options.go @@ -301,6 +301,13 @@ func ContactEmail(name, email string) RouterOption { }} } +// DocsRoutePrefix enables the API documentation to be available from `prefix/{docs, openapi.yaml}` +func DocsRoutePrefix(prefix string) RouterOption { + return &routerOption{func(r *Router) { + r.docsPrefix = prefix + }} +} + // BasicAuth adds a named HTTP Basic Auth security scheme. func BasicAuth(name string) RouterOption { return &routerOption{func(r *Router) { diff --git a/router.go b/router.go index 595c5c0a..2e156914 100644 --- a/router.go +++ b/router.go @@ -318,6 +318,7 @@ type Router struct { root *cobra.Command prestart []func() docsHandler Handler + docsPrefix string corsHandler Handler // Tracks the currently running server for graceful shutdown. @@ -357,6 +358,7 @@ func NewRouter(docs, version string, options ...RouterOption) *Router { engine: g, prestart: []func(){}, docsHandler: RapiDocHandler(title), + docsPrefix: "", corsHandler: cors.Default(), } @@ -378,10 +380,10 @@ func NewRouter(docs, version string, options ...RouterOption) *Router { } // Set up handlers for the auto-generated spec and docs. - r.engine.GET("/openapi.json", openAPIHandlerJSON(r)) - r.engine.GET("/openapi.yaml", openAPIHandlerYAML(r)) + r.engine.GET(fmt.Sprintf("%s/openapi.json", r.docsPrefix), openAPIHandlerJSON(r)) + r.engine.GET(fmt.Sprintf("%s/openapi.yaml", r.docsPrefix), openAPIHandlerYAML(r)) - r.engine.GET("/docs", func(c *gin.Context) { + r.engine.GET(fmt.Sprintf("%s/docs", r.docsPrefix), func(c *gin.Context) { r.docsHandler(c) }) diff --git a/router_test.go b/router_test.go index d5f08cda..b4fc77c5 100644 --- a/router_test.go +++ b/router_test.go @@ -264,6 +264,43 @@ func TestRouter(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) } +func TestRouterDocsPrefix(t *testing.T) { + type EchoResponse struct { + Value string `json:"value" description:"The echoed back word"` + } + + r := NewTestRouter(t, DocsRoutePrefix("/prefix")) + + r.Resource("/echo", + PathParam("word", "The word to echo back"), + QueryParam("greet", "Return a greeting", false), + ResponseJSON(http.StatusOK, "Successful echo response"), + ResponseError(http.StatusBadRequest, "Invalid input"), + ).Put("Echo back an input word.", func(word string, greet bool) (*EchoResponse, *ErrorModel) { + if word == "test" { + return nil, &ErrorModel{Detail: "Value not allowed: test"} + } + + v := word + if greet { + v = "Hello, " + word + } + + return &EchoResponse{Value: v}, nil + }) + + // Check spec & docs routes + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/prefix/openapi.json", nil) + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + + w = httptest.NewRecorder() + req, _ = http.NewRequest(http.MethodGet, "/prefix/docs", nil) + r.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) +} + func TestRouterRequestBody(t *testing.T) { type EchoRequest struct { Value string `json:"value"` From e7a339cd2151c279791d797ba45d9b8d2d9be278 Mon Sep 17 00:00:00 2001 From: David Weiser Date: Mon, 17 Aug 2020 14:25:11 -0700 Subject: [PATCH 2/3] ensure the dom contains the prefixed path to the openapi file --- router_test.go | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/router_test.go b/router_test.go index b4fc77c5..8679c5ea 100644 --- a/router_test.go +++ b/router_test.go @@ -265,29 +265,9 @@ func TestRouter(t *testing.T) { } func TestRouterDocsPrefix(t *testing.T) { - type EchoResponse struct { - Value string `json:"value" description:"The echoed back word"` - } - - r := NewTestRouter(t, DocsRoutePrefix("/prefix")) - - r.Resource("/echo", - PathParam("word", "The word to echo back"), - QueryParam("greet", "Return a greeting", false), - ResponseJSON(http.StatusOK, "Successful echo response"), - ResponseError(http.StatusBadRequest, "Invalid input"), - ).Put("Echo back an input word.", func(word string, greet bool) (*EchoResponse, *ErrorModel) { - if word == "test" { - return nil, &ErrorModel{Detail: "Value not allowed: test"} - } - v := word - if greet { - v = "Hello, " + word - } - - return &EchoResponse{Value: v}, nil - }) + r := NewRouter("api", "v", DocsRoutePrefix("/prefix")) + r.Resource("/hello").Get("doc", func() string { return "Hello" }) // Check spec & docs routes w := httptest.NewRecorder() @@ -299,6 +279,7 @@ func TestRouterDocsPrefix(t *testing.T) { req, _ = http.NewRequest(http.MethodGet, "/prefix/docs", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, "prefix/openapi", w.Body.String()) } func TestRouterRequestBody(t *testing.T) { From c252e4d86c60e0e2587802820e704fad08635e62 Mon Sep 17 00:00:00 2001 From: David Weiser Date: Tue, 18 Aug 2020 11:16:27 -0700 Subject: [PATCH 3/3] ensure that docs ui renders with correct path to openapi --- docs.go | 56 ++++++++++++++++++++++++++++++-------------------- options.go | 11 ++++++++++ router.go | 16 +++++++++++++-- router_test.go | 2 +- 4 files changed, 60 insertions(+), 25 deletions(-) diff --git a/docs.go b/docs.go index 737d7296..f3327322 100644 --- a/docs.go +++ b/docs.go @@ -19,10 +19,10 @@ func splitDocs(docs string) (title, desc string) { return } -// RapiDocHandler renders documentation using RapiDoc. -func RapiDocHandler(pageTitle string) Handler { - return func(c *gin.Context) { - c.Data(200, "text/html", []byte(fmt.Sprintf(` +// RapiDocTemplate is the template used to generate the RapiDoc. It needs two args to render: +// 1. the title +// 2. the path to the openapi.yaml file +var RapiDocTemplate = ` %s @@ -31,21 +31,19 @@ func RapiDocHandler(pageTitle string) Handler { -`, pageTitle))) - } -} +` -// ReDocHandler renders documentation using ReDoc. -func ReDocHandler(pageTitle string) Handler { - return func(c *gin.Context) { - c.Data(200, "text/html", []byte(fmt.Sprintf(` +// ReDocTemplate is the template used to generate the RapiDoc. It needs two args to render: +// 1. the title +// 2. the path to the openapi.yaml file +var ReDocTemplate = ` %s @@ -55,17 +53,12 @@ func ReDocHandler(pageTitle string) Handler { - + -`, pageTitle))) - } -} +` -// SwaggerUIHandler renders documentation using Swagger UI. -func SwaggerUIHandler(pageTitle string) Handler { - return func(c *gin.Context) { - c.Data(200, "text/html", []byte(fmt.Sprintf(` +var SwaggerUITemplate = ` @@ -104,7 +97,7 @@ func SwaggerUIHandler(pageTitle string) Handler { window.onload = function() { // Begin Swagger UI call region const ui = SwaggerUIBundle({ - url: "/openapi.json", + url: "%s", dom_id: '#swagger-ui', deepLinking: true, presets: [ @@ -122,6 +115,25 @@ func SwaggerUIHandler(pageTitle string) Handler { } -`, pageTitle))) +` + +// RapiDocHandler renders documentation using RapiDoc. +func RapiDocHandler(pageTitle string) Handler { + return func(c *gin.Context) { + c.Data(200, "text/html", []byte(fmt.Sprintf(RapiDocTemplate, pageTitle, "/openapi.json"))) + } +} + +// ReDocHandler renders documentation using ReDoc. +func ReDocHandler(pageTitle string) Handler { + return func(c *gin.Context) { + c.Data(200, "text/html", []byte(fmt.Sprintf(ReDocTemplate, pageTitle, "/openapi.json"))) + } +} + +// SwaggerUIHandler renders documentation using Swagger UI. +func SwaggerUIHandler(pageTitle string) Handler { + return func(c *gin.Context) { + c.Data(200, "text/html", []byte(fmt.Sprintf(SwaggerUITemplate, pageTitle, "/openapi.json"))) } } diff --git a/options.go b/options.go index a9e0ab34..573e6985 100644 --- a/options.go +++ b/options.go @@ -376,12 +376,23 @@ func HTTPServer(server *http.Server) RouterOption { // DocsHandler sets the documentation rendering handler function. You can // use `huma.RapiDocHandler`, `huma.ReDocHandler`, `huma.SwaggerUIHandler`, or // provide your own (e.g. with custom auth or branding). +// +// DEPRECATED! Use `DocsDomType` instead! func DocsHandler(f Handler) RouterOption { + fmt.Println("This option is deprecated, use `DocsDomType` instead") return &routerOption{func(r *Router) { r.docsHandler = f }} } +// DocsDomType sets the presentation for the docs UI. Valid values are: +// "rapi" (default), "redoc", or "swagger" +func DocsDomType(t string) RouterOption { + return &routerOption{func(r *Router) { + r.docsDomType = t + }} +} + // CORSHandler sets the CORS handler function. This can be used to set custom // domains, headers, auth, etc. If not given, then a default CORS handler is // used instead. diff --git a/router.go b/router.go index 2e156914..b150a0d9 100644 --- a/router.go +++ b/router.go @@ -318,6 +318,7 @@ type Router struct { root *cobra.Command prestart []func() docsHandler Handler + docsDomType string docsPrefix string corsHandler Handler @@ -358,6 +359,7 @@ func NewRouter(docs, version string, options ...RouterOption) *Router { engine: g, prestart: []func(){}, docsHandler: RapiDocHandler(title), + docsDomType: "rapi", docsPrefix: "", corsHandler: cors.Default(), } @@ -380,11 +382,21 @@ func NewRouter(docs, version string, options ...RouterOption) *Router { } // Set up handlers for the auto-generated spec and docs. - r.engine.GET(fmt.Sprintf("%s/openapi.json", r.docsPrefix), openAPIHandlerJSON(r)) + openapiJsonPath := fmt.Sprintf("%s/openapi.json", r.docsPrefix) + r.engine.GET(openapiJsonPath, openAPIHandlerJSON(r)) r.engine.GET(fmt.Sprintf("%s/openapi.yaml", r.docsPrefix), openAPIHandlerYAML(r)) r.engine.GET(fmt.Sprintf("%s/docs", r.docsPrefix), func(c *gin.Context) { - r.docsHandler(c) + docsPayload := "" + switch r.docsDomType { + case "rapi": + docsPayload = fmt.Sprintf(RapiDocTemplate, title, openapiJsonPath) + case "swagger": + docsPayload = fmt.Sprintf(SwaggerUITemplate, title, openapiJsonPath) + case "redoc": + docsPayload = fmt.Sprintf(ReDocTemplate, title, openapiJsonPath) + } + c.Data(200, "text/html", []byte(docsPayload)) }) // If downloads like a CLI or SDKs are available, serve them automatically diff --git a/router_test.go b/router_test.go index 8679c5ea..51b31a86 100644 --- a/router_test.go +++ b/router_test.go @@ -279,7 +279,7 @@ func TestRouterDocsPrefix(t *testing.T) { req, _ = http.NewRequest(http.MethodGet, "/prefix/docs", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) - assert.Contains(t, "prefix/openapi", w.Body.String()) + assert.Contains(t, w.Body.String(), "prefix/openapi") } func TestRouterRequestBody(t *testing.T) {