← back to Cli Printing Press
fix(cli): dedup colliding flag identifiers in generated commands (#283)
72a84a88d671174c814b09fb4f5032cc26f4b867 · 2026-04-25 10:13:32 -0700 · Trevin Chow
* fix(cli): dedup colliding flag identifiers in generated commands
When a spec lists params whose names camelize to the same Go
identifier — Twilio's StartTime, StartTime>, StartTime< all
collapsing to "StartTime" — or has a literal "all" param on a
paginated endpoint colliding with pagination's reserved flagAll,
the generator emitted multiple `var flagX` declarations in one
function and the resulting CLI refused to compile.
A pre-render dedup pass now walks each endpoint's params and
populates a new IdentName field on collisions, suffixing _2/_3
until the camel identifier and kebab flag name are both unique
relative to other params on the same endpoint and to reserved
generator names (flagAll for paginated endpoints; flagWait*
for async). Templates derive Go identifiers and CLI flag names
through paramIdent (IdentName when set, else Name). Wire-side
serialization — URL params, JSON keys, path substitution —
keeps reading Name, so request bodies and query strings still
hit the API with the parameter names it expects.
Reported in #275 (F-2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): align sub-resource async key in flag dedup pass
Code review on #283 surfaced that the dedup pass keyed
sub-resource async lookups on resName.subName/epName, while
the rest of the generator (see generator.go:1195) keys them
on subName/epName without the parent prefix. The mismatch
hid behind the fact that DetectAsyncJobs does not currently
walk sub-resources, so the lookup was a no-op either way,
but any future async coverage of sub-resources would have
silently skipped the wait identifier protection.
Drop the parent-resource prefix and add a comment pointing
at the existing convention. Also add a regression test for
the top-level async case (a user param named "wait" on an
async-detected endpoint), which had no coverage before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
A internal/generator/flag_collision.goA internal/generator/flag_collision_test.goM internal/generator/generator.goM internal/generator/templates/command_endpoint.go.tmplM internal/generator/templates/command_promoted.go.tmplM internal/spec/spec.go
Diff
commit 72a84a88d671174c814b09fb4f5032cc26f4b867
Author: Trevin Chow <trevin@trevinchow.com>
Date: Sat Apr 25 10:13:32 2026 -0700
fix(cli): dedup colliding flag identifiers in generated commands (#283)
* fix(cli): dedup colliding flag identifiers in generated commands
When a spec lists params whose names camelize to the same Go
identifier — Twilio's StartTime, StartTime>, StartTime< all
collapsing to "StartTime" — or has a literal "all" param on a
paginated endpoint colliding with pagination's reserved flagAll,
the generator emitted multiple `var flagX` declarations in one
function and the resulting CLI refused to compile.
A pre-render dedup pass now walks each endpoint's params and
populates a new IdentName field on collisions, suffixing _2/_3
until the camel identifier and kebab flag name are both unique
relative to other params on the same endpoint and to reserved
generator names (flagAll for paginated endpoints; flagWait*
for async). Templates derive Go identifiers and CLI flag names
through paramIdent (IdentName when set, else Name). Wire-side
serialization — URL params, JSON keys, path substitution —
keeps reading Name, so request bodies and query strings still
hit the API with the parameter names it expects.
Reported in #275 (F-2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): align sub-resource async key in flag dedup pass
Code review on #283 surfaced that the dedup pass keyed
sub-resource async lookups on resName.subName/epName, while
the rest of the generator (see generator.go:1195) keys them
on subName/epName without the parent prefix. The mismatch
hid behind the fact that DetectAsyncJobs does not currently
walk sub-resources, so the lookup was a no-op either way,
but any future async coverage of sub-resources would have
silently skipped the wait identifier protection.
Drop the parent-resource prefix and add a comment pointing
at the existing convention. Also add a regression test for
the top-level async case (a user param named "wait" on an
async-detected endpoint), which had no coverage before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
internal/generator/flag_collision.go | 135 +++++++++++++
internal/generator/flag_collision_test.go | 222 +++++++++++++++++++++
internal/generator/generator.go | 7 +
.../generator/templates/command_endpoint.go.tmpl | 130 ++++++------
.../generator/templates/command_promoted.go.tmpl | 48 ++---
internal/spec/spec.go | 9 +
6 files changed, 462 insertions(+), 89 deletions(-)
diff --git a/internal/generator/flag_collision.go b/internal/generator/flag_collision.go
new file mode 100644
index 00000000..806dd966
--- /dev/null
+++ b/internal/generator/flag_collision.go
@@ -0,0 +1,135 @@
+package generator
+
+import (
+ "fmt"
+
+ "github.com/mvanhorn/cli-printing-press/internal/spec"
+)
+
+// dedupeFlagIdentifiers ensures that no two non-positional params on a single
+// endpoint share a Go identifier (flag<Camel>) or cobra flag name after
+// camelization or kebab-casing, and that no param collides with a reserved
+// generator-introduced identifier (pagination's flagAll; async's flagWait,
+// flagWaitTimeout, flagWaitInterval).
+//
+// Conflicting params have Name suffixed with _2, _3, ... until the identifier
+// and flag name are both unique. Without this, specs that use date-range
+// filter conventions (e.g., Twilio's StartTime, StartTime>, StartTime< all
+// camelize to "StartTime") or expose a literal "all" parameter on a paginated
+// endpoint (e.g., GitHub notifications colliding with pagination's --all)
+// produce duplicate `var flagX` declarations and refuse to compile.
+func (g *Generator) dedupeFlagIdentifiers() {
+ if g.Spec == nil {
+ return
+ }
+ for resName, res := range g.Spec.Resources {
+ for epName, ep := range res.Endpoints {
+ idents, flags := reservedFlagNamesForEndpoint(resName, epName, ep, g.AsyncJobs)
+ ep.Params = uniquifyParamNames(ep.Params, idents, flags)
+ res.Endpoints[epName] = ep
+ }
+ for subName, sub := range res.SubResources {
+ // Sub-resource async lookups elsewhere in the generator (see
+ // generator.go:1195) key on subName/epName without the parent
+ // resource prefix; mirror that here so any future async
+ // detection on sub-resources protects the wait identifiers
+ // correctly. DetectAsyncJobs does not currently walk
+ // sub-resources, so this lookup is a no-op today.
+ for epName, ep := range sub.Endpoints {
+ idents, flags := reservedFlagNamesForEndpoint(subName, epName, ep, g.AsyncJobs)
+ ep.Params = uniquifyParamNames(ep.Params, idents, flags)
+ sub.Endpoints[epName] = ep
+ }
+ res.SubResources[subName] = sub
+ }
+ g.Spec.Resources[resName] = res
+ }
+}
+
+// reservedFlagNamesForEndpoint returns identifiers and flag names that the
+// command templates emit themselves and that user params therefore must not
+// shadow.
+func reservedFlagNamesForEndpoint(resKey, epName string, ep spec.Endpoint, asyncJobs map[string]AsyncJobInfo) (idents, flags map[string]struct{}) {
+ idents = map[string]struct{}{}
+ flags = map[string]struct{}{}
+ if ep.Pagination != nil {
+ idents["flagAll"] = struct{}{}
+ flags["all"] = struct{}{}
+ }
+ if _, isAsync := asyncJobs[resKey+"/"+epName]; isAsync {
+ idents["flagWait"] = struct{}{}
+ idents["flagWaitTimeout"] = struct{}{}
+ idents["flagWaitInterval"] = struct{}{}
+ flags["wait"] = struct{}{}
+ flags["wait-timeout"] = struct{}{}
+ flags["wait-interval"] = struct{}{}
+ }
+ return idents, flags
+}
+
+// uniquifyParamNames returns params with IdentName populated whenever a
+// param's Go identifier or cobra flag name would otherwise collide with
+// another param earlier in the list or with a reserved generator name. The
+// first occurrence of each colliding pattern keeps IdentName empty (templates
+// fall back to Name); subsequent ones get IdentName set to Name with _2, _3,
+// ... appended. Wire-side serialization always reads from Name and is never
+// mutated. Positional params are not flagged and pass through.
+func uniquifyParamNames(params []spec.Param, reservedIdents, reservedFlags map[string]struct{}) []spec.Param {
+ if len(params) == 0 {
+ return params
+ }
+ usedIdents := map[string]struct{}{}
+ usedFlags := map[string]struct{}{}
+ for k := range reservedIdents {
+ usedIdents[k] = struct{}{}
+ }
+ for k := range reservedFlags {
+ usedFlags[k] = struct{}{}
+ }
+
+ out := make([]spec.Param, len(params))
+ for i, p := range params {
+ if p.Positional {
+ out[i] = p
+ continue
+ }
+ ident := "flag" + toCamel(p.Name)
+ flag := flagName(p.Name)
+ if _, identTaken := usedIdents[ident]; !identTaken {
+ if _, flagTaken := usedFlags[flag]; !flagTaken {
+ usedIdents[ident] = struct{}{}
+ usedFlags[flag] = struct{}{}
+ out[i] = p
+ continue
+ }
+ }
+ for n := 2; ; n++ {
+ candidate := fmt.Sprintf("%s_%d", p.Name, n)
+ ident = "flag" + toCamel(candidate)
+ flag = flagName(candidate)
+ _, identTaken := usedIdents[ident]
+ _, flagTaken := usedFlags[flag]
+ if !identTaken && !flagTaken {
+ p.IdentName = candidate
+ usedIdents[ident] = struct{}{}
+ usedFlags[flag] = struct{}{}
+ out[i] = p
+ break
+ }
+ }
+ }
+ return out
+}
+
+// paramIdent returns the name a Param should use when deriving Go
+// identifiers (via camel) or cobra flag names (via flagName). It is
+// IdentName when populated by the dedup pass and Name otherwise. The
+// resulting string must never be used for wire-side serialization;
+// callers writing URL params, JSON keys, or path substitutions read
+// Name directly.
+func paramIdent(p spec.Param) string {
+ if p.IdentName != "" {
+ return p.IdentName
+ }
+ return p.Name
+}
diff --git a/internal/generator/flag_collision_test.go b/internal/generator/flag_collision_test.go
new file mode 100644
index 00000000..1010d6ac
--- /dev/null
+++ b/internal/generator/flag_collision_test.go
@@ -0,0 +1,222 @@
+package generator
+
+import (
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/mvanhorn/cli-printing-press/internal/spec"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// TestGenerateDeduplicatesCamelCollidingParams covers Case A from issue #275 F-2.
+// Twilio's spec lists StartTime, StartTime>, and StartTime< as distinct query
+// params for date-range filtering. toCamel strips '>' and '<' as non-alphanumeric,
+// so all three would yield Go identifier "StartTime" and the template would emit
+// three `var flagStartTime` declarations in one function — illegal redeclaration.
+func TestGenerateDeduplicatesCamelCollidingParams(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("collide-camel")
+ // Two endpoints so `list` renders to its own file rather than being
+ // consolidated by the single-endpoint promotion path.
+ apiSpec.Resources["calls"] = spec.Resource{
+ Description: "Calls",
+ Endpoints: map[string]spec.Endpoint{
+ "list": {
+ Method: "GET",
+ Path: "/calls",
+ Description: "List calls within a date range",
+ Params: []spec.Param{
+ {Name: "StartTime", Type: "string", Description: "Exact timestamp"},
+ {Name: "StartTime>", Type: "string", Description: "After timestamp"},
+ {Name: "StartTime<", Type: "string", Description: "Before timestamp"},
+ },
+ },
+ "get": {
+ Method: "GET",
+ Path: "/calls/{id}",
+ Description: "Get one call",
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "collide-camel-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ flagVars, flagBindings := parseFlagDeclarations(t,
+ filepath.Join(outputDir, "internal", "cli", "calls_list.go"))
+
+ assertNoDuplicates(t, flagVars,
+ "each param must produce a distinct Go identifier")
+ assertNoDuplicates(t, flagBindings,
+ "each param must register a distinct cobra flag name")
+ require.Len(t, flagVars, 3,
+ "all three params must still be represented after dedup")
+}
+
+// TestGenerateRenamesParamCollidingWithPaginationAll covers Case B from issue #275 F-2.
+// GitHub's spec has a `repos_notifications_activity-list-repo-for-authenticated-user`
+// endpoint that takes an `all` param and is paginated. The endpoint template emits
+// `var flagAll` once for the user-defined `all` param and again for pagination's
+// "fetch all pages" flag — illegal redeclaration.
+func TestGenerateRenamesParamCollidingWithPaginationAll(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("collide-all")
+ apiSpec.Resources["notifications"] = spec.Resource{
+ Description: "Notifications",
+ Endpoints: map[string]spec.Endpoint{
+ "list": {
+ Method: "GET",
+ Path: "/notifications",
+ Description: "List notifications",
+ Params: []spec.Param{
+ {Name: "all", Type: "bool", Description: "Include read notifications"},
+ },
+ Pagination: &spec.Pagination{
+ Type: "page_token",
+ LimitParam: "per_page",
+ CursorParam: "page",
+ NextCursorPath: "next",
+ HasMoreField: "has_more",
+ },
+ },
+ "get": {
+ Method: "GET",
+ Path: "/notifications/{id}",
+ Description: "Get one notification",
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "collide-all-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ flagVars, flagBindings := parseFlagDeclarations(t,
+ filepath.Join(outputDir, "internal", "cli", "notifications_list.go"))
+
+ assertNoDuplicates(t, flagVars,
+ "pagination's reserved flagAll must not collide with a user param named 'all'")
+ assertNoDuplicates(t, flagBindings,
+ "--all from pagination must not collide with --all from a user param")
+ assert.Contains(t, flagVars, "flagAll",
+ "pagination's flagAll keeps the canonical name")
+}
+
+// TestGenerateRenamesParamCollidingWithAsyncWait covers the async-reserved-name
+// path. Async-job endpoints emit `var flagWait`, `var flagWaitTimeout`, and
+// `var flagWaitInterval` from the IsAsync branch in command_endpoint.go.tmpl;
+// a user param literally named `wait` (or `wait_timeout`, `wait_interval`)
+// would otherwise produce a duplicate `var flagWait` in the same function.
+//
+// Async detection requires a job-id-shaped response field plus a sibling status
+// endpoint, so the spec mirrors that contract.
+func TestGenerateRenamesParamCollidingWithAsyncWait(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("collide-async")
+ apiSpec.Types = map[string]spec.TypeDef{
+ "JobResp": {Fields: []spec.TypeField{
+ {Name: "job_id", Type: "string"},
+ {Name: "status", Type: "string"},
+ }},
+ }
+ apiSpec.Resources["videos"] = spec.Resource{
+ Description: "Videos",
+ Endpoints: map[string]spec.Endpoint{
+ "create": {
+ Method: "POST",
+ Path: "/videos",
+ Description: "Create a video render job",
+ Response: spec.ResponseDef{Type: "object", Item: "JobResp"},
+ Params: []spec.Param{
+ {Name: "wait", Type: "string", Description: "Watermark text on the rendered video"},
+ },
+ },
+ "get": {
+ Method: "GET",
+ Path: "/videos/{id}",
+ Description: "Get one video",
+ Response: spec.ResponseDef{Type: "object", Item: "JobResp"},
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "collide-async-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ flagVars, flagBindings := parseFlagDeclarations(t,
+ filepath.Join(outputDir, "internal", "cli", "videos_create.go"))
+
+ assertNoDuplicates(t, flagVars,
+ "async's reserved flagWait must not collide with a user param named 'wait'")
+ assertNoDuplicates(t, flagBindings,
+ "--wait from async must not collide with --wait from a user param")
+ assert.Contains(t, flagVars, "flagWait",
+ "async's flagWait keeps the canonical name")
+}
+
+// parseFlagDeclarations returns the names of all `var flagXxx` declarations and
+// the literal flag names passed to cobra's *Var registrations.
+func parseFlagDeclarations(t *testing.T, path string) (vars, bindings []string) {
+ t.Helper()
+ src, err := os.ReadFile(path)
+ require.NoError(t, err, "read generated file")
+
+ fset := token.NewFileSet()
+ file, err := parser.ParseFile(fset, path, src, 0)
+ require.NoError(t, err, "generated file must parse as Go")
+
+ ast.Inspect(file, func(n ast.Node) bool {
+ switch decl := n.(type) {
+ case *ast.GenDecl:
+ if decl.Tok != token.VAR {
+ return true
+ }
+ for _, sp := range decl.Specs {
+ vs, ok := sp.(*ast.ValueSpec)
+ if !ok {
+ continue
+ }
+ for _, name := range vs.Names {
+ if strings.HasPrefix(name.Name, "flag") {
+ vars = append(vars, name.Name)
+ }
+ }
+ }
+ case *ast.CallExpr:
+ // cobra registrations: cmd.Flags().StringVar(&flagX, "name", ...)
+ sel, ok := decl.Fun.(*ast.SelectorExpr)
+ if !ok || !strings.HasSuffix(sel.Sel.Name, "Var") {
+ return true
+ }
+ if len(decl.Args) < 2 {
+ return true
+ }
+ lit, ok := decl.Args[1].(*ast.BasicLit)
+ if !ok || lit.Kind != token.STRING {
+ return true
+ }
+ bindings = append(bindings, strings.Trim(lit.Value, `"`))
+ }
+ return true
+ })
+ return vars, bindings
+}
+
+func assertNoDuplicates(t *testing.T, names []string, msg string) {
+ t.Helper()
+ seen := map[string]int{}
+ for _, n := range names {
+ seen[n]++
+ }
+ for n, count := range seen {
+ assert.Equal(t, 1, count, "%s: %q appears %d times", msg, n, count)
+ }
+}
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 456000fa..25645401 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -198,6 +198,7 @@ func New(s *spec.APISpec, outputDir string) *Generator {
"mcpDescription": mcpDescription,
"mcpDescriptionRich": mcpDescriptionRich,
"flagName": flagName,
+ "paramIdent": paramIdent,
"safeTypeName": safeTypeName,
"hasNonScalarType": func(types map[string]spec.TypeDef) bool {
for _, td := range types {
@@ -947,6 +948,12 @@ func (g *Generator) Generate() error {
g.AsyncJobs = DetectAsyncJobs(g.Spec)
}
+ // Suffix any param whose Go identifier or cobra flag name would collide
+ // with another param on the same endpoint or with a generator-introduced
+ // reserved name (pagination's flagAll, async's flagWait*). Must run after
+ // AsyncJobs detection so async endpoints reserve the wait identifiers.
+ g.dedupeFlagIdentifiers()
+
// Generate single files
singleFiles := map[string]string{
"main.go.tmpl": filepath.Join("cmd", naming.CLI(g.Spec.Name), "main.go"),
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index 642d8ab4..3af167a4 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -23,11 +23,11 @@ import (
func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra.Command {
{{- range .Endpoint.Params}}
{{- if not .Positional}}
- var flag{{camel .Name}} {{goTypeForParam .Name .Type}}
+ var flag{{camel (paramIdent .)}} {{goTypeForParam .Name .Type}}
{{- end}}
{{- end}}
{{- range .Endpoint.Body}}
- var body{{camel .Name}} {{goType .Type}}
+ var body{{camel (paramIdent .)}} {{goType .Type}}
{{- end}}
{{- if .Endpoint.Pagination}}
var flagAll bool
@@ -56,24 +56,24 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
{{- end}}
{{- range .Endpoint.Params}}
{{- if and .Required (not .Positional) (not .Default)}}
- if !cmd.Flags().Changed("{{flagName .Name}}") && !flags.dryRun {
- return fmt.Errorf("required flag \"%s\" not set", "{{flagName .Name}}")
+ if !cmd.Flags().Changed("{{flagName (paramIdent .)}}") && !flags.dryRun {
+ return fmt.Errorf("required flag \"%s\" not set", "{{flagName (paramIdent .)}}")
}
{{- end}}
{{- end}}
{{- range .Endpoint.Params}}
{{- if and .Enum (not .Positional) (eq .Type "string")}}
- if cmd.Flags().Changed("{{flagName .Name}}") {
- allowed{{camel .Name}} := []string{ {{enumLiteral .Enum}} }
- valid{{camel .Name}} := false
- for _, v := range allowed{{camel .Name}} {
- if flag{{camel .Name}} == v {
- valid{{camel .Name}} = true
+ if cmd.Flags().Changed("{{flagName (paramIdent .)}}") {
+ allowed{{camel (paramIdent .)}} := []string{ {{enumLiteral .Enum}} }
+ valid{{camel (paramIdent .)}} := false
+ for _, v := range allowed{{camel (paramIdent .)}} {
+ if flag{{camel (paramIdent .)}} == v {
+ valid{{camel (paramIdent .)}} = true
break
}
}
- if !valid{{camel .Name}} {
- fmt.Fprintf(os.Stderr, "warning: --%s %q not in allowed set %v\n", "{{flagName .Name}}", flag{{camel .Name}}, allowed{{camel .Name}})
+ if !valid{{camel (paramIdent .)}} {
+ fmt.Fprintf(os.Stderr, "warning: --%s %q not in allowed set %v\n", "{{flagName (paramIdent .)}}", flag{{camel (paramIdent .)}}, allowed{{camel (paramIdent .)}})
}
}
{{- end}}
@@ -81,17 +81,17 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
{{- range .Endpoint.Params}}
{{- if and (not .Positional) (jsonStringParam .)}}
{{- $param := .}}
- if cmd.Flags().Changed("{{flagName $param.Name}}") {
- var parsed{{camel $param.Name}} any
- if err := json.Unmarshal([]byte(flag{{camel $param.Name}}), &parsed{{camel $param.Name}}); err != nil {
+ if cmd.Flags().Changed("{{flagName (paramIdent $param)}}") {
+ var parsed{{camel (paramIdent $param)}} any
+ if err := json.Unmarshal([]byte(flag{{camel (paramIdent $param)}}), &parsed{{camel (paramIdent $param)}}); err != nil {
{{- with jsonEnumSuggestion $param $.Endpoint.Params}}
for _, v := range []string{ {{enumLiteral .Values}} } {
- if flag{{camel $param.Name}} == v {
- return fmt.Errorf("--{{flagName $param.Name}} must be valid JSON. Did you mean --{{.FlagName}} %s?", flag{{camel $param.Name}})
+ if flag{{camel (paramIdent $param)}} == v {
+ return fmt.Errorf("--{{flagName (paramIdent $param)}} must be valid JSON. Did you mean --{{.FlagName}} %s?", flag{{camel (paramIdent $param)}})
}
}
{{- end}}
- return fmt.Errorf("--{{flagName $param.Name}} must be valid JSON: %w", err)
+ return fmt.Errorf("--{{flagName (paramIdent $param)}} must be valid JSON: %w", err)
}
}
{{- end}}
@@ -100,8 +100,8 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
if !stdinBody {
{{- range .Endpoint.Body}}
{{- if and .Required (not .Default)}}
- if !cmd.Flags().Changed("{{flagName .Name}}") && !flags.dryRun {
- return fmt.Errorf("required flag \"%s\" not set", "{{flagName .Name}}")
+ if !cmd.Flags().Changed("{{flagName (paramIdent .)}}") && !flags.dryRun {
+ return fmt.Errorf("required flag \"%s\" not set", "{{flagName (paramIdent .)}}")
}
{{- end}}
{{- end}}
@@ -127,7 +127,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
{{- end}}
{{- range .Endpoint.Params}}
{{- if and .PathParam (not .Positional)}}
- path = replacePathParam(path, "{{.Name}}", fmt.Sprintf("%v", flag{{camel .Name}}))
+ path = replacePathParam(path, "{{.Name}}", fmt.Sprintf("%v", flag{{camel (paramIdent .)}}))
{{- end}}
{{- end}}
@@ -138,8 +138,8 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
htmlRequestParams["{{.Name}}"] = args[{{$i}}]
{{- end}}
{{- if and (not .Positional) (not .PathParam)}}
- if flag{{camel .Name}} != {{zeroValForParam .Name .Type}} {
- htmlRequestParams["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel .Name}})
+ if flag{{camel (paramIdent .)}} != {{zeroValForParam .Name .Type}} {
+ htmlRequestParams["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel (paramIdent .)}})
}
{{- end}}
{{- end}}
@@ -162,7 +162,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
"{{.Name}}": args[{{$i}}],
{{- end}}
{{- if and (not .Positional) (not .PathParam)}}
- "{{.Name}}": fmt.Sprintf("%v", flag{{camel .Name}}),
+ "{{.Name}}": fmt.Sprintf("%v", flag{{camel (paramIdent .)}}),
{{- end}}
{{- end}}
}, flagAll, "{{.Endpoint.Pagination.CursorParam}}", "{{.Endpoint.Pagination.NextCursorPath}}", "{{.Endpoint.Pagination.HasMoreField}}")
@@ -173,7 +173,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
"{{.Name}}": args[{{$i}}],
{{- end}}
{{- if and (not .Positional) (not .PathParam)}}
- "{{.Name}}": fmt.Sprintf("%v", flag{{camel .Name}}),
+ "{{.Name}}": fmt.Sprintf("%v", flag{{camel (paramIdent .)}}),
{{- end}}
{{- end}}
}, flagAll, "{{.Endpoint.Pagination.CursorParam}}", "{{.Endpoint.Pagination.NextCursorPath}}", "{{.Endpoint.Pagination.HasMoreField}}")
@@ -185,8 +185,8 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
params["{{.Name}}"] = args[{{$i}}]
{{- end}}
{{- if and (not .Positional) (not .PathParam)}}
- if flag{{camel .Name}} != {{zeroValForParam .Name .Type}} {
- params["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel .Name}})
+ if flag{{camel (paramIdent .)}} != {{zeroValForParam .Name .Type}} {
+ params["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel (paramIdent .)}})
}
{{- end}}
{{- end}}
@@ -216,24 +216,24 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
body = map[string]any{}
{{- range .Endpoint.Body}}
{{- if or (eq .Type "object") (eq .Type "array")}}
- if body{{camel .Name}} != "" {
- var parsed{{camel .Name}} any
- if err := json.Unmarshal([]byte(body{{camel .Name}}), &parsed{{camel .Name}}); err != nil {
- return fmt.Errorf("parsing --{{flagName .Name}} JSON: %w", err)
+ if body{{camel (paramIdent .)}} != "" {
+ var parsed{{camel (paramIdent .)}} any
+ if err := json.Unmarshal([]byte(body{{camel (paramIdent .)}}), &parsed{{camel (paramIdent .)}}); err != nil {
+ return fmt.Errorf("parsing --{{flagName (paramIdent .)}} JSON: %w", err)
}
- body["{{.Name}}"] = parsed{{camel .Name}}
+ body["{{.Name}}"] = parsed{{camel (paramIdent .)}}
}
{{- else if jsonStringParam .}}
- if body{{camel .Name}} != "" {
- var parsed{{camel .Name}} any
- if err := json.Unmarshal([]byte(body{{camel .Name}}), &parsed{{camel .Name}}); err != nil {
- return fmt.Errorf("parsing --{{flagName .Name}} JSON: %w", err)
+ if body{{camel (paramIdent .)}} != "" {
+ var parsed{{camel (paramIdent .)}} any
+ if err := json.Unmarshal([]byte(body{{camel (paramIdent .)}}), &parsed{{camel (paramIdent .)}}); err != nil {
+ return fmt.Errorf("parsing --{{flagName (paramIdent .)}} JSON: %w", err)
}
- body["{{.Name}}"] = body{{camel .Name}}
+ body["{{.Name}}"] = body{{camel (paramIdent .)}}
}
{{- else}}
- if body{{camel .Name}} != {{zeroVal .Type}} {
- body["{{.Name}}"] = body{{camel .Name}}
+ if body{{camel (paramIdent .)}} != {{zeroVal .Type}} {
+ body["{{.Name}}"] = body{{camel (paramIdent .)}}
}
{{- end}}
{{- end}}
@@ -265,24 +265,24 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
body = map[string]any{}
{{- range .Endpoint.Body}}
{{- if or (eq .Type "object") (eq .Type "array")}}
- if body{{camel .Name}} != "" {
- var parsed{{camel .Name}} any
- if err := json.Unmarshal([]byte(body{{camel .Name}}), &parsed{{camel .Name}}); err != nil {
- return fmt.Errorf("parsing --{{flagName .Name}} JSON: %w", err)
+ if body{{camel (paramIdent .)}} != "" {
+ var parsed{{camel (paramIdent .)}} any
+ if err := json.Unmarshal([]byte(body{{camel (paramIdent .)}}), &parsed{{camel (paramIdent .)}}); err != nil {
+ return fmt.Errorf("parsing --{{flagName (paramIdent .)}} JSON: %w", err)
}
- body["{{.Name}}"] = parsed{{camel .Name}}
+ body["{{.Name}}"] = parsed{{camel (paramIdent .)}}
}
{{- else if jsonStringParam .}}
- if body{{camel .Name}} != "" {
- var parsed{{camel .Name}} any
- if err := json.Unmarshal([]byte(body{{camel .Name}}), &parsed{{camel .Name}}); err != nil {
- return fmt.Errorf("parsing --{{flagName .Name}} JSON: %w", err)
+ if body{{camel (paramIdent .)}} != "" {
+ var parsed{{camel (paramIdent .)}} any
+ if err := json.Unmarshal([]byte(body{{camel (paramIdent .)}}), &parsed{{camel (paramIdent .)}}); err != nil {
+ return fmt.Errorf("parsing --{{flagName (paramIdent .)}} JSON: %w", err)
}
- body["{{.Name}}"] = body{{camel .Name}}
+ body["{{.Name}}"] = body{{camel (paramIdent .)}}
}
{{- else}}
- if body{{camel .Name}} != {{zeroVal .Type}} {
- body["{{.Name}}"] = body{{camel .Name}}
+ if body{{camel (paramIdent .)}} != {{zeroVal .Type}} {
+ body["{{.Name}}"] = body{{camel (paramIdent .)}}
}
{{- end}}
{{- end}}
@@ -308,24 +308,24 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
body = map[string]any{}
{{- range .Endpoint.Body}}
{{- if or (eq .Type "object") (eq .Type "array")}}
- if body{{camel .Name}} != "" {
- var parsed{{camel .Name}} any
- if err := json.Unmarshal([]byte(body{{camel .Name}}), &parsed{{camel .Name}}); err != nil {
- return fmt.Errorf("parsing --{{flagName .Name}} JSON: %w", err)
+ if body{{camel (paramIdent .)}} != "" {
+ var parsed{{camel (paramIdent .)}} any
+ if err := json.Unmarshal([]byte(body{{camel (paramIdent .)}}), &parsed{{camel (paramIdent .)}}); err != nil {
+ return fmt.Errorf("parsing --{{flagName (paramIdent .)}} JSON: %w", err)
}
- body["{{.Name}}"] = parsed{{camel .Name}}
+ body["{{.Name}}"] = parsed{{camel (paramIdent .)}}
}
{{- else if jsonStringParam .}}
- if body{{camel .Name}} != "" {
- var parsed{{camel .Name}} any
- if err := json.Unmarshal([]byte(body{{camel .Name}}), &parsed{{camel .Name}}); err != nil {
- return fmt.Errorf("parsing --{{flagName .Name}} JSON: %w", err)
+ if body{{camel (paramIdent .)}} != "" {
+ var parsed{{camel (paramIdent .)}} any
+ if err := json.Unmarshal([]byte(body{{camel (paramIdent .)}}), &parsed{{camel (paramIdent .)}}); err != nil {
+ return fmt.Errorf("parsing --{{flagName (paramIdent .)}} JSON: %w", err)
}
- body["{{.Name}}"] = body{{camel .Name}}
+ body["{{.Name}}"] = body{{camel (paramIdent .)}}
}
{{- else}}
- if body{{camel .Name}} != {{zeroVal .Type}} {
- body["{{.Name}}"] = body{{camel .Name}}
+ if body{{camel (paramIdent .)}} != {{zeroVal .Type}} {
+ body["{{.Name}}"] = body{{camel (paramIdent .)}}
}
{{- end}}
{{- end}}
@@ -512,11 +512,11 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
{{- range .Endpoint.Params}}
{{- if not .Positional}}
- cmd.Flags().{{cobraFlagFuncForParam .Name .Type}}(&flag{{camel .Name}}, "{{flagName .Name}}", {{defaultValForParam .}}, "{{oneline .Description}}{{enumDescriptionHint .Enum}}")
+ cmd.Flags().{{cobraFlagFuncForParam .Name .Type}}(&flag{{camel (paramIdent .)}}, "{{flagName (paramIdent .)}}", {{defaultValForParam .}}, "{{oneline .Description}}{{enumDescriptionHint .Enum}}")
{{- end}}
{{- end}}
{{- range .Endpoint.Body}}
- cmd.Flags().{{cobraFlagFunc .Type}}(&body{{camel .Name}}, "{{flagName .Name}}", {{defaultVal .}}, "{{oneline .Description}}")
+ cmd.Flags().{{cobraFlagFunc .Type}}(&body{{camel (paramIdent .)}}, "{{flagName (paramIdent .)}}", {{defaultVal .}}, "{{oneline .Description}}")
{{- end}}
{{- if .Endpoint.Pagination}}
cmd.Flags().BoolVar(&flagAll, "all", false, "Fetch all pages")
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index a392f767..60c56691 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -14,7 +14,7 @@ import (
func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
{{- range .Endpoint.Params}}
{{- if not .Positional}}
- var flag{{camel .Name}} {{goTypeForParam .Name .Type}}
+ var flag{{camel (paramIdent .)}} {{goTypeForParam .Name .Type}}
{{- end}}
{{- end}}
{{- if .Endpoint.Pagination}}
@@ -29,24 +29,24 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
{{- range .Endpoint.Params}}
{{- if and .Required (not .Positional) (not .Default)}}
- if !cmd.Flags().Changed("{{flagName .Name}}") && !flags.dryRun {
- return fmt.Errorf("required flag \"%s\" not set", "{{flagName .Name}}")
+ if !cmd.Flags().Changed("{{flagName (paramIdent .)}}") && !flags.dryRun {
+ return fmt.Errorf("required flag \"%s\" not set", "{{flagName (paramIdent .)}}")
}
{{- end}}
{{- end}}
{{- range .Endpoint.Params}}
{{- if and .Enum (not .Positional) (eq .Type "string")}}
- if cmd.Flags().Changed("{{flagName .Name}}") {
- allowed{{camel .Name}} := []string{ {{enumLiteral .Enum}} }
- valid{{camel .Name}} := false
- for _, v := range allowed{{camel .Name}} {
- if flag{{camel .Name}} == v {
- valid{{camel .Name}} = true
+ if cmd.Flags().Changed("{{flagName (paramIdent .)}}") {
+ allowed{{camel (paramIdent .)}} := []string{ {{enumLiteral .Enum}} }
+ valid{{camel (paramIdent .)}} := false
+ for _, v := range allowed{{camel (paramIdent .)}} {
+ if flag{{camel (paramIdent .)}} == v {
+ valid{{camel (paramIdent .)}} = true
break
}
}
- if !valid{{camel .Name}} {
- fmt.Fprintf(os.Stderr, "warning: --%s %q not in allowed set %v\n", "{{flagName .Name}}", flag{{camel .Name}}, allowed{{camel .Name}})
+ if !valid{{camel (paramIdent .)}} {
+ fmt.Fprintf(os.Stderr, "warning: --%s %q not in allowed set %v\n", "{{flagName (paramIdent .)}}", flag{{camel (paramIdent .)}}, allowed{{camel (paramIdent .)}})
}
}
{{- end}}
@@ -54,17 +54,17 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
{{- range .Endpoint.Params}}
{{- if and (not .Positional) (jsonStringParam .)}}
{{- $param := .}}
- if cmd.Flags().Changed("{{flagName $param.Name}}") {
- var parsed{{camel $param.Name}} any
- if err := json.Unmarshal([]byte(flag{{camel $param.Name}}), &parsed{{camel $param.Name}}); err != nil {
+ if cmd.Flags().Changed("{{flagName (paramIdent $param)}}") {
+ var parsed{{camel (paramIdent $param)}} any
+ if err := json.Unmarshal([]byte(flag{{camel (paramIdent $param)}}), &parsed{{camel (paramIdent $param)}}); err != nil {
{{- with jsonEnumSuggestion $param $.Endpoint.Params}}
for _, v := range []string{ {{enumLiteral .Values}} } {
- if flag{{camel $param.Name}} == v {
- return fmt.Errorf("--{{flagName $param.Name}} must be valid JSON. Did you mean --{{.FlagName}} %s?", flag{{camel $param.Name}})
+ if flag{{camel (paramIdent $param)}} == v {
+ return fmt.Errorf("--{{flagName (paramIdent $param)}} must be valid JSON. Did you mean --{{.FlagName}} %s?", flag{{camel (paramIdent $param)}})
}
}
{{- end}}
- return fmt.Errorf("--{{flagName $param.Name}} must be valid JSON: %w", err)
+ return fmt.Errorf("--{{flagName (paramIdent $param)}} must be valid JSON: %w", err)
}
}
{{- end}}
@@ -93,8 +93,8 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
htmlRequestParams["{{.Name}}"] = args[{{$i}}]
{{- end}}
{{- if not .Positional}}
- if flag{{camel .Name}} != {{zeroValForParam .Name .Type}} {
- htmlRequestParams["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel .Name}})
+ if flag{{camel (paramIdent .)}} != {{zeroValForParam .Name .Type}} {
+ htmlRequestParams["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel (paramIdent .)}})
}
{{- end}}
{{- end}}
@@ -108,7 +108,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
"{{.Name}}": args[{{$i}}],
{{- end}}
{{- if not .Positional}}
- "{{.Name}}": fmt.Sprintf("%v", flag{{camel .Name}}),
+ "{{.Name}}": fmt.Sprintf("%v", flag{{camel (paramIdent .)}}),
{{- end}}
{{- end}}
}, flagAll, "{{.Endpoint.Pagination.CursorParam}}", "{{.Endpoint.Pagination.NextCursorPath}}", "{{.Endpoint.Pagination.HasMoreField}}")
@@ -119,7 +119,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
"{{.Name}}": args[{{$i}}],
{{- end}}
{{- if not .Positional}}
- "{{.Name}}": fmt.Sprintf("%v", flag{{camel .Name}}),
+ "{{.Name}}": fmt.Sprintf("%v", flag{{camel (paramIdent .)}}),
{{- end}}
{{- end}}
}, flagAll, "{{.Endpoint.Pagination.CursorParam}}", "{{.Endpoint.Pagination.NextCursorPath}}", "{{.Endpoint.Pagination.HasMoreField}}")
@@ -131,8 +131,8 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
params["{{.Name}}"] = args[{{$i}}]
{{- end}}
{{- if not .Positional}}
- if flag{{camel .Name}} != {{zeroValForParam .Name .Type}} {
- params["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel .Name}})
+ if flag{{camel (paramIdent .)}} != {{zeroValForParam .Name .Type}} {
+ params["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel (paramIdent .)}})
}
{{- end}}
{{- end}}
@@ -218,7 +218,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
{{- range .Endpoint.Params}}
{{- if not .Positional}}
- cmd.Flags().{{cobraFlagFuncForParam .Name .Type}}(&flag{{camel .Name}}, "{{flagName .Name}}", {{defaultValForParam .}}, "{{oneline .Description}}{{enumDescriptionHint .Enum}}")
+ cmd.Flags().{{cobraFlagFuncForParam .Name .Type}}(&flag{{camel (paramIdent .)}}, "{{flagName (paramIdent .)}}", {{defaultValForParam .}}, "{{oneline .Description}}{{enumDescriptionHint .Enum}}")
{{- end}}
{{- end}}
{{- if .Endpoint.Pagination}}
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index 58a48f65..bd977e63 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -394,6 +394,15 @@ type Param struct {
Fields []Param `yaml:"fields" json:"fields"` // for nested objects
Enum []string `yaml:"enum,omitempty" json:"enum,omitempty"` // enum constraints for the parameter
Format string `yaml:"format,omitempty" json:"format,omitempty"` // OpenAPI format hints (date-time, email, uri, etc.)
+ // IdentName, when set, overrides Name for Go identifier and CLI flag
+ // derivation (camel/flagName). Name remains the wire-side parameter name
+ // used in URLs, JSON keys, and path substitution. Populated by the
+ // generator's flag-collision dedup pass when two params on the same
+ // endpoint would otherwise produce identical Go identifiers or CLI flag
+ // names — for example Twilio's StartTime/StartTime>/StartTime< all
+ // collapsing to "StartTime" through camelization. Most params leave this
+ // empty and template helpers fall back to Name.
+ IdentName string `yaml:"-" json:"-"`
}
type ResponseDef struct {
← 6bbae936 fix(cli): honor explicit --output flag in generate (#281)
·
back to Cli Printing Press
·
fix(cli): prepend T to type names that match Go reserved wor 1a95a78a →