diff --git a/docs/doc-site/usage/server/deprecated-shims.md b/docs/doc-site/usage/server/deprecated-shims.md index 3089df57..8cfb3f85 100644 --- a/docs/doc-site/usage/server/deprecated-shims.md +++ b/docs/doc-site/usage/server/deprecated-shims.md @@ -73,6 +73,7 @@ Field-to-option mapping for the `*Opts` structs: | `Title` | `WithUITitle(s)` | | `Template` | `WithUITemplate(s)` | | `RapiDocURL` / `RedocURL` / `SwaggerURL` | `WithUIAssetsURL(s)` | +| `RedocV2` | `WithRedocV2()` | | Swagger-UI-specific knobs (`OAuthCallbackURL`, presets, favicons) | `WithSwaggerUIOptions(docui.SwaggerUIOptions{…})` | Migration example: diff --git a/docs/doc-site/usage/standalone/doc-ui.md b/docs/doc-site/usage/standalone/doc-ui.md index 0c96d91c..8ac1f621 100644 --- a/docs/doc-site/usage/standalone/doc-ui.md +++ b/docs/doc-site/usage/standalone/doc-ui.md @@ -55,7 +55,7 @@ your OAuth provider. | `WithUIBasePath(string)` | Base path the UI is served from. Slash is prepended if missing. | `/` | | `WithUIPath(string)` | Sub-path under the base path (final URL: `{base}/{path}`). | `docs` | | `WithUITitle(string)` | HTML `` of the rendered page. | `API documentation` | -| `WithUIAssetsURL(string)` | URL of the JS bundle for the UI. | Redoc → `https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js`<br/>RapiDoc → `https://unpkg.com/rapidoc/dist/rapidoc-min.js`<br/>Swagger UI → `https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js` | +| `WithUIAssetsURL(string)` | URL of the JS bundle for the UI. | Redoc → `https://cdn.jsdelivr.net/npm/redoc@3/bundles/redoc.standalone.js`<br/>RapiDoc → `https://unpkg.com/rapidoc/dist/rapidoc-min.js`<br/>Swagger UI → `https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js` | | `WithUITemplate(tpl)` | Replace the bundled HTML template entirely (`~string` or `~[]byte`). | bundled minimal template | | `WithSpecURL(string)` | URL the UI fetches the spec from. | `/swagger.json` | | `WithSwaggerUIOptions(opts)` | Pass-through for Swagger-UI-specific knobs (OAuth2 client id, layout, …). | zero value | @@ -68,6 +68,14 @@ templates each UI accepts: - RapiDoc: <https://github.com/rapi-doc/RapiDoc> - Swagger UI: <https://github.com/swagger-api/swagger-ui> +### Redoc v2 + +`docui.Redoc` loads Redoc v3, which ships as an ES module: the default +template uses `<script type="module">`. The Redoc v2 bundle does not load +as a module. If you pin a Redoc v2 bundle with `WithUIAssetsURL`, add +`WithRedocV2()`: it switches to a classic `<script>` template and, if you +set no URL, to `https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js`. + ## Serving the spec document — `ServeSpec` / `UseSpec` The UIs only render — they do not host the spec document themselves. diff --git a/middleware/seam.go b/middleware/seam.go index b234395f..571953b6 100644 --- a/middleware/seam.go +++ b/middleware/seam.go @@ -289,12 +289,17 @@ type RedocOpts struct { // RedocURL points to the js that generates the redoc site. // - // Defaults to: https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js + // Defaults to: https://cdn.jsdelivr.net/npm/redoc@3/bundles/redoc.standalone.js RedocURL string + + // RedocV2 serves Redoc v2 instead of v3. Set it if RedocURL points to a Redoc v2 bundle. + // + // See [docui.WithRedocV2]. + RedocV2 bool } func (o RedocOpts) toFuncOptions() []docui.Option { - const structMembers = 6 + const structMembers = 7 opts := make([]docui.Option, 0, structMembers) if o.BasePath != "" { @@ -321,6 +326,10 @@ func (o RedocOpts) toFuncOptions() []docui.Option { opts = append(opts, docui.WithUIAssetsURL(o.RedocURL)) } + if o.RedocV2 { + opts = append(opts, docui.WithRedocV2()) + } + return opts } diff --git a/middleware/seam_test.go b/middleware/seam_test.go index e124758b..0178554d 100644 --- a/middleware/seam_test.go +++ b/middleware/seam_test.go @@ -44,6 +44,17 @@ func TestDeprecatedDocUIForwarders(t *testing.T) { assert.EqualT(t, http.StatusOK, rec.Code) }) + t.Run("middleware.Redoc serves Redoc v2 with RedocV2", func(t *testing.T) { + h := middleware.Redoc(middleware.RedocOpts{RedocV2: true}, nil) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) + require.NoError(t, err) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + assert.EqualT(t, http.StatusOK, rec.Code) + assert.StringContainsT(t, rec.Body.String(), "/redoc@2/") + assert.StringNotContainsT(t, rec.Body.String(), `type="module"`) + }) + t.Run("middleware.RapiDoc still serves the docs page", func(t *testing.T) { h := middleware.RapiDoc(middleware.RapiDocOpts{}, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) diff --git a/server-middleware/docui/options.go b/server-middleware/docui/options.go index c9e45f77..9734ef36 100644 --- a/server-middleware/docui/options.go +++ b/server-middleware/docui/options.go @@ -92,6 +92,9 @@ type ( // AssetsURL points to the js asset that generates the documentation page. AssetsURL string + + // redocV2 selects the Redoc v2 template and assets. + redocV2 bool } specOptions struct { @@ -138,7 +141,8 @@ func WithUITitle(title string) Option { // // Defaults: // -// - for Redoc: https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js +// - for Redoc: https://cdn.jsdelivr.net/npm/redoc@3/bundles/redoc.standalone.js +// (https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js with [WithRedocV2]) // - for RapiDoc, this defaults to: https://unpkg.com/rapidoc/dist/rapidoc-min.js // - for SwaggerUI: https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js func WithUIAssetsURL(assets string) Option { @@ -184,6 +188,28 @@ func WithSwaggerUIOptions(opts SwaggerUIOptions) Option { } } +//////////////////////////////////////////////////////////// +// Redoc UI options +//////////////////////////////////////////////////////////// + +// WithRedocV2 makes [Redoc] and [UseRedoc] serve Redoc v2 instead of v3. +// +// The default template loads the Redoc v3 bundle as an ES module (<script type="module">). +// The Redoc v2 bundle does not load as a module. +// +// With this option, the default template loads the bundle as a classic script and +// the default assets URL points to Redoc v2. Use this option if you set +// [WithUIAssetsURL] to a Redoc v2 bundle. +// +// [WithUITemplate] and [WithUIAssetsURL] still override these defaults. +// +// Other UIs ignore this option. +func WithRedocV2() Option { + return func(o *options) { + o.redocV2 = true + } +} + //////////////////////////////////////////////////////////// // Spec options //////////////////////////////////////////////////////////// diff --git a/server-middleware/docui/redoc.go b/server-middleware/docui/redoc.go index 31054a24..00df5c33 100644 --- a/server-middleware/docui/redoc.go +++ b/server-middleware/docui/redoc.go @@ -13,6 +13,8 @@ import ( // UseRedoc creates a middleware to serve a documentation site for a swagger spec using [Redoc]. // +// By default, the page loads Redoc v3. Use [WithRedocV2] to keep Redoc v2. +// // [Redoc]: https://redocly.com/docs/redoc func UseRedoc(opts ...Option) func(next http.Handler) http.Handler { pth, assets := redocSetup(opts) @@ -28,6 +30,8 @@ func UseRedoc(opts ...Option) func(next http.Handler) http.Handler { // // This allows for altering the spec before starting the [http] listener. // +// By default, the page loads Redoc v3. Use [WithRedocV2] to keep Redoc v2. +// // [Redoc]: https://redocly.com/docs/redoc func Redoc(next http.Handler, opts ...Option) http.Handler { pth, assets := redocSetup(opts) @@ -36,11 +40,21 @@ func Redoc(next http.Handler, opts ...Option) http.Handler { } func redocSetup(opts []Option) (pth string, assets []byte) { - o := optionsWithDefaults(opts, - // defaults for redoc - WithUITemplate(redocTemplate), - WithUIAssetsURL(redocLatest), - ) + o := optionsWithDefaults(opts) + + // defaults for redoc, resolved after all options so that WithRedocV2 applies in any order + if o.Template == "" { + o.Template = redocTemplate + if o.redocV2 { + o.Template = redocV2Template + } + } + if o.AssetsURL == "" { + o.AssetsURL = redocV3URL + if o.redocV2 { + o.AssetsURL = redocV2URL + } + } pth = path.Join(o.BasePath, o.Path) tmpl := template.Must(template.New("redoc").Parse(o.Template)) @@ -53,7 +67,10 @@ func redocSetup(opts []Option) (pth string, assets []byte) { } const ( - redocLatest = "https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js" // "https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js" + redocV3URL = "https://cdn.jsdelivr.net/npm/redoc@3/bundles/redoc.standalone.js" + redocV2URL = "https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js" + + // redocTemplate loads the Redoc v3 bundle, which is an ES module. redocTemplate = `<!DOCTYPE html> <html> <head> @@ -73,6 +90,36 @@ const ( } </style> </head> + <body> + <redoc spec-url='{{ .SpecURL }}'></redoc> + <script type="module" src="{{ .AssetsURL }}"> </script> + </body> +</html> +` + + // redocV2Template loads the Redoc v2 bundle, which is a classic UMD script. + // + // The v2 bundle fails when loaded as a module: its wrapper reads properties of the + // top-level "this", which is undefined in a module. + redocV2Template = `<!DOCTYPE html> +<html> + <head> + <title>{{ .Title }} + + + + + + + + diff --git a/server-middleware/docui/redoc_test.go b/server-middleware/docui/redoc_test.go index 65ab489b..f8e9e5f0 100644 --- a/server-middleware/docui/redoc_test.go +++ b/server-middleware/docui/redoc_test.go @@ -28,7 +28,48 @@ func TestRedocMiddleware(t *testing.T) { body := recorder.Body.String() assert.StringContainsT(t, body, fmt.Sprintf("%s", defaultDocsTitle)) assert.StringContainsT(t, body, fmt.Sprintf("", defaultDocsURL)) - assert.StringContainsT(t, body, redocLatest) + assert.StringContainsT(t, body, fmt.Sprintf(`` + + h := Redoc(nil, WithRedocV2(), WithUITemplate(tpl)) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/docs", nil) + require.NoError(t, err) + recorder := httptest.NewRecorder() + + h.ServeHTTP(recorder, req) + assert.EqualT(t, http.StatusOK, recorder.Code) + assert.StringContainsT(t, recorder.Body.String(), fmt.Sprintf(`