[object Object]

← back to Cli Printing Press

fix(cli): hide const-default query flags from --help (#1287)

ee3638b2e846e38c3fa7f23e4de82aac6d781332 · 2026-05-13 11:45:23 -0700 · Trevin Chow

* fix(cli): hide const-default query flags from --help

When an OpenAPI param declares a single-value enum whose only value
matches the declared default, the generator was emitting a visible
--flag for it. Users see and could mis-override a flag whose only valid
value is the default — the canonical case is single-URL routing APIs
(Namecheap-style `Command=namecheap.X.Y`) where every endpoint exposes
a `--command` flag that may only ever take its default.

Adds a paramIsConstDefault helper and threads it through the
per-endpoint and promoted command templates as `isConstDefault`. When
true, the templates emit `_ = cmd.Flags().MarkHidden("<flag>")` right
after the StringVar registration. The flag stays registered so the
wire-side default still flows; only --help visibility changes.

Coverage: TestParamIsConstDefault locks the helper logic across
string, integer, and boolean defaults plus the negative cases
(mismatched default, multi-value enum, no default, no enum).
TestGenerateMarksConstDefaultFlagsHidden runs the real generator
against a fixture spec and asserts MarkHidden emission for both the
per-endpoint (multi-endpoint resource) and promoted (single-endpoint
resource) templates, plus a negative assertion that a multi-value-enum
sibling flag stays visible and the required-check is suppressed for
the hidden const-default flag (Default is set, so the existing
template gate `(not .Default)` already skips it).

Goldens: existing fixtures have no const-default params, so the new
branch is not exercised by the golden suite. The end-to-end generator
test above locks the new behavior more precisely than a golden fixture
would, without conflating this change with unrelated template details.
`scripts/golden.sh verify` passes (17 cases).

Closes #1273

* fix(cli): handle float const-defaults losslessly and note body-flag gap

Addresses two Greptile findings on #1287:

1. fmt.Sprintf("%v", float64(2.0)) yields "2", not "2.0", so a
   heterogeneous spec declaring enum: ["2.0"] alongside default: 2.0
   would silently miss the const-default shape. paramIsConstDefault now
   parses the enum element as a float for float64/float32 defaults and
   compares numerically, so "2", "2.0", and "2.00" all match
   float64(2.0). Adds matching test coverage including a real-fraction
   match and a mismatch negative.

2. paramIsConstDefault is wired into the {{range Endpoint.Params}} loop
   in both command_endpoint.go.tmpl and command_promoted.go.tmpl
   (query/path/header). Body params registered via
   renderFlatBodyFlagReg take a separate code path and currently never
   receive MarkHidden, even when they satisfy the const-default shape.
   The primary use case (query-param routing selectors) is unaffected,
   but document the extension point so a future API surfacing a
   const-default body field has a clear hook.

Refs #1273

Files touched

Diff

commit ee3638b2e846e38c3fa7f23e4de82aac6d781332
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Wed May 13 11:45:23 2026 -0700

    fix(cli): hide const-default query flags from --help (#1287)
    
    * fix(cli): hide const-default query flags from --help
    
    When an OpenAPI param declares a single-value enum whose only value
    matches the declared default, the generator was emitting a visible
    --flag for it. Users see and could mis-override a flag whose only valid
    value is the default — the canonical case is single-URL routing APIs
    (Namecheap-style `Command=namecheap.X.Y`) where every endpoint exposes
    a `--command` flag that may only ever take its default.
    
    Adds a paramIsConstDefault helper and threads it through the
    per-endpoint and promoted command templates as `isConstDefault`. When
    true, the templates emit `_ = cmd.Flags().MarkHidden("<flag>")` right
    after the StringVar registration. The flag stays registered so the
    wire-side default still flows; only --help visibility changes.
    
    Coverage: TestParamIsConstDefault locks the helper logic across
    string, integer, and boolean defaults plus the negative cases
    (mismatched default, multi-value enum, no default, no enum).
    TestGenerateMarksConstDefaultFlagsHidden runs the real generator
    against a fixture spec and asserts MarkHidden emission for both the
    per-endpoint (multi-endpoint resource) and promoted (single-endpoint
    resource) templates, plus a negative assertion that a multi-value-enum
    sibling flag stays visible and the required-check is suppressed for
    the hidden const-default flag (Default is set, so the existing
    template gate `(not .Default)` already skips it).
    
    Goldens: existing fixtures have no const-default params, so the new
    branch is not exercised by the golden suite. The end-to-end generator
    test above locks the new behavior more precisely than a golden fixture
    would, without conflating this change with unrelated template details.
    `scripts/golden.sh verify` passes (17 cases).
    
    Closes #1273
    
    * fix(cli): handle float const-defaults losslessly and note body-flag gap
    
    Addresses two Greptile findings on #1287:
    
    1. fmt.Sprintf("%v", float64(2.0)) yields "2", not "2.0", so a
       heterogeneous spec declaring enum: ["2.0"] alongside default: 2.0
       would silently miss the const-default shape. paramIsConstDefault now
       parses the enum element as a float for float64/float32 defaults and
       compares numerically, so "2", "2.0", and "2.00" all match
       float64(2.0). Adds matching test coverage including a real-fraction
       match and a mismatch negative.
    
    2. paramIsConstDefault is wired into the {{range Endpoint.Params}} loop
       in both command_endpoint.go.tmpl and command_promoted.go.tmpl
       (query/path/header). Body params registered via
       renderFlatBodyFlagReg take a separate code path and currently never
       receive MarkHidden, even when they satisfy the const-default shape.
       The primary use case (query-param routing selectors) is unaffected,
       but document the extension point so a future API surfacing a
       const-default body field has a clear hook.
    
    Refs #1273
---
 internal/generator/const_default_test.go           | 171 +++++++++++++++++++++
 internal/generator/generator.go                    |  40 +++++
 .../generator/templates/command_endpoint.go.tmpl   |   3 +
 .../generator/templates/command_promoted.go.tmpl   |   3 +
 4 files changed, 217 insertions(+)

diff --git a/internal/generator/const_default_test.go b/internal/generator/const_default_test.go
new file mode 100644
index 00000000..5ec8a79a
--- /dev/null
+++ b/internal/generator/const_default_test.go
@@ -0,0 +1,171 @@
+package generator
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/mvanhorn/cli-printing-press/v4/internal/spec"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParamIsConstDefault(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name string
+		p    spec.Param
+		want bool
+	}{
+		{
+			name: "single-value enum with matching string default",
+			p:    spec.Param{Name: "Command", Type: "string", Enum: []string{"namecheap.domains.getList"}, Default: "namecheap.domains.getList"},
+			want: true,
+		},
+		{
+			name: "single-value enum with matching integer default",
+			p:    spec.Param{Name: "version", Type: "integer", Enum: []string{"2"}, Default: 2},
+			want: true,
+		},
+		{
+			name: "single-value enum with matching boolean default",
+			p:    spec.Param{Name: "strict", Type: "boolean", Enum: []string{"true"}, Default: true},
+			want: true,
+		},
+		{
+			name: "single-value enum with matching float default preserving fractional form",
+			p:    spec.Param{Name: "version", Type: "number", Enum: []string{"2.0"}, Default: float64(2.0)},
+			want: true,
+		},
+		{
+			name: "single-value enum with matching float default carrying real fraction",
+			p:    spec.Param{Name: "ratio", Type: "number", Enum: []string{"1.5"}, Default: float64(1.5)},
+			want: true,
+		},
+		{
+			name: "single-value float enum that does not match the default",
+			p:    spec.Param{Name: "ratio", Type: "number", Enum: []string{"1.5"}, Default: float64(2.5)},
+			want: false,
+		},
+		{
+			name: "single-value enum with mismatched default is not const",
+			p:    spec.Param{Name: "Command", Type: "string", Enum: []string{"a"}, Default: "b"},
+			want: false,
+		},
+		{
+			name: "multi-value enum with matching default is not const",
+			p:    spec.Param{Name: "order", Type: "string", Enum: []string{"asc", "desc"}, Default: "asc"},
+			want: false,
+		},
+		{
+			name: "single-value enum with no default is not const",
+			p:    spec.Param{Name: "Command", Type: "string", Enum: []string{"namecheap.x"}},
+			want: false,
+		},
+		{
+			name: "no enum but default is not const",
+			p:    spec.Param{Name: "limit", Type: "integer", Default: 25},
+			want: false,
+		},
+		{
+			name: "no enum and no default is not const",
+			p:    spec.Param{Name: "name", Type: "string"},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			assert.Equal(t, tt.want, paramIsConstDefault(tt.p))
+		})
+	}
+}
+
+// TestGenerateMarksConstDefaultFlagsHidden is the end-to-end check that
+// when an endpoint Param has a single-value enum whose only value equals
+// the default, the generated command registers the flag (so the wire-side
+// default still flows) but marks it hidden so --help does not list a flag
+// whose only valid value is the default. This is the single-URL routing
+// API shape, where an operation is selected via a fixed query param.
+func TestGenerateMarksConstDefaultFlagsHidden(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("const-default")
+	constParam := spec.Param{
+		Name:        "Command",
+		Type:        "string",
+		Description: "Constant operation selector",
+		Required:    true,
+		Enum:        []string{"namecheap.domains.getList"},
+		Default:     "namecheap.domains.getList",
+	}
+	multiEnumParam := spec.Param{
+		Name:        "SortBy",
+		Type:        "string",
+		Description: "Sort field",
+		Enum:        []string{"name", "created"},
+		Default:     "name",
+	}
+
+	apiSpec.Resources["domains"] = spec.Resource{
+		Description: "Manage domains",
+		Endpoints: map[string]spec.Endpoint{
+			"list": {
+				Method:      "GET",
+				Path:        "/",
+				Description: "List domains",
+				Params:      []spec.Param{constParam, multiEnumParam},
+			},
+			"renew": {
+				Method:      "POST",
+				Path:        "/",
+				Description: "Renew a domain",
+				Params:      []spec.Param{constParam},
+			},
+		},
+	}
+	apiSpec.Resources["dns"] = spec.Resource{
+		Description: "Manage DNS",
+		Endpoints: map[string]spec.Endpoint{
+			"list": {
+				Method:      "GET",
+				Path:        "/",
+				Description: "List DNS records",
+				Params:      []spec.Param{constParam},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "const-default-pp-cli")
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	cliDir := filepath.Join(outputDir, "internal", "cli")
+
+	endpointSrc, err := os.ReadFile(filepath.Join(cliDir, "domains_list.go"))
+	require.NoError(t, err)
+	endpointGot := string(endpointSrc)
+
+	require.Contains(t, endpointGot, `cmd.Flags().StringVar(&flagCommand, "command",`,
+		"per-endpoint template: const-default flag must still be registered so its default is sent on the wire")
+	require.Contains(t, endpointGot, `_ = cmd.Flags().MarkHidden("command")`,
+		"per-endpoint template: const-default flag must be marked hidden")
+	require.Contains(t, endpointGot, `cmd.Flags().StringVar(&flagSortBy, "sort-by",`,
+		"per-endpoint template: multi-value-enum flag must remain visible")
+	require.NotContains(t, endpointGot, `_ = cmd.Flags().MarkHidden("sort-by")`,
+		"per-endpoint template: multi-value-enum flag must not be marked hidden")
+	// A required flag with a wired default never needs a runtime "required flag not set"
+	// check — the template gates that emission on `(not .Default)`. Lock that interaction
+	// so a future template edit re-enabling the check for Required:true would not produce
+	// a generated binary that errors at runtime on a hidden const-default flag.
+	require.NotContains(t, endpointGot, `required flag "command" not set`,
+		"per-endpoint template: required-check must be suppressed for const-default (Default is set)")
+
+	promotedSrc, err := os.ReadFile(filepath.Join(cliDir, "promoted_dns.go"))
+	require.NoError(t, err)
+	promotedGot := string(promotedSrc)
+
+	require.Contains(t, promotedGot, `cmd.Flags().StringVar(&flagCommand, "command",`,
+		"promoted template: const-default flag must still be registered so its default is sent on the wire")
+	require.Contains(t, promotedGot, `_ = cmd.Flags().MarkHidden("command")`,
+		"promoted template: const-default flag must be marked hidden")
+}
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 6529c138..6b903c52 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -209,6 +209,7 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 		"cobraFlagFuncForParam": cobraFlagFuncForParam,
 		"defaultVal":            defaultVal,
 		"defaultValForParam":    defaultValForParam,
+		"isConstDefault":        paramIsConstDefault,
 		"zeroVal":               zeroVal,
 		"zeroValForParam": func(name, t string) string {
 			kind := primitiveKind(t)
@@ -3070,6 +3071,38 @@ func defaultValForParam(p spec.Param) string {
 	return defaultVal(p)
 }
 
+// paramIsConstDefault holds for single-value-enum params whose default
+// equals the only enum value. Templates emit MarkHidden for these so
+// --help does not list a flag whose only valid value is the default,
+// while the flag stays registered so the wire-side default still flows.
+// This catches the single-URL routing-selector shape, where an API
+// selects an operation via a fixed query param.
+func paramIsConstDefault(p spec.Param) bool {
+	if len(p.Enum) != 1 || p.Default == nil {
+		return false
+	}
+	// Float defaults round-trip ambiguously under any single string format:
+	// fmt.Sprintf("%v", float64(2.0)) and strconv.FormatFloat with -1
+	// precision both yield "2", which would miss a heterogeneous spec
+	// declaring `enum: ["2.0"]` alongside `default: 2.0`. Parse the enum
+	// element as a float and compare numerically so "2", "2.0", and "2.00"
+	// are all equivalent to float64(2.0).
+	switch v := p.Default.(type) {
+	case float64:
+		if enumF, err := strconv.ParseFloat(p.Enum[0], 64); err == nil {
+			return enumF == v
+		}
+		return false
+	case float32:
+		if enumF, err := strconv.ParseFloat(p.Enum[0], 32); err == nil {
+			return float32(enumF) == v
+		}
+		return false
+	default:
+		return fmt.Sprintf("%v", p.Default) == p.Enum[0]
+	}
+}
+
 type jsonFlagSuggestion struct {
 	FlagName string
 	Values   []string
@@ -3359,6 +3392,13 @@ func renderBodyFlagRegs(b *strings.Builder, body []spec.Param, identPrefix, flag
 	}
 }
 
+// renderFlatBodyFlagReg emits cobra flag registrations for a single body
+// param. Const-default detection via paramIsConstDefault is intentionally
+// not wired through here yet: the primary use case is the query-param
+// routing-selector shape, not body fields. If a future API surfaces a
+// const-default body field, extend the emission here with a MarkHidden
+// line gated by paramIsConstDefault, mirroring the command_endpoint and
+// command_promoted templates.
 func renderFlatBodyFlagReg(b *strings.Builder, p spec.Param, identPrefix, flagPrefix string, topLevel bool) {
 	ident := identPrefix + toCamel(paramIdent(p))
 	flag := joinFlag(flagPrefix, publicFlagName(p))
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index ba4033de..a81a253b 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -760,6 +760,9 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 {{- if not .Positional}}
 {{- $param := .}}
 	cmd.Flags().{{cobraFlagFuncForParam .Name .Type}}(&flag{{camel (paramIdent .)}}, "{{publicFlagName .}}", {{defaultValForParam .}}, "{{oneline .Description}}{{enumDescriptionHint .Enum}}")
+{{- if isConstDefault .}}
+	_ = cmd.Flags().MarkHidden("{{publicFlagName .}}")
+{{- end}}
 {{- range publicFlagAliases $param}}
 	cmd.Flags().{{cobraFlagFuncForParam $param.Name $param.Type}}(&flag{{camel (paramIdent $param)}}, "{{.}}", {{defaultValForParam $param}}, "{{oneline $param.Description}}{{enumDescriptionHint $param.Enum}}")
 	_ = cmd.Flags().MarkHidden("{{.}}")
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index f8857aa6..f0664903 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -304,6 +304,9 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 {{- if not .Positional}}
 {{- $param := .}}
 	cmd.Flags().{{cobraFlagFuncForParam .Name .Type}}(&flag{{camel (paramIdent .)}}, "{{publicFlagName .}}", {{defaultValForParam .}}, "{{oneline .Description}}{{enumDescriptionHint .Enum}}")
+{{- if isConstDefault .}}
+	_ = cmd.Flags().MarkHidden("{{publicFlagName .}}")
+{{- end}}
 {{- range publicFlagAliases $param}}
 	cmd.Flags().{{cobraFlagFuncForParam $param.Name $param.Type}}(&flag{{camel (paramIdent $param)}}, "{{.}}", {{defaultValForParam $param}}, "{{oneline $param.Description}}{{enumDescriptionHint $param.Enum}}")
 	_ = cmd.Flags().MarkHidden("{{.}}")

← b8488ee4 fix(cli): route in:query params to URL on PUT/DELETE/POST/PA  ·  back to Cli Printing Press  ·  fix(cli): raise API error body truncation cap from 200 to 40 513ae01d →