feat(api): normalise OpenAPI server metadata

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 17:31:45 +00:00
parent 5b59a1dd10
commit c48effb6b7
7 changed files with 48 additions and 16 deletions

View file

@ -230,6 +230,7 @@ func (c *OpenAPIClient) loadSpec() error {
}
}
}
c.servers = normaliseServers(c.servers)
if c.baseURL == "" {
for _, server := range c.servers {

View file

@ -278,8 +278,9 @@ info:
title: Test API
version: 1.0.0
servers:
- url: " `+srv.URL+` "
- url: /
- url: `+srv.URL+`
- url: " `+srv.URL+` "
paths:
/hello:
get:

View file

@ -37,17 +37,12 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
},
}
if len(sb.Servers) > 0 {
servers := make([]map[string]any, 0, len(sb.Servers))
for _, server := range sb.Servers {
if server == "" {
continue
}
servers = append(servers, map[string]any{"url": server})
}
if len(servers) > 0 {
spec["servers"] = servers
if servers := normaliseServers(sb.Servers); len(servers) > 0 {
out := make([]map[string]any, 0, len(servers))
for _, server := range servers {
out = append(out, map[string]any{"url": server})
}
spec["servers"] = out
}
// Add component schemas for the response envelope.

View file

@ -779,9 +779,10 @@ func TestSpecBuilder_Good_Servers(t *testing.T) {
Title: "Test",
Version: "1.0.0",
Servers: []string{
"https://api.example.com",
" https://api.example.com ",
"/",
"",
"https://api.example.com",
},
}
@ -800,7 +801,7 @@ func TestSpecBuilder_Good_Servers(t *testing.T) {
t.Fatalf("expected servers array, got %T", spec["servers"])
}
if len(servers) != 2 {
t.Fatalf("expected 2 non-empty servers, got %d", len(servers))
t.Fatalf("expected 2 normalised servers, got %d", len(servers))
}
first := servers[0].(map[string]any)

View file

@ -134,7 +134,7 @@ func WithSwagger(title, description, version string) Option {
// 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...)
e.swaggerServers = normaliseServers(servers)
}
}

34
servers.go Normal file
View file

@ -0,0 +1,34 @@
// SPDX-License-Identifier: EUPL-1.2
package api
import "strings"
// normaliseServers trims whitespace, removes empty entries, and preserves
// the first occurrence of each server URL.
func normaliseServers(servers []string) []string {
if len(servers) == 0 {
return nil
}
cleaned := make([]string, 0, len(servers))
seen := make(map[string]struct{}, len(servers))
for _, server := range servers {
server = strings.TrimSpace(server)
if server == "" {
continue
}
if _, ok := seen[server]; ok {
continue
}
seen[server] = struct{}{}
cleaned = append(cleaned, server)
}
if len(cleaned) == 0 {
return nil
}
return cleaned
}

View file

@ -263,7 +263,7 @@ func TestSwagger_Good_UsesServerMetadata(t *testing.T) {
e, err := api.New(
api.WithSwagger("Server API", "Server metadata test", "1.0.0"),
api.WithSwaggerServers("https://api.example.com", "/", ""),
api.WithSwaggerServers(" https://api.example.com ", "/", "", "https://api.example.com"),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
@ -293,7 +293,7 @@ func TestSwagger_Good_UsesServerMetadata(t *testing.T) {
t.Fatalf("expected servers array, got %T", doc["servers"])
}
if len(servers) != 2 {
t.Fatalf("expected 2 servers, got %d", len(servers))
t.Fatalf("expected 2 normalised servers, got %d", len(servers))
}
first := servers[0].(map[string]any)