diff --git a/api.go b/api.go index 5e58dce..63a092c 100644 --- a/api.go +++ b/api.go @@ -25,19 +25,21 @@ const shutdownTimeout = 10 * time.Second // Engine is the central API server managing route groups and middleware. type Engine struct { - addr string - groups []RouteGroup - middlewares []gin.HandlerFunc - wsHandler http.Handler - sseBroker *SSEBroker - swaggerEnabled bool - swaggerTitle string - swaggerDesc string - swaggerVersion string - swaggerServers []string - pprofEnabled bool - expvarEnabled bool - graphql *graphqlConfig + addr string + groups []RouteGroup + middlewares []gin.HandlerFunc + wsHandler http.Handler + sseBroker *SSEBroker + swaggerEnabled bool + swaggerTitle string + swaggerDesc string + swaggerVersion string + swaggerServers []string + swaggerLicenseName string + swaggerLicenseURL string + pprofEnabled bool + expvarEnabled bool + graphql *graphqlConfig } // New creates an Engine with the given options. @@ -185,7 +187,16 @@ func (e *Engine) build() *gin.Engine { // Mount Swagger UI if enabled. if e.swaggerEnabled { - registerSwagger(r, e.swaggerTitle, e.swaggerDesc, e.swaggerVersion, e.swaggerServers, e.groups) + registerSwagger( + r, + e.swaggerTitle, + e.swaggerDesc, + e.swaggerVersion, + e.swaggerServers, + e.swaggerLicenseName, + e.swaggerLicenseURL, + e.groups, + ) } // Mount pprof profiling endpoints if enabled. diff --git a/docs/architecture.md b/docs/architecture.md index 7fd1ca5..f15df25 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -162,6 +162,7 @@ They execute after `gin.Recovery()` but before any route handler. The `Option` t | `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 | +| `WithSwaggerLicense(name, url)` | OpenAPI licence metadata | Populates the Swagger spec info block without manual `SpecBuilder` wiring | | `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 | diff --git a/docs/index.md b/docs/index.md index 9f9179e..d4292c3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -44,6 +44,7 @@ func main() { api.WithSecure(), api.WithSlog(nil), api.WithSwagger("My API", "A service description", "1.0.0"), + api.WithSwaggerLicense("EUPL-1.2", "https://eupl.eu/1.2/en/"), ) engine.Register(myRoutes) // any RouteGroup implementation diff --git a/options.go b/options.go index bb973a3..0205947 100644 --- a/options.go +++ b/options.go @@ -138,6 +138,19 @@ func WithSwaggerServers(servers ...string) Option { } } +// WithSwaggerLicense adds licence metadata to the generated Swagger spec. +// Pass both a name and URL to populate the OpenAPI info block consistently. +// +// Example: +// +// api.WithSwaggerLicense("EUPL-1.2", "https://eupl.eu/1.2/en/") +func WithSwaggerLicense(name, url string) Option { + return func(e *Engine) { + e.swaggerLicenseName = name + e.swaggerLicenseURL = url + } +} + // 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 diff --git a/swagger.go b/swagger.go index b69fa93..d913c41 100644 --- a/swagger.go +++ b/swagger.go @@ -40,13 +40,15 @@ func (s *swaggerSpec) ReadDoc() string { } // registerSwagger mounts the Swagger UI and doc.json endpoint. -func registerSwagger(g *gin.Engine, title, description, version string, servers []string, groups []RouteGroup) { +func registerSwagger(g *gin.Engine, title, description, version string, servers []string, licenseName, licenseURL string, groups []RouteGroup) { spec := &swaggerSpec{ builder: &SpecBuilder{ Title: title, Description: description, Version: version, Servers: servers, + LicenseName: licenseName, + LicenseURL: licenseURL, }, groups: groups, } diff --git a/swagger_test.go b/swagger_test.go index 70bfe66..9d512ad 100644 --- a/swagger_test.go +++ b/swagger_test.go @@ -258,6 +258,49 @@ func TestSwagger_Good_InfoFromOptions(t *testing.T) { } } +func TestSwagger_Good_UsesLicenseMetadata(t *testing.T) { + gin.SetMode(gin.TestMode) + + e, err := api.New( + api.WithSwagger("Licensed API", "Licensed test", "1.0.0"), + api.WithSwaggerLicense("EUPL-1.2", "https://eupl.eu/1.2/en/"), + ) + 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) + } + + info := doc["info"].(map[string]any) + license, ok := info["license"].(map[string]any) + if !ok { + t.Fatal("expected license metadata in swagger doc") + } + if license["name"] != "EUPL-1.2" { + t.Fatalf("expected license name=%q, got %v", "EUPL-1.2", license["name"]) + } + if license["url"] != "https://eupl.eu/1.2/en/" { + t.Fatalf("expected license url=%q, got %v", "https://eupl.eu/1.2/en/", license["url"]) + } +} + func TestSwagger_Good_UsesServerMetadata(t *testing.T) { gin.SetMode(gin.TestMode)