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/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..573e6985 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) { @@ -369,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 595c5c0a..b150a0d9 100644 --- a/router.go +++ b/router.go @@ -318,6 +318,8 @@ type Router struct { root *cobra.Command prestart []func() docsHandler Handler + docsDomType string + docsPrefix string corsHandler Handler // Tracks the currently running server for graceful shutdown. @@ -357,6 +359,8 @@ func NewRouter(docs, version string, options ...RouterOption) *Router { engine: g, prestart: []func(){}, docsHandler: RapiDocHandler(title), + docsDomType: "rapi", + docsPrefix: "", corsHandler: cors.Default(), } @@ -378,11 +382,21 @@ 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("/docs", func(c *gin.Context) { - r.docsHandler(c) + 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) { + 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 d5f08cda..51b31a86 100644 --- a/router_test.go +++ b/router_test.go @@ -264,6 +264,24 @@ func TestRouter(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) } +func TestRouterDocsPrefix(t *testing.T) { + + r := NewRouter("api", "v", DocsRoutePrefix("/prefix")) + r.Resource("/hello").Get("doc", func() string { return "Hello" }) + + // 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) + assert.Contains(t, w.Body.String(), "prefix/openapi") +} + func TestRouterRequestBody(t *testing.T) { type EchoRequest struct { Value string `json:"value"`