feat(openapi): document path parameters
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
2f8f8f805e
commit
edb1cf0c1e
2 changed files with 99 additions and 0 deletions
42
openapi.go
42
openapi.go
|
|
@ -132,6 +132,10 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
|
|||
"responses": operationResponses(method, rd.Response),
|
||||
}
|
||||
|
||||
if params := pathParameters(fullPath); len(params) > 0 {
|
||||
operation["parameters"] = params
|
||||
}
|
||||
|
||||
// Add request body for methods that accept one.
|
||||
// The contract only excludes GET; other verbs may legitimately carry bodies.
|
||||
if rd.RequestBody != nil && method != "get" {
|
||||
|
|
@ -307,6 +311,44 @@ func (sb *SpecBuilder) buildTags(groups []RouteGroup) []map[string]any {
|
|||
return tags
|
||||
}
|
||||
|
||||
// pathParameters extracts unique OpenAPI path parameters from a path template.
|
||||
// Parameters are returned in the order they appear in the path.
|
||||
func pathParameters(path string) []map[string]any {
|
||||
const (
|
||||
open = '{'
|
||||
close = '}'
|
||||
)
|
||||
|
||||
seen := map[string]bool{}
|
||||
params := make([]map[string]any, 0)
|
||||
|
||||
for i := 0; i < len(path); i++ {
|
||||
if path[i] != open {
|
||||
continue
|
||||
}
|
||||
end := strings.IndexByte(path[i+1:], close)
|
||||
if end < 0 {
|
||||
continue
|
||||
}
|
||||
name := path[i+1 : i+1+end]
|
||||
if name == "" || strings.ContainsAny(name, "/{}") || seen[name] {
|
||||
continue
|
||||
}
|
||||
seen[name] = true
|
||||
params = append(params, map[string]any{
|
||||
"name": name,
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
})
|
||||
i += end + 1
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// resolvedOperationTags returns the explicit route tags when provided, or a
|
||||
// stable fallback derived from the group's name when the route omits tags.
|
||||
func resolvedOperationTags(g RouteGroup, rd RouteDescription) []string {
|
||||
|
|
|
|||
|
|
@ -567,6 +567,63 @@ func TestSpecBuilder_Good_RequestBodyOnHead(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_PathParameters(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}/{slug}",
|
||||
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)
|
||||
}
|
||||
|
||||
op := spec["paths"].(map[string]any)["/api/users/{id}/{slug}"].(map[string]any)["get"].(map[string]any)
|
||||
params, ok := op["parameters"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected parameters array, got %T", op["parameters"])
|
||||
}
|
||||
if len(params) != 2 {
|
||||
t.Fatalf("expected 2 path parameters, got %d", len(params))
|
||||
}
|
||||
|
||||
first := params[0].(map[string]any)
|
||||
if first["name"] != "id" {
|
||||
t.Fatalf("expected first parameter name=id, got %v", first["name"])
|
||||
}
|
||||
if first["in"] != "path" {
|
||||
t.Fatalf("expected first parameter in=path, got %v", first["in"])
|
||||
}
|
||||
if required, ok := first["required"].(bool); !ok || !required {
|
||||
t.Fatalf("expected first parameter to be required, got %v", first["required"])
|
||||
}
|
||||
|
||||
second := params[1].(map[string]any)
|
||||
if second["name"] != "slug" {
|
||||
t.Fatalf("expected second parameter name=slug, got %v", second["name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_NonDescribableGroup(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue