From 6017ac7132b3fb8aa079df4c316890e2a72b1a71 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 20:01:34 +0000 Subject: [PATCH] feat(api): collapse equivalent OpenAPI servers Normalise server metadata so trailing-slash variants deduplicate to a single entry. Adds a regression test covering both absolute and relative server URLs. Co-Authored-By: Virgil --- openapi_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ servers.go | 21 ++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/openapi_test.go b/openapi_test.go index a894b0b..ae3bd50 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -1252,3 +1252,43 @@ func TestSpecBuilder_Good_Servers(t *testing.T) { t.Fatalf("expected second server url=%q, got %v", "/", second["url"]) } } + +func TestSpecBuilder_Good_ServersCollapseTrailingSlashes(t *testing.T) { + sb := &api.SpecBuilder{ + Title: "Test", + Version: "1.0.0", + Servers: []string{ + "https://api.example.com/", + "https://api.example.com", + "/api/", + "/api", + }, + } + + data, err := sb.Build(nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var spec map[string]any + if err := json.Unmarshal(data, &spec); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + servers, ok := spec["servers"].([]any) + if !ok { + t.Fatalf("expected servers array, got %T", spec["servers"]) + } + if len(servers) != 2 { + t.Fatalf("expected 2 collapsed 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"] != "/api" { + t.Fatalf("expected second server url=%q, got %v", "/api", second["url"]) + } +} diff --git a/servers.go b/servers.go index 5a9f5c1..676eafe 100644 --- a/servers.go +++ b/servers.go @@ -15,7 +15,7 @@ func normaliseServers(servers []string) []string { seen := make(map[string]struct{}, len(servers)) for _, server := range servers { - server = strings.TrimSpace(server) + server = normaliseServer(server) if server == "" { continue } @@ -32,3 +32,22 @@ func normaliseServers(servers []string) []string { return cleaned } + +// normaliseServer trims surrounding whitespace and removes a trailing slash +// from non-root server URLs so equivalent metadata collapses to one entry. +func normaliseServer(server string) string { + server = strings.TrimSpace(server) + if server == "" { + return "" + } + if server == "/" { + return server + } + + server = strings.TrimRight(server, "/") + if server == "" { + return "/" + } + + return server +}