feat(openapi): support gin-style path params
Normalise Gin colon and wildcard route segments into OpenAPI template parameters so documented paths match the framework's route syntax. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
13cc93f4f4
commit
7c3e8e7ba6
2 changed files with 85 additions and 10 deletions
35
openapi.go
35
openapi.go
|
|
@ -177,7 +177,8 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
|
|||
}
|
||||
|
||||
// joinOpenAPIPath normalises a base path and relative route path into a single
|
||||
// OpenAPI path without duplicate or missing separators.
|
||||
// OpenAPI path without duplicate or missing separators. Gin-style parameters
|
||||
// such as :id and *path are converted to OpenAPI template parameters.
|
||||
func joinOpenAPIPath(basePath, routePath string) string {
|
||||
basePath = strings.TrimSpace(basePath)
|
||||
routePath = strings.TrimSpace(routePath)
|
||||
|
|
@ -190,30 +191,44 @@ func joinOpenAPIPath(basePath, routePath string) string {
|
|||
}
|
||||
|
||||
basePath = normaliseOpenAPIPath(basePath)
|
||||
routePath = strings.TrimLeft(routePath, "/")
|
||||
routePath = normaliseOpenAPIPath(routePath)
|
||||
|
||||
if basePath == "/" {
|
||||
return "/" + routePath
|
||||
return routePath
|
||||
}
|
||||
|
||||
return basePath + "/" + routePath
|
||||
return strings.TrimRight(basePath, "/") + "/" + strings.TrimPrefix(routePath, "/")
|
||||
}
|
||||
|
||||
// normaliseOpenAPIPath trims whitespace and collapses trailing separators
|
||||
// while preserving the root path.
|
||||
// while preserving the root path and converting Gin-style path parameters.
|
||||
func normaliseOpenAPIPath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return "/"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
|
||||
segments := strings.Split(path, "/")
|
||||
cleaned := make([]string, 0, len(segments))
|
||||
for _, segment := range segments {
|
||||
segment = strings.TrimSpace(segment)
|
||||
if segment == "" {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(segment, ":") && len(segment) > 1:
|
||||
segment = "{" + segment[1:] + "}"
|
||||
case strings.HasPrefix(segment, "*") && len(segment) > 1:
|
||||
segment = "{" + segment[1:] + "}"
|
||||
}
|
||||
cleaned = append(cleaned, segment)
|
||||
}
|
||||
path = strings.TrimRight(path, "/")
|
||||
if path == "" {
|
||||
|
||||
if len(cleaned) == 0 {
|
||||
return "/"
|
||||
}
|
||||
return path
|
||||
|
||||
return "/" + strings.Join(cleaned, "/")
|
||||
}
|
||||
|
||||
// operationResponses builds the standard response set for a documented API
|
||||
|
|
|
|||
|
|
@ -661,6 +661,66 @@ func TestSpecBuilder_Good_PathNormalisation(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_GinPathParameters(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",
|
||||
},
|
||||
},
|
||||
{
|
||||
Method: "GET",
|
||||
Path: "files/*path",
|
||||
Summary: "Get file",
|
||||
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)
|
||||
|
||||
userOp := paths["/api/users/{id}"].(map[string]any)["get"].(map[string]any)
|
||||
userParams := userOp["parameters"].([]any)
|
||||
if len(userParams) != 1 {
|
||||
t.Fatalf("expected 1 parameter for gin path, got %d", len(userParams))
|
||||
}
|
||||
if userParams[0].(map[string]any)["name"] != "id" {
|
||||
t.Fatalf("expected gin path parameter name=id, got %v", userParams[0])
|
||||
}
|
||||
|
||||
fileOp := paths["/api/files/{path}"].(map[string]any)["get"].(map[string]any)
|
||||
fileParams := fileOp["parameters"].([]any)
|
||||
if len(fileParams) != 1 {
|
||||
t.Fatalf("expected 1 parameter for wildcard path, got %d", len(fileParams))
|
||||
}
|
||||
if fileParams[0].(map[string]any)["name"] != "path" {
|
||||
t.Fatalf("expected wildcard parameter name=path, got %v", fileParams[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_ExplicitParameters(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue