Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Contributing

Thanks for your interest in contributing to spec-ui.

## Development Setup

1. Fork and clone the repository.
2. Ensure you have Go installed (the project uses Go modules).
3. Install dependencies:

```bash
go mod tidy
```

## Run Checks Locally

Before opening a pull request, run:

```bash
go test ./...
```

If you have golangci-lint installed:

```bash
golangci-lint run
```

## Embedded Assets Notes

For library users, embedded UI assets are already included in this module.

For maintainers only: if you intentionally update bundled provider assets (CSS/JS/favicon files), regenerate them with:

```bash
make download-assets
```

Then run tests again:

```bash
go test ./...
```

## Pull Requests

- Keep changes focused and scoped to one concern when possible.
- Add or update tests for behavior changes.
- Update documentation (README or this file) when behavior/config changes.
- Use clear commit messages.

## Issues and Discussions

For major changes, please open an issue first so we can discuss approach and compatibility.

## Code of Conduct

Please be respectful and constructive in all interactions.
32 changes: 31 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Variables
PKG := ./...
COVERAGE_FILE := coverage.out
SWAGGERUI_VER := 5.32.1
STOPLIGHT_VER := 9.0.16
REDOC_VER := 2.5.2
SCALAR_VER := 1.51.0
RAPIDOC_VER := 9.3.8
CDN := https://cdn.jsdelivr.net/npm

# Default target
.PHONY: all
Expand Down Expand Up @@ -59,4 +65,28 @@ clean:
.PHONY: update
update:
@echo "Updating dependencies..."
@go get -u ./...
@go get -u ./...

.PHONY: download-assets
download-assets:
@mkdir -p internal/swaggeruiemb/assets
@curl -fsSL $(CDN)/swagger-ui@$(SWAGGERUI_VER)/dist/swagger-ui.min.css -o internal/swaggeruiemb/assets/swagger-ui.min.css
@curl -fsSL $(CDN)/swagger-ui@$(SWAGGERUI_VER)/dist/swagger-ui-bundle.js -o internal/swaggeruiemb/assets/swagger-ui-bundle.js
@curl -fsSL $(CDN)/swagger-ui@$(SWAGGERUI_VER)/dist/swagger-ui-standalone-preset.js -o internal/swaggeruiemb/assets/swagger-ui-standalone-preset.js
@curl -fsSL https://petstore.swagger.io/favicon-16x16.png -o internal/swaggeruiemb/assets/favicon-16x16.png
@curl -fsSL https://petstore.swagger.io/favicon-32x32.png -o internal/swaggeruiemb/assets/favicon-32x32.png
@mkdir -p internal/stoplightelementsemb/assets
@curl -fsSL $(CDN)/@stoplight/elements@$(STOPLIGHT_VER)/styles.min.css -o internal/stoplightelementsemb/assets/styles.min.css
@curl -fsSL $(CDN)/@stoplight/elements@$(STOPLIGHT_VER)/web-components.min.js -o internal/stoplightelementsemb/assets/web-components.min.js
@mkdir -p internal/stoplightelementsemb/assets/favicons
@curl -fsSL https://docs.stoplight.io/favicons/favicon.ico -o internal/stoplightelementsemb/assets/favicons/favicon.ico
@mkdir -p internal/redocemb/assets
@curl -fsSL $(CDN)/redoc@$(REDOC_VER)/bundles/redoc.standalone.js -o internal/redocemb/assets/redoc.standalone.js
@mkdir -p internal/scalaremb/assets/browser
@curl -fsSL $(CDN)/@scalar/api-reference@$(SCALAR_VER)/dist/style.min.css -o internal/scalaremb/assets/style.min.css
@curl -fsSL $(CDN)/@scalar/api-reference@$(SCALAR_VER)/dist/browser/standalone.min.js -o internal/scalaremb/assets/browser/standalone.min.js
@curl -fsSL https://scalar.com/favicon.png -o internal/scalaremb/assets/favicon.png
@mkdir -p internal/rapidocemb/assets
@curl -fsSL $(CDN)/rapidoc@$(RAPIDOC_VER)/dist/rapidoc-min.js -o internal/rapidocemb/assets/rapidoc-min.js
@mkdir -p internal/rapidocemb/assets/images
@curl -fsSL https://rapidocweb.com/images/logo.png -o internal/rapidocemb/assets/images/logo.png
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A Go library that provides multiple OpenAPI documentation UIs. Serve beautiful,
- ⚡ **Easy Integration**: Simple HTTP handler integration with Go's standard library
- 🎨 **Customizable**: Configure titles, base paths, and OpenAPI spec locations
- 🔧 **Flexible**: Works with any Go HTTP router or framework
- 📦 **Optional Embedded UI Assets**: Enable local embedded assets for self-contained binaries in air-gapped environments

## Installation

Expand Down Expand Up @@ -135,6 +136,40 @@ The handler provides convenient methods for integration:
- `handler.Spec()` - Returns HTTP handler for the OpenAPI specification
- `handler.SpecFunc()` - Returns the HTTP handler function for serving the OpenAPI specification
- `handler.SpecPath()` - Returns the OpenAPI spec path (e.g., `/docs/openapi.yaml`)
- `handler.AssetsEnabled()` - Returns `true` when UI assets are served from embedded files
- `handler.AssetsPath()` - Returns the assets URL prefix (default: `/docs/_assets`)
- `handler.Assets()` - Returns the embedded assets handler (or `nil` in CDN mode)

## Embedded Assets (Optional)

By default, UI CSS/JS assets are loaded from CDN.

If you need offline or air-gapped usage, enable embed mode at runtime.

No extra download step is required for library users; embedded assets are already included in this module.

Register docs/spec as usual, then conditionally register the assets route:

```go
handler := specui.NewHandler(
specui.WithEmbedAssets(),
specui.WithSwaggerUI(),
specui.WithSpecFile("openapi.yaml"),
// specui.WithAssetsPath("/custom/assets"), // optional override
)

r.Get(handler.DocsPath(), handler.DocsFunc())
r.Get(handler.SpecPath(), handler.SpecFunc())

if handler.AssetsEnabled() {
r.Get(handler.AssetsPath()+"/*", handler.Assets().ServeHTTP)
}
```

Notes:

- CDN mode (default): assets are loaded from provider CDN URLs.
- Embed mode (`specui.WithEmbedAssets()`): docs pages reference local asset URLs under `handler.AssetsPath()`.

## Basic Usage

Expand Down Expand Up @@ -179,6 +214,8 @@ The library uses functional options for flexible configuration:
| `WithSpecEmbedFS` | Set spec file location with embedded filesystem | `specui.WithSpecEmbedFS("openapi.yaml", embedFS)` |
| `WithSpecIOFS` | Set spec file location with OS filesystem | `specui.WithSpecIOFS("openapi.yaml", os.DirFS("docs"))` |
| `WithCacheAge` | Set cache age for the documentation | `specui.WithCacheAge(3600)` |
| `WithEmbedAssets` | Enable serving UI assets from embedded files | `specui.WithEmbedAssets()` |
| `WithAssetsPath` | Set URL prefix for embedded assets (embed mode) | `specui.WithAssetsPath("/docs/_assets")` |

### UI Selection with Configuration

Expand Down Expand Up @@ -319,7 +356,7 @@ Check out the [`examples`](/examples) directory for more examples.

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
Contributions are welcome. Please see [CONTRIBUTING.md](/CONTRIBUTING.md) for setup, checks, and pull request guidelines.

## License

Expand Down
6 changes: 6 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type SpecUI struct {
SpecIOFS fs.FS // Filesystem for the OpenAPI specification
SpecEmbedFS *embed.FS // Embedded file system for the OpenAPI specification
SpecGenerator SpecGenerator // OpenAPI specification generator
AssetsPath string // Path to embedded assets, defaults to "/docs/_assets"
EmbedAssets bool // True when local UI assets are served from embedded files

Provider Provider // Provider type
SwaggerUI *SwaggerUI // Swagger UI configuration
Expand All @@ -44,6 +46,10 @@ type SpecUI struct {
// provider's handler is created at request time. Only the referenced
// provider's code is linked into the binary, enabling tree-shaking.
DocsHandlerFactory func(*SpecUI) http.Handler

// AssetsHandlerFactory is set by With<Provider> options only when embedded
// assets are enabled for the selected provider.
AssetsHandlerFactory func(*SpecUI) http.Handler
}

type SwaggerLayout string
Expand Down
3 changes: 3 additions & 0 deletions examples/chi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ func main() {

r.Get(handler.DocsPath(), handler.DocsFunc())
r.Get(handler.SpecPath(), handler.SpecFunc())
if handler.AssetsEnabled() {
r.Handle(handler.AssetsPath()+"/*", handler.Assets())
}

log.Printf("OpenAPI Documentation available at http://localhost:3000/docs")
log.Printf("OpenAPI YAML available at http://localhost:3000/docs/openapi.yaml")
Expand Down
3 changes: 3 additions & 0 deletions examples/echo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ func main() {

e.GET(handler.DocsPath(), echo.WrapHandler(handler.Docs()))
e.GET(handler.SpecPath(), echo.WrapHandler(handler.Spec()))
if handler.AssetsEnabled() {
e.GET(handler.AssetsPath()+"/*", echo.WrapHandler(handler.Assets()))
}

log.Printf("OpenAPI Documentation available at http://localhost:3000/docs")
log.Printf("OpenAPI YAML available at http://localhost:3000/docs/openapi.yaml")
Expand Down
3 changes: 3 additions & 0 deletions examples/fiber/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ func main() {

app.Get(handler.DocsPath(), adaptor.HTTPHandler(handler.Docs()))
app.Get(handler.SpecPath(), adaptor.HTTPHandler(handler.Spec()))
if handler.AssetsEnabled() {
app.Get(handler.AssetsPath()+"/*", adaptor.HTTPHandler(handler.Assets()))
}

log.Printf("OpenAPI Documentation available at http://localhost:3000/docs")
log.Printf("OpenAPI YAML available at http://localhost:3000/docs/openapi.yaml")
Expand Down
3 changes: 3 additions & 0 deletions examples/gin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ func main() {

r.GET(handler.DocsPath(), gin.WrapH(handler.Docs()))
r.GET(handler.SpecPath(), gin.WrapH(handler.Spec()))
if handler.AssetsEnabled() {
r.GET(handler.AssetsPath()+"/*filepath", gin.WrapH(handler.Assets()))
}

log.Printf("OpenAPI Documentation available at http://localhost:3000/docs")
log.Printf("OpenAPI YAML available at http://localhost:3000/docs/openapi.yaml")
Expand Down
3 changes: 3 additions & 0 deletions examples/http/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ func main() {

mux.Handle("GET "+handler.DocsPath(), handler.Docs())
mux.Handle("GET "+handler.SpecPath(), handler.Spec())
if handler.AssetsEnabled() {
mux.Handle("GET "+handler.AssetsPath()+"/", handler.Assets())
}

log.Printf("OpenAPI Documentation available at http://localhost:3000/docs")
log.Printf("OpenAPI YAML available at http://localhost:3000/docs/openapi.yaml")
Expand Down
5 changes: 4 additions & 1 deletion examples/mux/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ func main() {
specui.WithStoplightElements(),
)

mux.Handle(handler.DocsPath(), handler.Spec()).Methods("GET")
mux.Handle(handler.DocsPath(), handler.Docs()).Methods("GET")
mux.Handle(handler.SpecPath(), handler.Spec()).Methods("GET")
if handler.AssetsEnabled() {
mux.PathPrefix(handler.AssetsPath() + "/").Handler(handler.Assets()).Methods("GET")
}

log.Printf("OpenAPI Documentation available at http://localhost:3000/docs")
log.Printf("OpenAPI YAML available at http://localhost:3000/docs/openapi.yaml")
Expand Down
24 changes: 24 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type Handler struct {
cfg *config.SpecUI
docsOnce sync.Once
docsHandler http.Handler
assetsOnce sync.Once
assets http.Handler
}

// DocsPath returns the path to the API documentation.
Expand All @@ -35,6 +37,16 @@ func (h *Handler) SpecPath() string {
return h.cfg.SpecPath
}

// AssetsEnabled returns true when embedded assets are enabled.
func (h *Handler) AssetsEnabled() bool {
return h.cfg.EmbedAssets
}

// AssetsPath returns the URL prefix used for embedded assets.
func (h *Handler) AssetsPath() string {
return h.cfg.AssetsPath
}

// Docs returns the HTTP handler for the API documentation.
// The handler is created once and cached for subsequent calls.
func (h *Handler) Docs() http.Handler {
Expand All @@ -47,6 +59,18 @@ func (h *Handler) Docs() http.Handler {
return h.docsHandler
}

// Assets returns the HTTP handler for embedded UI assets.
// Returns nil when running in CDN mode.
func (h *Handler) Assets() http.Handler {
if h.cfg.AssetsHandlerFactory == nil {
return nil
}
h.assetsOnce.Do(func() {
h.assets = h.cfg.AssetsHandlerFactory(h.cfg)
})
return h.assets
}

// DocsFunc returns the HTTP handler function for the API documentation.
func (h *Handler) DocsFunc() http.HandlerFunc {
return h.Docs().ServeHTTP
Expand Down
52 changes: 52 additions & 0 deletions handler_assets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package specui_test

import (
"net/http"
"net/http/httptest"
"testing"

specui "github.com/oaswrap/spec-ui"
"github.com/stretchr/testify/assert"
)

func TestHandlerAssetsInCDNMode(t *testing.T) {
h := specui.NewHandler(
specui.WithSwaggerUI(),
specui.WithSpecFile("testdata/petstore.yaml"),
)

assert.False(t, h.AssetsEnabled())
assert.Equal(t, "/docs/_assets", h.AssetsPath())
assert.Nil(t, h.Assets())
}

func TestHandlerAssetsInEmbedMode(t *testing.T) {
h := specui.NewHandler(
specui.WithEmbedAssets(),
specui.WithSwaggerUI(),
specui.WithSpecFile("testdata/petstore.yaml"),
)

assert.True(t, h.AssetsEnabled())
assert.Equal(t, "/docs/_assets", h.AssetsPath())
assert.NotNil(t, h.Assets())

req := httptest.NewRequest(http.MethodGet, "/docs/_assets/swagger-ui.min.css", nil)
rec := httptest.NewRecorder()
h.Assets().ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)
assert.NotEmpty(t, rec.Body.String())
}

func TestWithAssetsPathInCDNMode(t *testing.T) {
h := specui.NewHandler(
specui.WithSwaggerUI(),
specui.WithAssetsPath("/custom/assets"),
specui.WithSpecFile("testdata/petstore.yaml"),
)

assert.Equal(t, "/custom/assets", h.AssetsPath())
assert.False(t, h.AssetsEnabled())
assert.Nil(t, h.Assets())
}
Loading
Loading