Skip to content

Commit 206250b

Browse files
authored
Add configurable OpenAPI UI support (Scalar, SwaggerUI, Redoc) (#1)
* Add support for multiple OpenAPI UI types (Scalar, SwaggerUI, Redoc) * Complete implementation and testing of multiple OpenAPI UI support
1 parent f74396f commit 206250b

10 files changed

Lines changed: 275 additions & 5 deletions

File tree

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ resp := responder.NewResponder(
7676
infoHandler := info.NewInfoHandler(
7777
info.WithInfoResponder(resp),
7878
info.WithBaseURL("https://api.example.com"),
79+
info.WithUIType(info.UIScalar), // Choose your preferred OpenAPI UI
7980
info.WithInfoProvider(func() any {
8081
return map[string]string{
8182
"version": version,
@@ -162,8 +163,13 @@ middleware with the built-in defaults.
162163

163164
## Health, Docs & Probes
164165

165-
- **HTML docs**: An embedded Stoplight viewer (`info/assets/stoplight.html`)
166-
serves your OpenAPI spec without extra tooling.
166+
- **HTML docs**: Multiple OpenAPI documentation UIs are supported out of the box:
167+
- **Stoplight Elements** (default): `info/assets/stoplight.html`
168+
- **Scalar**: Modern, interactive API documentation
169+
- **SwaggerUI**: The classic OpenAPI documentation tool
170+
- **Redoc**: Clean, responsive OpenAPI documentation
171+
172+
Use `info.WithUIType()` to select your preferred UI (e.g., `info.WithUIType(info.UIScalar)`).
167173
- **JSON docs**: Provide a `SwaggerProvider` (or `OpenAPIProvider`) to serve the
168174
raw spec alongside the viewer.
169175
- **Readiness/Liveness**: Compose the built-in probes (`probe` package) or pass

go.sum

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,36 @@ github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCc
77
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
88
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
99
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1011
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1112
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
1213
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
1314
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
1415
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
1516
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
1617
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
18+
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
19+
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
20+
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
21+
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
1722
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
1823
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
1924
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
2025
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
26+
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
27+
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
2128
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
2229
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
30+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
31+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
32+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
33+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
2334
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
2435
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
2536
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
2637
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
38+
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
39+
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
2740
github.com/oapi-codegen/nethttp-middleware v1.1.2 h1:TQwEU3WM6ifc7ObBEtiJgbRPaCe513tvJpiMJjypVPA=
2841
github.com/oapi-codegen/nethttp-middleware v1.1.2/go.mod h1:5qzjxMSiI8HjLljiOEjvs4RdrWyMPKnExeFS2kr8om4=
2942
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
@@ -35,26 +48,48 @@ github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNs
3548
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
3649
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
3750
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
51+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
3852
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
53+
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
54+
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
3955
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
4056
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
4157
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
4258
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
4359
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
4460
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
4561
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
62+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
4663
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
4764
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
4865
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
66+
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
67+
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
4968
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
5069
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
70+
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
71+
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
72+
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
73+
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
74+
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
75+
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
76+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
77+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
5178
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
5279
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
5380
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
5481
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
82+
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
83+
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
84+
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
85+
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
5586
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
5687
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
88+
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
89+
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
5790
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
91+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
92+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
5893
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
5994
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
6095
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

info/assets/redoc.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<title>API Doc</title>
7+
</head>
8+
<body>
9+
<redoc spec-url="{{ .BaseURL }}/info/openapi.json"></redoc>
10+
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
11+
</body>
12+
</html>

info/assets/scalar.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<title>API Doc</title>
7+
</head>
8+
<body>
9+
<script
10+
id="api-reference"
11+
data-url="{{ .BaseURL }}/info/openapi.json"
12+
></script>
13+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@latest"></script>
14+
</body>
15+
</html>

info/assets/swaggerui.html

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<title>API Doc</title>
7+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@latest/swagger-ui.css">
8+
</head>
9+
<body>
10+
<div id="swagger-ui"></div>
11+
<script src="https://unpkg.com/swagger-ui-dist@latest/swagger-ui-bundle.js"></script>
12+
<script src="https://unpkg.com/swagger-ui-dist@latest/swagger-ui-standalone-preset.js"></script>
13+
<script>
14+
window.onload = function() {
15+
window.ui = SwaggerUIBundle({
16+
url: "{{ .BaseURL }}/info/openapi.json",
17+
dom_id: '#swagger-ui',
18+
deepLinking: true,
19+
presets: [
20+
SwaggerUIBundle.presets.apis,
21+
SwaggerUIStandalonePreset
22+
],
23+
plugins: [
24+
SwaggerUIBundle.plugins.DownloadUrl
25+
],
26+
layout: "StandaloneLayout"
27+
});
28+
};
29+
</script>
30+
</body>
31+
</html>

info/doc.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11
// Package info exposes build metadata, health probes, and OpenAPI endpoints.
2-
// See ExampleInfoHandler_full for a runnable wiring of the handler and probes.
2+
//
3+
// The package includes support for multiple OpenAPI documentation UIs:
4+
// - Stoplight Elements (default)
5+
// - Scalar
6+
// - SwaggerUI
7+
// - Redoc
8+
//
9+
// Use WithUIType to select your preferred UI when creating an InfoHandler.
10+
//
11+
// See ExampleInfoHandler_full for a runnable wiring of the handler and probes,
12+
// and ExampleInfoHandler_differentUITypes for examples of using different UIs.
313
package info

info/example_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,47 @@ func ExampleInfoHandler_customTemplate() {
6767
// 200
6868
// <div>https://api.example.com/info/openapi.json</div>
6969
}
70+
71+
func ExampleInfoHandler_differentUITypes() {
72+
// Create handlers with different UI types
73+
handlerScalar := info.NewInfoHandler(
74+
info.WithBaseURL("https://api.example.com"),
75+
info.WithUIType(info.UIScalar),
76+
info.WithSwaggerProvider(func() ([]byte, error) {
77+
return []byte(`{"openapi":"3.1.0"}`), nil
78+
}),
79+
)
80+
81+
handlerSwaggerUI := info.NewInfoHandler(
82+
info.WithBaseURL("https://api.example.com"),
83+
info.WithUIType(info.UISwaggerUI),
84+
)
85+
86+
handlerRedoc := info.NewInfoHandler(
87+
info.WithBaseURL("https://api.example.com"),
88+
info.WithUIType(info.UIRedoc),
89+
)
90+
91+
// Test Scalar UI
92+
reqScalar := httptest.NewRequest(http.MethodGet, "/docs", nil)
93+
rrScalar := httptest.NewRecorder()
94+
handlerScalar.GetOpenAPIHTML(rrScalar, reqScalar)
95+
fmt.Println("Scalar:", rrScalar.Code, strings.Contains(rrScalar.Body.String(), "@scalar/api-reference"))
96+
97+
// Test SwaggerUI
98+
reqSwagger := httptest.NewRequest(http.MethodGet, "/docs", nil)
99+
rrSwagger := httptest.NewRecorder()
100+
handlerSwaggerUI.GetOpenAPIHTML(rrSwagger, reqSwagger)
101+
fmt.Println("SwaggerUI:", rrSwagger.Code, strings.Contains(rrSwagger.Body.String(), "swagger-ui-dist"))
102+
103+
// Test Redoc
104+
reqRedoc := httptest.NewRequest(http.MethodGet, "/docs", nil)
105+
rrRedoc := httptest.NewRecorder()
106+
handlerRedoc.GetOpenAPIHTML(rrRedoc, reqRedoc)
107+
fmt.Println("Redoc:", rrRedoc.Code, strings.Contains(rrRedoc.Body.String(), "redoc"))
108+
109+
// Output:
110+
// Scalar: 200 true
111+
// SwaggerUI: 200 true
112+
// Redoc: 200 true
113+
}

info/handler.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type InfoHandler struct {
4747
probeTimeout time.Duration
4848
livenessChecks []ProbeFunc
4949
readinessChecks []ProbeFunc
50+
uiType UIType
5051
}
5152

5253
// NewInfoHandler constructs an InfoHandler with sensible defaults. Callers can
@@ -64,6 +65,7 @@ func NewInfoHandler(opts ...InfoOption) *InfoHandler {
6465
openapiTemplate: defaultOpenAPITemplate,
6566
dataProvider: defaultTemplateDataProvider,
6667
probeTimeout: defaultProbeTimeout,
68+
uiType: UIStoplight,
6769
}
6870
for _, opt := range opts {
6971
if opt != nil {
@@ -157,6 +159,26 @@ func WithReadinessChecks(checks ...ProbeFunc) InfoOption {
157159
}
158160
}
159161

162+
// WithUIType sets the OpenAPI documentation UI to use. Supported values are
163+
// UIStoplight (default), UIScalar, UISwaggerUI, and UIRedoc.
164+
func WithUIType(uiType UIType) InfoOption {
165+
return func(ih *InfoHandler) {
166+
ih.uiType = uiType
167+
switch uiType {
168+
case UIScalar:
169+
ih.openapiTemplate = templateScalar
170+
case UISwaggerUI:
171+
ih.openapiTemplate = templateSwaggerUI
172+
case UIRedoc:
173+
ih.openapiTemplate = templateRedoc
174+
case UIStoplight:
175+
ih.openapiTemplate = templateStoplight
176+
default:
177+
ih.openapiTemplate = templateStoplight
178+
}
179+
}
180+
}
181+
160182
func defaultTemplateDataProvider(_ *http.Request, baseURL string) any {
161183
return map[string]any{
162184
"BaseURL": baseURL,

info/routes_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ func TestInfoHandler_GetOpenAPIHTML(t *testing.T) {
203203
t.Run("falls back to default data provider", testGetOpenAPIHTMLDefaultData)
204204
t.Run("missing template returns problem response", testGetOpenAPIHTMLMissingTemplate)
205205
t.Run("template execution errors are surfaced", testGetOpenAPIHTMLTemplateError)
206+
t.Run("renders different UI types", testGetOpenAPIHTMLDifferentUITypes)
206207
}
207208

208209
func testGetOpenAPIHTMLCustomData(t *testing.T) {
@@ -302,3 +303,61 @@ func testGetOpenAPIHTMLTemplateError(t *testing.T) {
302303
t.Fatalf("expected detail to include render failure, got %q", problem.Detail)
303304
}
304305
}
306+
307+
func testGetOpenAPIHTMLDifferentUITypes(t *testing.T) {
308+
t.Helper()
309+
testCases := []struct {
310+
name string
311+
uiType UIType
312+
expectedInBody string
313+
}{
314+
{
315+
name: "stoplight UI",
316+
uiType: UIStoplight,
317+
expectedInBody: "@stoplight/elements",
318+
},
319+
{
320+
name: "scalar UI",
321+
uiType: UIScalar,
322+
expectedInBody: "@scalar/api-reference",
323+
},
324+
{
325+
name: "swaggerui UI",
326+
uiType: UISwaggerUI,
327+
expectedInBody: "swagger-ui-dist",
328+
},
329+
{
330+
name: "redoc UI",
331+
uiType: UIRedoc,
332+
expectedInBody: "redoc",
333+
},
334+
}
335+
336+
for _, tc := range testCases {
337+
t.Run(tc.name, func(t *testing.T) {
338+
handler := NewInfoHandler(
339+
WithBaseURL("https://api.example.com"),
340+
WithUIType(tc.uiType),
341+
)
342+
req := httptest.NewRequest(http.MethodGet, "/docs", nil)
343+
rr := httptest.NewRecorder()
344+
345+
handler.GetOpenAPIHTML(rr, req)
346+
347+
if rr.Code != http.StatusOK {
348+
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
349+
}
350+
if rr.Header().Get("Content-Type") != "text/html" {
351+
t.Fatalf("expected text/html content type, got %s", rr.Header().Get("Content-Type"))
352+
}
353+
body := rr.Body.String()
354+
if !strings.Contains(body, tc.expectedInBody) {
355+
t.Fatalf("expected body to contain %q, but got: %s", tc.expectedInBody, body)
356+
}
357+
// Verify BaseURL is rendered in all templates (may be escaped in JSON)
358+
if !strings.Contains(body, "https://api.example.com") && !strings.Contains(body, "https:\\/\\/api.example.com") {
359+
t.Fatalf("expected body to contain BaseURL, but got: %s", body)
360+
}
361+
})
362+
}
363+
}

info/template.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,42 @@ import (
88
//go:embed assets/stoplight.html
99
var openapiHTMLStoplight []byte
1010

11-
var defaultOpenAPITemplate = template.Must(
12-
template.New("openapi-stoplight").Parse(string(openapiHTMLStoplight)),
11+
//go:embed assets/scalar.html
12+
var openapiHTMLScalar []byte
13+
14+
//go:embed assets/swaggerui.html
15+
var openapiHTMLSwaggerUI []byte
16+
17+
//go:embed assets/redoc.html
18+
var openapiHTMLRedoc []byte
19+
20+
// UIType specifies which OpenAPI documentation UI to use.
21+
type UIType string
22+
23+
const (
24+
// UIStoplight uses Stoplight Elements for OpenAPI rendering (default).
25+
UIStoplight UIType = "stoplight"
26+
// UIScalar uses Scalar for OpenAPI rendering.
27+
UIScalar UIType = "scalar"
28+
// UISwaggerUI uses SwaggerUI for OpenAPI rendering.
29+
UISwaggerUI UIType = "swaggerui"
30+
// UIRedoc uses Redoc for OpenAPI rendering.
31+
UIRedoc UIType = "redoc"
1332
)
33+
34+
var (
35+
templateStoplight = template.Must(
36+
template.New("openapi-stoplight").Parse(string(openapiHTMLStoplight)),
37+
)
38+
templateScalar = template.Must(
39+
template.New("openapi-scalar").Parse(string(openapiHTMLScalar)),
40+
)
41+
templateSwaggerUI = template.Must(
42+
template.New("openapi-swaggerui").Parse(string(openapiHTMLSwaggerUI)),
43+
)
44+
templateRedoc = template.Must(
45+
template.New("openapi-redoc").Parse(string(openapiHTMLRedoc)),
46+
)
47+
)
48+
49+
var defaultOpenAPITemplate = templateStoplight

0 commit comments

Comments
 (0)