feat(api): expose swagger server metadata
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
e713fb9f56
commit
90600aa434
6 changed files with 64 additions and 3 deletions
3
api.go
3
api.go
|
|
@ -34,6 +34,7 @@ type Engine struct {
|
|||
swaggerTitle string
|
||||
swaggerDesc string
|
||||
swaggerVersion string
|
||||
swaggerServers []string
|
||||
pprofEnabled bool
|
||||
expvarEnabled bool
|
||||
graphql *graphqlConfig
|
||||
|
|
@ -184,7 +185,7 @@ func (e *Engine) build() *gin.Engine {
|
|||
|
||||
// Mount Swagger UI if enabled.
|
||||
if e.swaggerEnabled {
|
||||
registerSwagger(r, e.swaggerTitle, e.swaggerDesc, e.swaggerVersion, e.groups)
|
||||
registerSwagger(r, e.swaggerTitle, e.swaggerDesc, e.swaggerVersion, e.swaggerServers, e.groups)
|
||||
}
|
||||
|
||||
// Mount pprof profiling endpoints if enabled.
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ They execute after `gin.Recovery()` but before any route handler. The `Option` t
|
|||
| `WithWSHandler(h)` | WebSocket at `/ws` | Wraps any `http.Handler` |
|
||||
| `WithAuthentik(cfg)` | Authentik forward-auth + OIDC JWT | Permissive; populates context, never rejects |
|
||||
| `WithSwagger(title, desc, ver)` | Swagger UI at `/swagger/` | Runtime spec via `SpecBuilder` |
|
||||
| `WithSwaggerServers(servers...)` | OpenAPI server metadata | Feeds the runtime Swagger spec and exported docs |
|
||||
| `WithPprof()` | Go profiling at `/debug/pprof/` | WARNING: do not expose in production without authentication |
|
||||
| `WithExpvar()` | Runtime metrics at `/debug/vars` | WARNING: do not expose in production without authentication |
|
||||
| `WithSecure()` | Security headers | HSTS 1 year, X-Frame-Options DENY, nosniff, strict referrer |
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ engine.Register(&Routes{service: svc})
|
|||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `api.go` | `Engine` struct, `New()`, `build()`, `Serve()`, `Handler()`, `Channels()` |
|
||||
| `options.go` | All `With*()` option functions (27 options) |
|
||||
| `options.go` | All `With*()` option functions (28 options) |
|
||||
| `group.go` | `RouteGroup`, `StreamGroup`, `DescribableGroup` interfaces; `RouteDescription` |
|
||||
| `response.go` | `Response[T]`, `Error`, `Meta`, `OK()`, `Fail()`, `FailWithDetails()`, `Paginated()` |
|
||||
| `middleware.go` | `bearerAuthMiddleware()`, `requestIDMiddleware()` |
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@ func WithSwagger(title, description, version string) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithSwaggerServers adds OpenAPI server metadata to the generated Swagger spec.
|
||||
// Empty strings are ignored. Combine it with WithSwagger() to expose the same
|
||||
// server list through both the runtime Swagger UI and exported OpenAPI files.
|
||||
func WithSwaggerServers(servers ...string) Option {
|
||||
return func(e *Engine) {
|
||||
e.swaggerServers = append([]string(nil), servers...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithPprof enables Go runtime profiling endpoints at /debug/pprof/.
|
||||
// The standard pprof handlers (index, cmdline, profile, symbol, trace,
|
||||
// allocs, block, goroutine, heap, mutex, threadcreate) are registered
|
||||
|
|
|
|||
|
|
@ -40,12 +40,13 @@ func (s *swaggerSpec) ReadDoc() string {
|
|||
}
|
||||
|
||||
// registerSwagger mounts the Swagger UI and doc.json endpoint.
|
||||
func registerSwagger(g *gin.Engine, title, description, version string, groups []RouteGroup) {
|
||||
func registerSwagger(g *gin.Engine, title, description, version string, servers []string, groups []RouteGroup) {
|
||||
spec := &swaggerSpec{
|
||||
builder: &SpecBuilder{
|
||||
Title: title,
|
||||
Description: description,
|
||||
Version: version,
|
||||
Servers: servers,
|
||||
},
|
||||
groups: groups,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,6 +258,55 @@ func TestSwagger_Good_InfoFromOptions(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSwagger_Good_UsesServerMetadata(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
e, err := api.New(
|
||||
api.WithSwagger("Server API", "Server metadata test", "1.0.0"),
|
||||
api.WithSwaggerServers("https://api.example.com", "/", ""),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(e.Handler())
|
||||
defer srv.Close()
|
||||
|
||||
resp, err := http.Get(srv.URL + "/swagger/doc.json")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read body: %v", err)
|
||||
}
|
||||
|
||||
var doc map[string]any
|
||||
if err := json.Unmarshal(body, &doc); err != nil {
|
||||
t.Fatalf("invalid JSON: %v", err)
|
||||
}
|
||||
|
||||
servers, ok := doc["servers"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected servers array, got %T", doc["servers"])
|
||||
}
|
||||
if len(servers) != 2 {
|
||||
t.Fatalf("expected 2 servers, got %d", len(servers))
|
||||
}
|
||||
|
||||
first := servers[0].(map[string]any)
|
||||
if first["url"] != "https://api.example.com" {
|
||||
t.Fatalf("expected first server url=%q, got %v", "https://api.example.com", first["url"])
|
||||
}
|
||||
|
||||
second := servers[1].(map[string]any)
|
||||
if second["url"] != "/" {
|
||||
t.Fatalf("expected second server url=%q, got %v", "/", second["url"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwagger_Good_ValidOpenAPI(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue