-
Notifications
You must be signed in to change notification settings - Fork 148
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
feat: more options for UI middleware
- refactored UI middleware * factorized chore middleware to remove duplicated code * factorized UI middleware options: to avoid breaking changes in the options types, there is a decode/encode to a common structure * added more options: * allows to fully customize the UI template * added more unit tests - Spec middleware: added support for optional SpecOption argument * allows to serve the spec from a custom path / document name - serving with or without trailing "/" (cf. issue #238) * replaced path.Join() by path.Clean(), which is the intended behavior (i.e. serve the path, irrespective of the presence of a trailing slash) * generalized this behavior to all UI and Spec middleware, not just swaggerUI - API Context: * exposed middleware to serve RapiDoc UI * allowed new UIOption (...UIOption) to the APIHandler, etc middleware * coordinated UI / Spec middleware to be consistent when non-default path/document URL is served * fixes #192 * fixes #226 Signed-off-by: Frederic BIDON <[email protected]>
Showing
15 changed files
with
1,009 additions
and
269 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
package middleware | ||
|
||
import ( | ||
"bytes" | ||
"encoding/gob" | ||
"fmt" | ||
"net/http" | ||
"path" | ||
"strings" | ||
) | ||
|
||
const ( | ||
// constants that are common to all UI-serving middlewares | ||
defaultDocsPath = "docs" | ||
defaultDocsURL = "/swagger.json" | ||
defaultDocsTitle = "API Documentation" | ||
) | ||
|
||
// uiOptions defines common options for UI serving middlewares. | ||
type uiOptions struct { | ||
// BasePath for the UI, defaults to: / | ||
BasePath string | ||
|
||
// Path combines with BasePath to construct the path to the UI, defaults to: "docs". | ||
Path string | ||
|
||
// SpecURL is the URL of the spec document. | ||
// | ||
// Defaults to: /swagger.json | ||
SpecURL string | ||
|
||
// Title for the documentation site, default to: API documentation | ||
Title string | ||
|
||
// Template specifies a custom template to serve the UI | ||
Template string | ||
} | ||
|
||
// toCommonUIOptions converts any UI option type to retain the common options. | ||
// | ||
// This uses gob encoding/decoding to convert common fields from one struct to another. | ||
func toCommonUIOptions(opts interface{}) uiOptions { | ||
var buf bytes.Buffer | ||
enc := gob.NewEncoder(&buf) | ||
dec := gob.NewDecoder(&buf) | ||
var o uiOptions | ||
err := enc.Encode(opts) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
err = dec.Decode(&o) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
return o | ||
} | ||
|
||
func fromCommonToAnyOptions[T any](source uiOptions, target *T) { | ||
var buf bytes.Buffer | ||
enc := gob.NewEncoder(&buf) | ||
dec := gob.NewDecoder(&buf) | ||
err := enc.Encode(source) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
err = dec.Decode(target) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
// UIOption can be applied to UI serving middleware, such as Context.APIHandler or | ||
// Context.APIHandlerSwaggerUI to alter the defaut behavior. | ||
type UIOption func(*uiOptions) | ||
|
||
func uiOptionsWithDefaults(opts []UIOption) uiOptions { | ||
var o uiOptions | ||
for _, apply := range opts { | ||
apply(&o) | ||
} | ||
|
||
return o | ||
} | ||
|
||
// WithUIBasePath sets the base path from where to serve the UI assets. | ||
// | ||
// By default, Context middleware sets this value to the API base path. | ||
func WithUIBasePath(base string) UIOption { | ||
return func(o *uiOptions) { | ||
if !strings.HasPrefix(base, "/") { | ||
base = "/" + base | ||
} | ||
o.BasePath = base | ||
} | ||
} | ||
|
||
// WithUIPath sets the path from where to serve the UI assets (i.e. /{basepath}/{path}. | ||
func WithUIPath(pth string) UIOption { | ||
return func(o *uiOptions) { | ||
o.Path = pth | ||
} | ||
} | ||
|
||
// WithUISpecURL sets the path from where to serve swagger spec document. | ||
// | ||
// This may be specified as a full URL or a path. | ||
// | ||
// By default, this is "/swagger.json" | ||
func WithUISpecURL(specURL string) UIOption { | ||
return func(o *uiOptions) { | ||
o.SpecURL = specURL | ||
} | ||
} | ||
|
||
// WithUITitle sets the title of the UI. | ||
// | ||
// By default, Context middleware sets this value to the title found in the API spec. | ||
func WithUITitle(title string) UIOption { | ||
return func(o *uiOptions) { | ||
o.Title = title | ||
} | ||
} | ||
|
||
// WithTemplate allows to set a custom template for the UI. | ||
// | ||
// UI middleware will panic if the template does not parse or execute properly. | ||
func WithTemplate(tpl string) UIOption { | ||
return func(o *uiOptions) { | ||
o.Template = tpl | ||
} | ||
} | ||
|
||
// EnsureDefaults in case some options are missing | ||
func (r *uiOptions) EnsureDefaults() { | ||
if r.BasePath == "" { | ||
r.BasePath = "/" | ||
} | ||
if r.Path == "" { | ||
r.Path = defaultDocsPath | ||
} | ||
if r.SpecURL == "" { | ||
r.SpecURL = defaultDocsURL | ||
} | ||
if r.Title == "" { | ||
r.Title = defaultDocsTitle | ||
} | ||
} | ||
|
||
// serveUI creates a middleware that serves a templated asset as text/html. | ||
func serveUI(pth string, assets []byte, next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||
if path.Clean(r.URL.Path) == pth { | ||
rw.Header().Set(contentTypeHeader, "text/html; charset=utf-8") | ||
rw.WriteHeader(http.StatusOK) | ||
_, _ = rw.Write(assets) | ||
|
||
return | ||
} | ||
|
||
if next != nil { | ||
next.ServeHTTP(rw, r) | ||
|
||
return | ||
} | ||
|
||
rw.Header().Set(contentTypeHeader, "text/plain") | ||
rw.WriteHeader(http.StatusNotFound) | ||
_, _ = rw.Write([]byte(fmt.Sprintf("%q not found", pth))) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package middleware | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestConvertOptions(t *testing.T) { | ||
t.Run("from any UI options to uiOptions", func(t *testing.T) { | ||
t.Run("from RedocOpts", func(t *testing.T) { | ||
in := RedocOpts{ | ||
BasePath: "a", | ||
Path: "b", | ||
SpecURL: "c", | ||
Template: "d", | ||
Title: "e", | ||
RedocURL: "f", | ||
} | ||
out := toCommonUIOptions(in) | ||
|
||
require.Equal(t, "a", out.BasePath) | ||
require.Equal(t, "b", out.Path) | ||
require.Equal(t, "c", out.SpecURL) | ||
require.Equal(t, "d", out.Template) | ||
require.Equal(t, "e", out.Title) | ||
}) | ||
|
||
t.Run("from RapiDocOpts", func(t *testing.T) { | ||
in := RapiDocOpts{ | ||
BasePath: "a", | ||
Path: "b", | ||
SpecURL: "c", | ||
Template: "d", | ||
Title: "e", | ||
RapiDocURL: "f", | ||
} | ||
out := toCommonUIOptions(in) | ||
|
||
require.Equal(t, "a", out.BasePath) | ||
require.Equal(t, "b", out.Path) | ||
require.Equal(t, "c", out.SpecURL) | ||
require.Equal(t, "d", out.Template) | ||
require.Equal(t, "e", out.Title) | ||
}) | ||
|
||
t.Run("from SwaggerUIOpts", func(t *testing.T) { | ||
in := SwaggerUIOpts{ | ||
BasePath: "a", | ||
Path: "b", | ||
SpecURL: "c", | ||
Template: "d", | ||
Title: "e", | ||
SwaggerURL: "f", | ||
} | ||
out := toCommonUIOptions(in) | ||
|
||
require.Equal(t, "a", out.BasePath) | ||
require.Equal(t, "b", out.Path) | ||
require.Equal(t, "c", out.SpecURL) | ||
require.Equal(t, "d", out.Template) | ||
require.Equal(t, "e", out.Title) | ||
}) | ||
}) | ||
|
||
t.Run("from uiOptions to any UI options", func(t *testing.T) { | ||
in := uiOptions{ | ||
BasePath: "a", | ||
Path: "b", | ||
SpecURL: "c", | ||
Template: "d", | ||
Title: "e", | ||
} | ||
|
||
t.Run("to RedocOpts", func(t *testing.T) { | ||
var out RedocOpts | ||
fromCommonToAnyOptions(in, &out) | ||
require.Equal(t, "a", out.BasePath) | ||
require.Equal(t, "b", out.Path) | ||
require.Equal(t, "c", out.SpecURL) | ||
require.Equal(t, "d", out.Template) | ||
require.Equal(t, "e", out.Title) | ||
}) | ||
|
||
t.Run("to RapiDocOpts", func(t *testing.T) { | ||
var out RapiDocOpts | ||
fromCommonToAnyOptions(in, &out) | ||
require.Equal(t, "a", out.BasePath) | ||
require.Equal(t, "b", out.Path) | ||
require.Equal(t, "c", out.SpecURL) | ||
require.Equal(t, "d", out.Template) | ||
require.Equal(t, "e", out.Title) | ||
}) | ||
|
||
t.Run("to SwaggerUIOpts", func(t *testing.T) { | ||
var out SwaggerUIOpts | ||
fromCommonToAnyOptions(in, &out) | ||
require.Equal(t, "a", out.BasePath) | ||
require.Equal(t, "b", out.Path) | ||
require.Equal(t, "c", out.SpecURL) | ||
require.Equal(t, "d", out.Template) | ||
require.Equal(t, "e", out.Title) | ||
}) | ||
}) | ||
} |