diff --git a/internal/openapi/schema.go b/internal/openapi/schema.go index b28b609..e2d4a56 100644 --- a/internal/openapi/schema.go +++ b/internal/openapi/schema.go @@ -354,14 +354,17 @@ func describeBody(op *operationInfo, field string, maxDepth int, names SchemaFla for _, p := range op.PathParams { doc.Args = append(doc.Args, SchemaArg{ Name: p.Name, - Placeholder: "<" + slugify(p.Name) + ">", + Placeholder: "<" + canonicalName(p.Name) + ">", Type: p.Type, Description: p.Description, }) } - for _, q := range op.QueryParams { + // Use the same resolution as flag registration so collision renames + // (--param-base-url, --foo-2) are reported exactly as the CLI accepts them. + for _, qf := range resolveQueryFlags(op) { + q := qf.Param doc.QueryParams = append(doc.QueryParams, SchemaQueryParam{ - Flag: "--" + slugify(q.Name), + Flag: "--" + qf.Name, Name: q.Name, Type: q.Type, Enum: q.Enum, diff --git a/internal/openapi/schema_test.go b/internal/openapi/schema_test.go index dcaef88..5e9a2a6 100644 --- a/internal/openapi/schema_test.go +++ b/internal/openapi/schema_test.go @@ -622,9 +622,14 @@ func TestSchema_BodylessGetDescribesArgsAndQueryParams(t *testing.T) { t.Fatalf("args = %v, want 1 entry", doc.Args) } arg := doc.Args[0] - if arg.Name != "widgetId" || arg.Placeholder != "" || arg.Type != "string" || arg.Description != "Widget UUID" { + if arg.Name != "widgetId" || arg.Placeholder != "" || arg.Type != "string" || arg.Description != "Widget UUID" { t.Errorf("arg = %+v, want the widgetId path param", arg) } + // The placeholder and flag spellings must match what --help and the usage + // line print (canonicalName), not the older bare-lowercase slugify form. + if strings.Contains(arg.Placeholder, "widgetid") { + t.Errorf("placeholder %q uses the old slugify spelling; want kebab-case", arg.Placeholder) + } byFlag := map[string]SchemaQueryParam{} for _, q := range doc.QueryParams { @@ -1132,3 +1137,55 @@ func TestSchema_StaticDepthLimitMatchesDescriber(t *testing.T) { t.Errorf("static truncation = %s, describer truncation = %s", got, want) } } + +// collidingQueryTestSpec has query params whose canonical flag names collide: +// "baseUrl" with the global --base-url, and "branchId"/"branch_id" with each +// other. Flag registration renames these (see resolveQueryFlags); --schema must +// advertise the renamed flags, not the bare canonical spelling. +const collidingQueryTestSpec = `{ + "openapi": "3.1.0", + "info": {"title": "test", "version": "1.0"}, + "paths": { + "/api/v1/widgets": { + "get": { + "operationId": "widgetsList", + "tags": ["widgets"], + "parameters": [ + {"name": "baseUrl", "in": "query", "description": "Filter by URL", "schema": {"type": "string"}}, + {"name": "branchId", "in": "query", "description": "Branch (camel)", "schema": {"type": "string"}}, + {"name": "branch_id", "in": "query", "description": "Branch (snake)", "schema": {"type": "string"}} + ], + "responses": {"200": {"description": "ok"}} + } + } + } +}` + +func TestSchema_QueryFlagsReflectCollisionRenames(t *testing.T) { + doc, err := runSchemaCmd(t, collidingQueryTestSpec, "list") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + got := map[string]string{} + for _, q := range doc.QueryParams { + got[q.Name] = q.Flag + } + want := map[string]string{ + "baseUrl": "--param-base-url", + "branchId": "--branch-id", + "branch_id": "--branch-id-2", + } + for name, flag := range want { + if got[name] != flag { + t.Errorf("query param %q advertised as %q, want %q (the flag the CLI actually registers)", name, got[name], flag) + } + } + + // And the advertised flags must all be accepted by the command itself. + cmd := subcommand(t, collidingQueryTestSpec, "list") + for _, flag := range want { + if cmd.Flags().Lookup(strings.TrimPrefix(flag, "--")) == nil { + t.Errorf("--schema advertises %s but the command does not register it", flag) + } + } +}