feat(api): expose swagger licence metadata

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 20:26:55 +00:00
parent 0ed1cfa1b1
commit b2d3c96ed7
6 changed files with 86 additions and 15 deletions

39
api.go
View file

@ -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.

View file

@ -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 |

View file

@ -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

View file

@ -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

View file

@ -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,
}

View file

@ -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)