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))) }) }