feat(api): normalise OpenAPI path joins
Normalise the concatenation of BasePath() and RouteDescription paths so trailing or missing slashes do not produce malformed OpenAPI entries. Add a regression test for mixed slash formatting. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
a055781d5d
commit
ea94081231
2 changed files with 78 additions and 1 deletions
42
openapi.go
42
openapi.go
|
|
@ -116,7 +116,7 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
|
|||
continue
|
||||
}
|
||||
for _, rd := range dg.Describe() {
|
||||
fullPath := g.BasePath() + rd.Path
|
||||
fullPath := joinOpenAPIPath(g.BasePath(), rd.Path)
|
||||
method := strings.ToLower(rd.Method)
|
||||
|
||||
operation := map[string]any{
|
||||
|
|
@ -174,6 +174,46 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
|
|||
return paths
|
||||
}
|
||||
|
||||
// joinOpenAPIPath normalises a base path and relative route path into a single
|
||||
// OpenAPI path without duplicate or missing separators.
|
||||
func joinOpenAPIPath(basePath, routePath string) string {
|
||||
basePath = strings.TrimSpace(basePath)
|
||||
routePath = strings.TrimSpace(routePath)
|
||||
|
||||
if basePath == "" {
|
||||
basePath = "/"
|
||||
}
|
||||
if routePath == "" || routePath == "/" {
|
||||
return normaliseOpenAPIPath(basePath)
|
||||
}
|
||||
|
||||
basePath = normaliseOpenAPIPath(basePath)
|
||||
routePath = strings.TrimLeft(routePath, "/")
|
||||
|
||||
if basePath == "/" {
|
||||
return "/" + routePath
|
||||
}
|
||||
|
||||
return basePath + "/" + routePath
|
||||
}
|
||||
|
||||
// normaliseOpenAPIPath trims whitespace and collapses trailing separators
|
||||
// while preserving the root path.
|
||||
func normaliseOpenAPIPath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return "/"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
path = strings.TrimRight(path, "/")
|
||||
if path == "" {
|
||||
return "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// operationResponses builds the standard response set for a documented API
|
||||
// operation. The framework always exposes the common envelope responses, plus
|
||||
// middleware-driven 429 and 504 errors.
|
||||
|
|
|
|||
|
|
@ -624,6 +624,43 @@ func TestSpecBuilder_Good_PathParameters(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_PathNormalisation(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
group := &specStubGroup{
|
||||
name: "users",
|
||||
basePath: "/api/",
|
||||
descs: []api.RouteDescription{
|
||||
{
|
||||
Method: "GET",
|
||||
Path: "users/{id}",
|
||||
Summary: "Get user",
|
||||
Response: map[string]any{
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := sb.Build([]api.RouteGroup{group})
|
||||
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)
|
||||
}
|
||||
|
||||
paths := spec["paths"].(map[string]any)
|
||||
if _, ok := paths["/api/users/{id}"]; !ok {
|
||||
t.Fatalf("expected normalised path /api/users/{id}, got %v", paths)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_ExplicitParameters(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue