mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-25 06:00:35 +00:00
Added OpenAPI UI Routes
Co-authored-by: IvanX006 <ivan@bitechsystems.co.za> Co-authored-by: Warkanum <HEIN.PUTH@GMAIL.COM> Co-authored-by: Hein <hein@bitechsystems.co.za>
This commit is contained in:
parent
02fbdbd651
commit
84d673ce14
@ -273,25 +273,151 @@ handler.SetOpenAPIGenerator(func() (string, error) {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using with Swagger UI
|
## Using the Built-in UI Handler
|
||||||
|
|
||||||
You can serve the generated OpenAPI spec with Swagger UI:
|
The package includes a built-in UI handler that serves popular OpenAPI visualization tools. No need to download or manage static files - everything is served from CDN.
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/openapi"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := mux.NewRouter()
|
||||||
|
|
||||||
|
// Setup your API routes and OpenAPI generator...
|
||||||
|
// (see examples above)
|
||||||
|
|
||||||
|
// Add the UI handler - defaults to Swagger UI
|
||||||
|
openapi.SetupUIRoute(router, "/docs", openapi.UIConfig{
|
||||||
|
UIType: openapi.SwaggerUI,
|
||||||
|
SpecURL: "/openapi",
|
||||||
|
Title: "My API Documentation",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Now visit http://localhost:8080/docs
|
||||||
|
http.ListenAndServe(":8080", router)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported UI Frameworks
|
||||||
|
|
||||||
|
The handler supports four popular OpenAPI UI frameworks:
|
||||||
|
|
||||||
|
#### 1. Swagger UI (Default)
|
||||||
|
The most widely used OpenAPI UI with excellent compatibility and features.
|
||||||
|
|
||||||
|
```go
|
||||||
|
openapi.SetupUIRoute(router, "/docs", openapi.UIConfig{
|
||||||
|
UIType: openapi.SwaggerUI,
|
||||||
|
Theme: "dark", // optional: "light" or "dark"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. RapiDoc
|
||||||
|
Modern, customizable, and feature-rich OpenAPI UI.
|
||||||
|
|
||||||
|
```go
|
||||||
|
openapi.SetupUIRoute(router, "/docs", openapi.UIConfig{
|
||||||
|
UIType: openapi.RapiDoc,
|
||||||
|
Theme: "dark",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Redoc
|
||||||
|
Clean, responsive documentation with great UX.
|
||||||
|
|
||||||
|
```go
|
||||||
|
openapi.SetupUIRoute(router, "/docs", openapi.UIConfig{
|
||||||
|
UIType: openapi.Redoc,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Scalar
|
||||||
|
Modern and sleek OpenAPI documentation.
|
||||||
|
|
||||||
|
```go
|
||||||
|
openapi.SetupUIRoute(router, "/docs", openapi.UIConfig{
|
||||||
|
UIType: openapi.Scalar,
|
||||||
|
Theme: "dark",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
```go
|
||||||
|
type UIConfig struct {
|
||||||
|
UIType UIType // SwaggerUI, RapiDoc, Redoc, or Scalar
|
||||||
|
SpecURL string // URL to OpenAPI spec (default: "/openapi")
|
||||||
|
Title string // Page title (default: "API Documentation")
|
||||||
|
FaviconURL string // Custom favicon URL (optional)
|
||||||
|
CustomCSS string // Custom CSS to inject (optional)
|
||||||
|
Theme string // "light" or "dark" (support varies by UI)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Styling Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
openapi.SetupUIRoute(router, "/docs", openapi.UIConfig{
|
||||||
|
UIType: openapi.SwaggerUI,
|
||||||
|
Title: "Acme Corp API",
|
||||||
|
CustomCSS: `
|
||||||
|
.swagger-ui .topbar {
|
||||||
|
background-color: #1976d2;
|
||||||
|
}
|
||||||
|
.swagger-ui .info .title {
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Multiple UIs
|
||||||
|
|
||||||
|
You can serve different UIs at different paths:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Swagger UI at /docs
|
||||||
|
openapi.SetupUIRoute(router, "/docs", openapi.UIConfig{
|
||||||
|
UIType: openapi.SwaggerUI,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Redoc at /redoc
|
||||||
|
openapi.SetupUIRoute(router, "/redoc", openapi.UIConfig{
|
||||||
|
UIType: openapi.Redoc,
|
||||||
|
})
|
||||||
|
|
||||||
|
// RapiDoc at /api-docs
|
||||||
|
openapi.SetupUIRoute(router, "/api-docs", openapi.UIConfig{
|
||||||
|
UIType: openapi.RapiDoc,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Handler Usage
|
||||||
|
|
||||||
|
If you need more control, use the handler directly:
|
||||||
|
|
||||||
|
```go
|
||||||
|
handler := openapi.UIHandler(openapi.UIConfig{
|
||||||
|
UIType: openapi.SwaggerUI,
|
||||||
|
SpecURL: "/api/openapi.json",
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Handle("/documentation", handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using with External Swagger UI
|
||||||
|
|
||||||
|
Alternatively, you can use an external Swagger UI instance:
|
||||||
|
|
||||||
1. Get the spec from `/openapi`
|
1. Get the spec from `/openapi`
|
||||||
2. Load it in Swagger UI at `https://petstore.swagger.io/`
|
2. Load it in Swagger UI at `https://petstore.swagger.io/`
|
||||||
3. Or self-host Swagger UI and point it to your `/openapi` endpoint
|
3. Or self-host Swagger UI and point it to your `/openapi` endpoint
|
||||||
|
|
||||||
Example with self-hosted Swagger UI:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Serve Swagger UI static files
|
|
||||||
router.PathPrefix("/swagger/").Handler(
|
|
||||||
http.StripPrefix("/swagger/", http.FileServer(http.Dir("./swagger-ui"))),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Configure Swagger UI to use /openapi
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
You can test the OpenAPI endpoint:
|
You can test the OpenAPI endpoint:
|
||||||
|
|||||||
@ -183,6 +183,69 @@ func ExampleWithFuncSpec() {
|
|||||||
_ = generatorFunc
|
_ = generatorFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExampleWithUIHandler shows how to serve OpenAPI documentation with a web UI
|
||||||
|
func ExampleWithUIHandler(db *gorm.DB) {
|
||||||
|
// Create handler and configure OpenAPI generator
|
||||||
|
handler := restheadspec.NewHandlerWithGORM(db)
|
||||||
|
registry := modelregistry.NewModelRegistry()
|
||||||
|
|
||||||
|
handler.SetOpenAPIGenerator(func() (string, error) {
|
||||||
|
generator := NewGenerator(GeneratorConfig{
|
||||||
|
Title: "My API",
|
||||||
|
Description: "API documentation with interactive UI",
|
||||||
|
Version: "1.0.0",
|
||||||
|
BaseURL: "http://localhost:8080",
|
||||||
|
Registry: registry,
|
||||||
|
IncludeRestheadSpec: true,
|
||||||
|
})
|
||||||
|
return generator.GenerateJSON()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup routes
|
||||||
|
router := mux.NewRouter()
|
||||||
|
restheadspec.SetupMuxRoutes(router, handler, nil)
|
||||||
|
|
||||||
|
// Add UI handlers for different frameworks
|
||||||
|
// Swagger UI at /docs (most popular)
|
||||||
|
SetupUIRoute(router, "/docs", UIConfig{
|
||||||
|
UIType: SwaggerUI,
|
||||||
|
SpecURL: "/openapi",
|
||||||
|
Title: "My API - Swagger UI",
|
||||||
|
Theme: "light",
|
||||||
|
})
|
||||||
|
|
||||||
|
// RapiDoc at /rapidoc (modern alternative)
|
||||||
|
SetupUIRoute(router, "/rapidoc", UIConfig{
|
||||||
|
UIType: RapiDoc,
|
||||||
|
SpecURL: "/openapi",
|
||||||
|
Title: "My API - RapiDoc",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Redoc at /redoc (clean and responsive)
|
||||||
|
SetupUIRoute(router, "/redoc", UIConfig{
|
||||||
|
UIType: Redoc,
|
||||||
|
SpecURL: "/openapi",
|
||||||
|
Title: "My API - Redoc",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Scalar at /scalar (modern and sleek)
|
||||||
|
SetupUIRoute(router, "/scalar", UIConfig{
|
||||||
|
UIType: Scalar,
|
||||||
|
SpecURL: "/openapi",
|
||||||
|
Title: "My API - Scalar",
|
||||||
|
Theme: "dark",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Now you can access:
|
||||||
|
// http://localhost:8080/docs - Swagger UI
|
||||||
|
// http://localhost:8080/rapidoc - RapiDoc
|
||||||
|
// http://localhost:8080/redoc - Redoc
|
||||||
|
// http://localhost:8080/scalar - Scalar
|
||||||
|
// http://localhost:8080/openapi - Raw OpenAPI JSON
|
||||||
|
|
||||||
|
_ = router
|
||||||
|
}
|
||||||
|
|
||||||
// ExampleCustomization shows advanced customization options
|
// ExampleCustomization shows advanced customization options
|
||||||
func ExampleCustomization() {
|
func ExampleCustomization() {
|
||||||
// Create registry and register models with descriptions using struct tags
|
// Create registry and register models with descriptions using struct tags
|
||||||
|
|||||||
294
pkg/openapi/ui_handler.go
Normal file
294
pkg/openapi/ui_handler.go
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
package openapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UIType represents the type of OpenAPI UI to serve
|
||||||
|
type UIType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SwaggerUI is the most popular OpenAPI UI
|
||||||
|
SwaggerUI UIType = "swagger-ui"
|
||||||
|
// RapiDoc is a modern, customizable OpenAPI UI
|
||||||
|
RapiDoc UIType = "rapidoc"
|
||||||
|
// Redoc is a clean, responsive OpenAPI UI
|
||||||
|
Redoc UIType = "redoc"
|
||||||
|
// Scalar is a modern and sleek OpenAPI UI
|
||||||
|
Scalar UIType = "scalar"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UIConfig holds configuration for the OpenAPI UI handler
|
||||||
|
type UIConfig struct {
|
||||||
|
// UIType specifies which UI framework to use (default: SwaggerUI)
|
||||||
|
UIType UIType
|
||||||
|
// SpecURL is the URL to the OpenAPI spec JSON (default: "/openapi")
|
||||||
|
SpecURL string
|
||||||
|
// Title is the page title (default: "API Documentation")
|
||||||
|
Title string
|
||||||
|
// FaviconURL is the URL to the favicon (optional)
|
||||||
|
FaviconURL string
|
||||||
|
// CustomCSS allows injecting custom CSS (optional)
|
||||||
|
CustomCSS string
|
||||||
|
// Theme for the UI (light/dark, depends on UI type)
|
||||||
|
Theme string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIHandler creates an HTTP handler that serves an OpenAPI UI
|
||||||
|
func UIHandler(config UIConfig) http.HandlerFunc {
|
||||||
|
// Set defaults
|
||||||
|
if config.UIType == "" {
|
||||||
|
config.UIType = SwaggerUI
|
||||||
|
}
|
||||||
|
if config.SpecURL == "" {
|
||||||
|
config.SpecURL = "/openapi"
|
||||||
|
}
|
||||||
|
if config.Title == "" {
|
||||||
|
config.Title = "API Documentation"
|
||||||
|
}
|
||||||
|
if config.Theme == "" {
|
||||||
|
config.Theme = "light"
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var htmlContent string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch config.UIType {
|
||||||
|
case SwaggerUI:
|
||||||
|
htmlContent, err = generateSwaggerUI(config)
|
||||||
|
case RapiDoc:
|
||||||
|
htmlContent, err = generateRapiDoc(config)
|
||||||
|
case Redoc:
|
||||||
|
htmlContent, err = generateRedoc(config)
|
||||||
|
case Scalar:
|
||||||
|
htmlContent, err = generateScalar(config)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Unsupported UI type", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to generate UI: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, err = w.Write([]byte(htmlContent))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to write response: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// templateData wraps UIConfig to properly handle CSS in templates
|
||||||
|
type templateData struct {
|
||||||
|
UIConfig
|
||||||
|
SafeCustomCSS template.CSS
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSwaggerUI generates the HTML for Swagger UI
|
||||||
|
func generateSwaggerUI(config UIConfig) (string, error) {
|
||||||
|
tmpl := `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
{{if .FaviconURL}}<link rel="icon" type="image/png" href="{{.FaviconURL}}">{{end}}
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
|
||||||
|
{{if .SafeCustomCSS}}<style>{{.SafeCustomCSS}}</style>{{end}}
|
||||||
|
<style>
|
||||||
|
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
|
||||||
|
*, *:before, *:after { box-sizing: inherit; }
|
||||||
|
body { margin: 0; padding: 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
const ui = SwaggerUIBundle({
|
||||||
|
url: "{{.SpecURL}}",
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
SwaggerUIStandalonePreset
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
SwaggerUIBundle.plugins.DownloadUrl
|
||||||
|
],
|
||||||
|
layout: "StandaloneLayout",
|
||||||
|
{{if eq .Theme "dark"}}
|
||||||
|
syntaxHighlight: {
|
||||||
|
activate: true,
|
||||||
|
theme: "monokai"
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
});
|
||||||
|
window.ui = ui;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
t, err := template.New("swagger").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := templateData{
|
||||||
|
UIConfig: config,
|
||||||
|
SafeCustomCSS: template.CSS(config.CustomCSS),
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
if err := t.Execute(&buf, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRapiDoc generates the HTML for RapiDoc
|
||||||
|
func generateRapiDoc(config UIConfig) (string, error) {
|
||||||
|
theme := "light"
|
||||||
|
if config.Theme == "dark" {
|
||||||
|
theme = "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
{{if .FaviconURL}}<link rel="icon" type="image/png" href="{{.FaviconURL}}">{{end}}
|
||||||
|
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
|
||||||
|
{{if .SafeCustomCSS}}<style>{{.SafeCustomCSS}}</style>{{end}}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<rapi-doc
|
||||||
|
spec-url="{{.SpecURL}}"
|
||||||
|
theme="` + theme + `"
|
||||||
|
render-style="read"
|
||||||
|
show-header="true"
|
||||||
|
show-info="true"
|
||||||
|
allow-try="true"
|
||||||
|
allow-server-selection="true"
|
||||||
|
allow-authentication="true"
|
||||||
|
api-key-name="Authorization"
|
||||||
|
api-key-location="header"
|
||||||
|
></rapi-doc>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
t, err := template.New("rapidoc").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := templateData{
|
||||||
|
UIConfig: config,
|
||||||
|
SafeCustomCSS: template.CSS(config.CustomCSS),
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
if err := t.Execute(&buf, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRedoc generates the HTML for Redoc
|
||||||
|
func generateRedoc(config UIConfig) (string, error) {
|
||||||
|
tmpl := `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
{{if .FaviconURL}}<link rel="icon" type="image/png" href="{{.FaviconURL}}">{{end}}
|
||||||
|
{{if .SafeCustomCSS}}<style>{{.SafeCustomCSS}}</style>{{end}}
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<redoc spec-url="{{.SpecURL}}" {{if eq .Theme "dark"}}theme='{"colors": {"primary": {"main": "#dd5522"}}}'{{end}}></redoc>
|
||||||
|
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
t, err := template.New("redoc").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := templateData{
|
||||||
|
UIConfig: config,
|
||||||
|
SafeCustomCSS: template.CSS(config.CustomCSS),
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
if err := t.Execute(&buf, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateScalar generates the HTML for Scalar
|
||||||
|
func generateScalar(config UIConfig) (string, error) {
|
||||||
|
tmpl := `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
{{if .FaviconURL}}<link rel="icon" type="image/png" href="{{.FaviconURL}}">{{end}}
|
||||||
|
{{if .SafeCustomCSS}}<style>{{.SafeCustomCSS}}</style>{{end}}
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script id="api-reference" data-url="{{.SpecURL}}" {{if eq .Theme "dark"}}data-theme="dark"{{end}}></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
t, err := template.New("scalar").Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := templateData{
|
||||||
|
UIConfig: config,
|
||||||
|
SafeCustomCSS: template.CSS(config.CustomCSS),
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
if err := t.Execute(&buf, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupUIRoute adds the OpenAPI UI route to a mux router
|
||||||
|
// This is a convenience function for the most common use case
|
||||||
|
func SetupUIRoute(router *mux.Router, path string, config UIConfig) {
|
||||||
|
router.Handle(path, UIHandler(config))
|
||||||
|
}
|
||||||
308
pkg/openapi/ui_handler_test.go
Normal file
308
pkg/openapi/ui_handler_test.go
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
package openapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUIHandler_SwaggerUI(t *testing.T) {
|
||||||
|
config := UIConfig{
|
||||||
|
UIType: SwaggerUI,
|
||||||
|
SpecURL: "/openapi",
|
||||||
|
Title: "Test API Docs",
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := UIHandler(config)
|
||||||
|
req := httptest.NewRequest("GET", "/docs", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
|
||||||
|
// Check for Swagger UI specific content
|
||||||
|
if !strings.Contains(body, "swagger-ui") {
|
||||||
|
t.Error("Expected Swagger UI content")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "SwaggerUIBundle") {
|
||||||
|
t.Error("Expected SwaggerUIBundle script")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, config.Title) {
|
||||||
|
t.Errorf("Expected title '%s' in HTML", config.Title)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, config.SpecURL) {
|
||||||
|
t.Errorf("Expected spec URL '%s' in HTML", config.SpecURL)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "swagger-ui-dist") {
|
||||||
|
t.Error("Expected Swagger UI CDN link")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIHandler_RapiDoc(t *testing.T) {
|
||||||
|
config := UIConfig{
|
||||||
|
UIType: RapiDoc,
|
||||||
|
SpecURL: "/api/spec",
|
||||||
|
Title: "RapiDoc Test",
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := UIHandler(config)
|
||||||
|
req := httptest.NewRequest("GET", "/docs", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
|
||||||
|
// Check for RapiDoc specific content
|
||||||
|
if !strings.Contains(body, "rapi-doc") {
|
||||||
|
t.Error("Expected rapi-doc element")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "rapidoc-min.js") {
|
||||||
|
t.Error("Expected RapiDoc script")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, config.Title) {
|
||||||
|
t.Errorf("Expected title '%s' in HTML", config.Title)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, config.SpecURL) {
|
||||||
|
t.Errorf("Expected spec URL '%s' in HTML", config.SpecURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIHandler_Redoc(t *testing.T) {
|
||||||
|
config := UIConfig{
|
||||||
|
UIType: Redoc,
|
||||||
|
SpecURL: "/spec.json",
|
||||||
|
Title: "Redoc Test",
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := UIHandler(config)
|
||||||
|
req := httptest.NewRequest("GET", "/docs", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
|
||||||
|
// Check for Redoc specific content
|
||||||
|
if !strings.Contains(body, "<redoc") {
|
||||||
|
t.Error("Expected redoc element")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "redoc.standalone.js") {
|
||||||
|
t.Error("Expected Redoc script")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, config.Title) {
|
||||||
|
t.Errorf("Expected title '%s' in HTML", config.Title)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, config.SpecURL) {
|
||||||
|
t.Errorf("Expected spec URL '%s' in HTML", config.SpecURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIHandler_Scalar(t *testing.T) {
|
||||||
|
config := UIConfig{
|
||||||
|
UIType: Scalar,
|
||||||
|
SpecURL: "/openapi.json",
|
||||||
|
Title: "Scalar Test",
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := UIHandler(config)
|
||||||
|
req := httptest.NewRequest("GET", "/docs", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
|
||||||
|
// Check for Scalar specific content
|
||||||
|
if !strings.Contains(body, "api-reference") {
|
||||||
|
t.Error("Expected api-reference element")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "@scalar/api-reference") {
|
||||||
|
t.Error("Expected Scalar script")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, config.Title) {
|
||||||
|
t.Errorf("Expected title '%s' in HTML", config.Title)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, config.SpecURL) {
|
||||||
|
t.Errorf("Expected spec URL '%s' in HTML", config.SpecURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIHandler_DefaultValues(t *testing.T) {
|
||||||
|
// Test with empty config to check defaults
|
||||||
|
config := UIConfig{}
|
||||||
|
|
||||||
|
handler := UIHandler(config)
|
||||||
|
req := httptest.NewRequest("GET", "/docs", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
|
||||||
|
// Should default to Swagger UI
|
||||||
|
if !strings.Contains(body, "swagger-ui") {
|
||||||
|
t.Error("Expected default to Swagger UI")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should default to /openapi spec URL
|
||||||
|
if !strings.Contains(body, "/openapi") {
|
||||||
|
t.Error("Expected default spec URL '/openapi'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should default to "API Documentation" title
|
||||||
|
if !strings.Contains(body, "API Documentation") {
|
||||||
|
t.Error("Expected default title 'API Documentation'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIHandler_CustomCSS(t *testing.T) {
|
||||||
|
customCSS := ".custom-class { color: red; }"
|
||||||
|
config := UIConfig{
|
||||||
|
UIType: SwaggerUI,
|
||||||
|
CustomCSS: customCSS,
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := UIHandler(config)
|
||||||
|
req := httptest.NewRequest("GET", "/docs", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
|
||||||
|
if !strings.Contains(body, customCSS) {
|
||||||
|
t.Errorf("Expected custom CSS to be included. Body:\n%s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIHandler_Favicon(t *testing.T) {
|
||||||
|
faviconURL := "https://example.com/favicon.ico"
|
||||||
|
config := UIConfig{
|
||||||
|
UIType: SwaggerUI,
|
||||||
|
FaviconURL: faviconURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := UIHandler(config)
|
||||||
|
req := httptest.NewRequest("GET", "/docs", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
|
||||||
|
if !strings.Contains(body, faviconURL) {
|
||||||
|
t.Error("Expected favicon URL to be included")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIHandler_DarkTheme(t *testing.T) {
|
||||||
|
config := UIConfig{
|
||||||
|
UIType: SwaggerUI,
|
||||||
|
Theme: "dark",
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := UIHandler(config)
|
||||||
|
req := httptest.NewRequest("GET", "/docs", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
|
||||||
|
// SwaggerUI uses monokai theme for dark mode
|
||||||
|
if !strings.Contains(body, "monokai") {
|
||||||
|
t.Error("Expected dark theme configuration for Swagger UI")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIHandler_InvalidUIType(t *testing.T) {
|
||||||
|
config := UIConfig{
|
||||||
|
UIType: "invalid-ui-type",
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := UIHandler(config)
|
||||||
|
req := httptest.NewRequest("GET", "/docs", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
if resp.StatusCode != http.StatusBadRequest {
|
||||||
|
t.Errorf("Expected status 400 for invalid UI type, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIHandler_ContentType(t *testing.T) {
|
||||||
|
config := UIConfig{
|
||||||
|
UIType: SwaggerUI,
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := UIHandler(config)
|
||||||
|
req := httptest.NewRequest("GET", "/docs", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler(w, req)
|
||||||
|
|
||||||
|
contentType := w.Header().Get("Content-Type")
|
||||||
|
if !strings.Contains(contentType, "text/html") {
|
||||||
|
t.Errorf("Expected Content-Type to contain 'text/html', got '%s'", contentType)
|
||||||
|
}
|
||||||
|
if !strings.Contains(contentType, "charset=utf-8") {
|
||||||
|
t.Errorf("Expected Content-Type to contain 'charset=utf-8', got '%s'", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupUIRoute(t *testing.T) {
|
||||||
|
router := mux.NewRouter()
|
||||||
|
|
||||||
|
config := UIConfig{
|
||||||
|
UIType: SwaggerUI,
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupUIRoute(router, "/api-docs", config)
|
||||||
|
|
||||||
|
// Test that the route was added and works
|
||||||
|
req := httptest.NewRequest("GET", "/api-docs", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it returns HTML
|
||||||
|
body := w.Body.String()
|
||||||
|
if !strings.Contains(body, "swagger-ui") {
|
||||||
|
t.Error("Expected Swagger UI content")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user