From 52712d0fb912abef2d1a5a08caa290f07a7327e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Jun 2026 11:33:40 +0200 Subject: [PATCH 1/4] feat: add OpenAPI spec configuration options for title, description, and version --- README.md | 13 +++++ _examples/simple/main.go | 13 +++-- config_test.go | 102 ++++++++++++++++++++++++++++++++++++++- fiberoapi.go | 28 ++++++++--- types.go | 3 ++ 5 files changed, 145 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b3b5d9a..62d3e90 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,19 @@ Default config when none is provided: - Docs path: `/docs` - JSON spec path: `/openapi.json` - YAML spec path: `/openapi.yaml` +- Spec title: `Fiber OpenAPI` +- Spec description: `API documentation generated by fiber-oapi` +- Spec version: `1.0.0` + +Override the `info` block of the generated spec via `Config`: + +```go +fiberoapi.New(app, fiberoapi.Config{ + OpenAPITitle: "My Service", + OpenAPIDescription: "Public REST API for the My Service platform", + OpenAPIVersion: "2024.10.1", +}) +``` ## HTTP Methods diff --git a/_examples/simple/main.go b/_examples/simple/main.go index 963f3e3..e92c907 100644 --- a/_examples/simple/main.go +++ b/_examples/simple/main.go @@ -84,11 +84,14 @@ func main() { // Example 1: Using custom configuration customConfig := fiberoapi.Config{ - EnableValidation: true, - EnableOpenAPIDocs: true, - OpenAPIDocsPath: "/documentation", // Custom docs path - OpenAPIJSONPath: "/api-spec.json", // Custom spec path - OpenAPIYamlPath: "/api-spec.yaml", // Custom YAML spec path + EnableValidation: true, + EnableOpenAPIDocs: true, + OpenAPIDocsPath: "/documentation", // Custom docs path + OpenAPIJSONPath: "/api-spec.json", // Custom spec path + OpenAPIYamlPath: "/api-spec.yaml", // Custom YAML spec path + OpenAPITitle: "Simple Example API", // Spec title + OpenAPIDescription: "A minimal fiber-oapi demo for the README", // Spec description + OpenAPIVersion: "0.1.0", // Spec version } appOApi := fiberoapi.New(app, customConfig) v1 := fiberoapi.Group(appOApi, "/api/v1") diff --git a/config_test.go b/config_test.go index 20759f6..8f6e9da 100644 --- a/config_test.go +++ b/config_test.go @@ -1,6 +1,9 @@ package fiberoapi import ( + "encoding/json" + "io" + "net/http/httptest" "testing" "github.com/gofiber/fiber/v3" @@ -71,7 +74,7 @@ func TestConfigMerging(t *testing.T) { AuthService: authService, }) config := oapi.Config() - + // Should respect explicit values when auth service is provided if config.EnableValidation { t.Error("Expected EnableValidation to be false when explicitly configured") @@ -83,4 +86,101 @@ func TestConfigMerging(t *testing.T) { t.Error("Expected AuthService to be set") } }) +} + +func TestOpenAPIInfoConfig(t *testing.T) { + t.Run("Defaults populate info block", func(t *testing.T) { + oapi := New(fiber.New()) + cfg := oapi.Config() + if cfg.OpenAPITitle != "Fiber OpenAPI" { + t.Errorf("default OpenAPITitle = %q, want %q", cfg.OpenAPITitle, "Fiber OpenAPI") + } + if cfg.OpenAPIDescription != "API documentation generated by fiber-oapi" { + t.Errorf("default OpenAPIDescription = %q", cfg.OpenAPIDescription) + } + if cfg.OpenAPIVersion != "1.0.0" { + t.Errorf("default OpenAPIVersion = %q, want %q", cfg.OpenAPIVersion, "1.0.0") + } + + spec := oapi.GenerateOpenAPISpec() + info := spec["info"].(map[string]interface{}) + if info["title"] != "Fiber OpenAPI" { + t.Errorf("spec title = %v", info["title"]) + } + if info["description"] != "API documentation generated by fiber-oapi" { + t.Errorf("spec description = %v", info["description"]) + } + if info["version"] != "1.0.0" { + t.Errorf("spec version = %v", info["version"]) + } + }) + + t.Run("Custom title/description/version override defaults", func(t *testing.T) { + oapi := New(fiber.New(), Config{ + OpenAPITitle: "My Service", + OpenAPIDescription: "Public REST API", + OpenAPIVersion: "2024.10.1", + }) + + spec := oapi.GenerateOpenAPISpec() + info := spec["info"].(map[string]interface{}) + if info["title"] != "My Service" { + t.Errorf("title = %v, want %q", info["title"], "My Service") + } + if info["description"] != "Public REST API" { + t.Errorf("description = %v", info["description"]) + } + if info["version"] != "2024.10.1" { + t.Errorf("version = %v, want %q", info["version"], "2024.10.1") + } + }) + + t.Run("Partial override preserves other defaults", func(t *testing.T) { + oapi := New(fiber.New(), Config{ + OpenAPITitle: "Only Title Set", + }) + cfg := oapi.Config() + if cfg.OpenAPITitle != "Only Title Set" { + t.Errorf("title not overridden: %q", cfg.OpenAPITitle) + } + if cfg.OpenAPIDescription != "API documentation generated by fiber-oapi" { + t.Errorf("description should keep default, got %q", cfg.OpenAPIDescription) + } + if cfg.OpenAPIVersion != "1.0.0" { + t.Errorf("version should keep default, got %q", cfg.OpenAPIVersion) + } + }) + + t.Run("Auto-served /openapi.json reflects custom info", func(t *testing.T) { + app := fiber.New() + New(app, Config{ + OpenAPITitle: "Auto Served", + OpenAPIDescription: "Served via auto docs route", + OpenAPIVersion: "9.9.9", + }) + + req := httptest.NewRequest("GET", "/openapi.json", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf("Test error: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("status = %d", resp.StatusCode) + } + body, _ := io.ReadAll(resp.Body) + var spec map[string]interface{} + if err := json.Unmarshal(body, &spec); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + info := spec["info"].(map[string]interface{}) + if info["title"] != "Auto Served" { + t.Errorf("title = %v", info["title"]) + } + if info["description"] != "Served via auto docs route" { + t.Errorf("description = %v", info["description"]) + } + if info["version"] != "9.9.9" { + t.Errorf("version = %v", info["version"]) + } + }) } \ No newline at end of file diff --git a/fiberoapi.go b/fiberoapi.go index 4c7b2b9..a1ea744 100644 --- a/fiberoapi.go +++ b/fiberoapi.go @@ -15,11 +15,14 @@ import ( // DefaultConfig returns the default configuration func DefaultConfig() Config { return Config{ - EnableValidation: true, - EnableOpenAPIDocs: true, - OpenAPIDocsPath: "/docs", - OpenAPIJSONPath: "/openapi.json", - OpenAPIYamlPath: "/openapi.yaml", + EnableValidation: true, + EnableOpenAPIDocs: true, + OpenAPIDocsPath: "/docs", + OpenAPIJSONPath: "/openapi.json", + OpenAPIYamlPath: "/openapi.yaml", + OpenAPITitle: "Fiber OpenAPI", + OpenAPIDescription: "API documentation generated by fiber-oapi", + OpenAPIVersion: "1.0.0", } } @@ -86,6 +89,15 @@ func New(app *fiber.App, config ...Config) *OApiApp { if provided.OpenAPIYamlPath != "" { cfg.OpenAPIYamlPath = provided.OpenAPIYamlPath } + if provided.OpenAPITitle != "" { + cfg.OpenAPITitle = provided.OpenAPITitle + } + if provided.OpenAPIDescription != "" { + cfg.OpenAPIDescription = provided.OpenAPIDescription + } + if provided.OpenAPIVersion != "" { + cfg.OpenAPIVersion = provided.OpenAPIVersion + } if provided.AuthService != nil { cfg.AuthService = provided.AuthService } @@ -160,9 +172,9 @@ func (o *OApiApp) GenerateOpenAPISpec() map[string]interface{} { spec := map[string]interface{}{ "openapi": "3.0.0", "info": map[string]interface{}{ - "title": "Fiber OpenAPI", - "version": "1.0.0", - "description": "API documentation generated by fiber-oapi", + "title": o.config.OpenAPITitle, + "version": o.config.OpenAPIVersion, + "description": o.config.OpenAPIDescription, }, "paths": make(map[string]interface{}), "components": make(map[string]interface{}), diff --git a/types.go b/types.go index 537c885..e106b88 100644 --- a/types.go +++ b/types.go @@ -69,6 +69,9 @@ type Config struct { OpenAPIDocsPath string // Path for documentation UI (default: "/docs") OpenAPIJSONPath string // Path for OpenAPI JSON spec (default: "/openapi.json") OpenAPIYamlPath string // Path for OpenAPI YAML spec (default: "/openapi.yaml") + OpenAPITitle string // Title of the OpenAPI spec (default: "Fiber OpenAPI") + OpenAPIDescription string // Description of the OpenAPI spec (default: "API documentation generated by fiber-oapi") + OpenAPIVersion string // Version of the OpenAPI spec (default: "1.0.0") AuthService AuthorizationService // Service for handling authentication and authorization SecuritySchemes map[string]SecurityScheme // OpenAPI security schemes DefaultSecurity []map[string][]string // Default security requirements From 22553d866f073a5400ee2bf7e2bf38967d69fa38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Jun 2026 11:42:12 +0200 Subject: [PATCH 2/4] feat: add OpenAPI spec configuration options for title, description, and version --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 62d3e90..da271c0 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,9 @@ type Config struct { OpenAPIDocsPath string // Path for docs UI (default: "/docs") OpenAPIJSONPath string // Path for JSON spec (default: "/openapi.json") OpenAPIYamlPath string // Path for YAML spec (default: "/openapi.yaml") + OpenAPITitle string // Spec title (default: "Fiber OpenAPI") + OpenAPIDescription string // Spec description (default: "API documentation generated by fiber-oapi") + OpenAPIVersion string // Spec version (default: "1.0.0") AuthService AuthorizationService // Service for handling auth SecuritySchemes map[string]SecurityScheme // OpenAPI security schemes DefaultSecurity []map[string][]string // Default security requirements From cfcaf805e91f95b53752361155e3f80119386185 Mon Sep 17 00:00:00 2001 From: Jeremy Mouton Date: Tue, 2 Jun 2026 11:42:20 +0200 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- config_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config_test.go b/config_test.go index 8f6e9da..52da578 100644 --- a/config_test.go +++ b/config_test.go @@ -164,10 +164,14 @@ func TestOpenAPIInfoConfig(t *testing.T) { if err != nil { t.Fatalf("Test error: %v", err) } + defer resp.Body.Close() if resp.StatusCode != 200 { t.Fatalf("status = %d", resp.StatusCode) } - body, _ := io.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("read response body: %v", err) + } var spec map[string]interface{} if err := json.Unmarshal(body, &spec); err != nil { t.Fatalf("invalid JSON: %v", err) From 9cd2fb3908891f369e23672fd60709841d27c8d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Jun 2026 11:46:41 +0200 Subject: [PATCH 4/4] feat: enhance OpenAPI config handling for explicit and metadata-only settings --- config_test.go | 32 ++++++++++++++++++++++++++++++++ fiberoapi.go | 39 +++++++++++++++++++++++++++------------ 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/config_test.go b/config_test.go index 52da578..560a2f6 100644 --- a/config_test.go +++ b/config_test.go @@ -151,6 +151,38 @@ func TestOpenAPIInfoConfig(t *testing.T) { } }) + t.Run("Info field + explicit disable + core signal propagates booleans", func(t *testing.T) { + // With a core signal (AuthService here) booleans from `provided` are honored, + // so the explicit EnableOpenAPIDocs:false actually disables auto docs even + // though the user also customized the title. + authService := &struct{ AuthorizationService }{} + oapi := New(fiber.New(), Config{ + OpenAPITitle: "Custom", + EnableOpenAPIDocs: false, + AuthService: authService, + }) + cfg := oapi.Config() + if cfg.EnableOpenAPIDocs { + t.Error("EnableOpenAPIDocs should be false when explicitly disabled alongside a core signal") + } + if cfg.OpenAPITitle != "Custom" { + t.Errorf("title = %q", cfg.OpenAPITitle) + } + }) + + t.Run("Info-only config keeps boolean defaults", func(t *testing.T) { + // Without any core signal, info-only customization must not silently flip + // EnableValidation / EnableOpenAPIDocs to false via Go's zero values. + oapi := New(fiber.New(), Config{OpenAPITitle: "Just Title"}) + cfg := oapi.Config() + if !cfg.EnableValidation { + t.Error("EnableValidation should stay true when only cosmetic fields are set") + } + if !cfg.EnableOpenAPIDocs { + t.Error("EnableOpenAPIDocs should stay true when only cosmetic fields are set") + } + }) + t.Run("Auto-served /openapi.json reflects custom info", func(t *testing.T) { app := fiber.New() New(app, Config{ diff --git a/fiberoapi.go b/fiberoapi.go index a1ea744..dbfa447 100644 --- a/fiberoapi.go +++ b/fiberoapi.go @@ -38,13 +38,19 @@ func New(app *fiber.App, config ...Config) *OApiApp { // We'll only override if the provided config appears to be intentionally configured // Simple approach: if the provided config has any non-default values, - // we assume the user intended to configure it explicitly + // we assume the user intended to configure it explicitly. + // Info fields (title/description/version) participate in this signal too so that + // e.g. `Config{OpenAPITitle: "X", EnableOpenAPIDocs: false}` propagates the + // explicit `false` to the resolved config (consistency with path fields). hasExplicitConfig := provided.EnableAuthorization || provided.AuthService != nil || provided.SecuritySchemes != nil || provided.OpenAPIDocsPath != "" || provided.OpenAPIJSONPath != "" || provided.OpenAPIYamlPath != "" || + provided.OpenAPITitle != "" || + provided.OpenAPIDescription != "" || + provided.OpenAPIVersion != "" || provided.ValidationErrorHandler != nil || provided.AuthErrorHandler != nil @@ -55,23 +61,32 @@ func New(app *fiber.App, config ...Config) *OApiApp { } // If no explicit config, keep the defaults (true, true, false) - // Heuristic: when only handler(s) are provided and all booleans are at zero value, - // assume the caller didn't intend to disable validation/docs — restore defaults. - otherExplicitConfig := provided.EnableAuthorization || + // Heuristic: when only "metadata" fields (paths, info, handlers — i.e. cosmetic + // customization that does not signal intent about validation/docs themselves) + // are provided and all booleans are at their zero value, restore boolean defaults. + // This avoids surprising users who only want to override the spec title/path but + // end up with validation silently disabled because Go zero values can't disambiguate + // "not set" from "set to false". To actually disable validation or docs, set at least + // one core signal field too (EnableAuthorization / AuthService / SecuritySchemes). + hasCoreSignal := provided.EnableAuthorization || provided.AuthService != nil || - provided.SecuritySchemes != nil || - provided.OpenAPIDocsPath != "" || + provided.SecuritySchemes != nil + + hasMetadataOnly := !hasCoreSignal && (provided.OpenAPIDocsPath != "" || provided.OpenAPIJSONPath != "" || - provided.OpenAPIYamlPath != "" + provided.OpenAPIYamlPath != "" || + provided.OpenAPITitle != "" || + provided.OpenAPIDescription != "" || + provided.OpenAPIVersion != "" || + provided.ValidationErrorHandler != nil || + provided.AuthErrorHandler != nil) // Only restore defaults if ALL boolean fields are false (suggesting they weren't explicitly set) allBooleansAreFalse := !provided.EnableValidation && !provided.EnableOpenAPIDocs && !provided.EnableAuthorization - hasOnlyHandlers := (provided.ValidationErrorHandler != nil || provided.AuthErrorHandler != nil) && !otherExplicitConfig - if hasOnlyHandlers && allBooleansAreFalse { - // Only handler(s) are set, so restore defaults for boolean fields - cfg.EnableValidation = true // Keep validation enabled - the handler needs it - cfg.EnableOpenAPIDocs = true // Keep docs enabled - default behavior + if hasMetadataOnly && allBooleansAreFalse { + cfg.EnableValidation = true // Restore — caller likely didn't mean to disable it + cfg.EnableOpenAPIDocs = true // Restore — caller likely didn't mean to disable it } // For EnableAuthorization: only set to true if explicitly provided