Skip to content

refactor: introduce self-describing OperationSpec via codegen#1520

Merged
bernerd-stripe merged 12 commits intomasterfrom
bernerd/self-describing-operations
Mar 31, 2026
Merged

refactor: introduce self-describing OperationSpec via codegen#1520
bernerd-stripe merged 12 commits intomasterfrom
bernerd/self-describing-operations

Conversation

@bernerd-stripe
Copy link
Copy Markdown
Contributor

@bernerd-stripe bernerd-stripe commented Mar 27, 2026

Reviewers

r?
cc @stripe/developer-products

Summary

NewOperationCmd previously took seven positional arguments (name, path, verb, propFlags, enumFlags, isPreview, serverURL). Each new piece of per-operation metadata required adding another raw argument and updating every call site. Operation data was also inaccessible after the command was built — the CLI had no way to inspect what an operation does, what parameters it accepts, or what types those parameters have.

This introduces OperationSpec as a single typed struct that carries all operation metadata, passed as one argument to NewOperationCmd. Adding new fields no longer touches the function signature. All generated code is now isolated in pkg/cmd/resources/, cleanly separated from the hand-written command code in pkg/cmd/. The named OperationSpec vars are accessible at runtime, allowing subsequent PRs to power richer help text, validations, and other features using spec data directly.

Codegen now emits a pkg/cmd/resources/ package with two generated files:

  • resources_gen.goAddAllResourcesCmds, the wiring function (unchanged behavior, exported)
  • specs_gen.go — one named OperationSpec variable per API operation

The types are in a new pkg/cmd/resource/spec_data.go: OperationSpec, ParamSpec, EnumSpec.

Other changes:

  • Generated files use the "Code generated by ...; DO NOT EDIT." header so GitHub Linguist auto-collapses them in diffs
  • The generator cleans *_gen.go files before writing, so renamed or removed operations don't leave orphans
  • Template output is deterministic (sorted by var name; previously random map iteration order caused CI failures)
  • pkg/cmd/resources_cmds.go (8 012 lines) is deleted
  • pkg/spec/spec.go parses description from Schema (was silently dropped before)
  • balance_settings added as an explicit case in GetResourceCmdName to prevent the default +s rule from producing the user-visible command balance_settingss
  • Removed dead descByValue map from mergeEnumValues (was built but never read)
  • Removed DenormalizeObject and its denormalizedObject helper, which were no longer called after addDenormalizedParams replaced them in codegen

Note on flag usage strings: All generated flags are currently registered with empty usage strings. WrappedRequestParamsFlagUsages does not render them, so this is inert — not a regression from the prior generated code. A follow-up PR adds ShortDescription to ParamSpec and wires it into help output.

bernerd-stripe and others added 9 commits March 27, 2026 18:25
Replaces the anonymous map-literal approach in resources_cmds.go with
named, typed OperationSpec structs — one per API operation — that are
accessible at runtime for introspection, rich help, or future features
like --dry-run validation and agent-oriented JSON output.

Key changes:

- pkg/cmd/resource/spec_data.go (new): OperationSpec, ParamSpec,
  EnumSpec types. Single source of truth for operation metadata.

- pkg/cmd/resource/operation.go: NewOperationCmd now takes
  *OperationSpec instead of seven positional args (name, path, verb,
  propFlags, enumFlags, isPreview, serverURL). Flag registration now
  iterates spec.Params with full type, description, and enum data.

- pkg/cmd/resources/ (new package, 100% generated): AddAllResourcesCmds
  wires the command tree; ~25 specs_*_gen.go files hold the OperationSpec
  vars organised by API namespace × sub-namespace.

- pkg/cmd/root.go: calls resources.AddAllResourcesCmds(rootCmd, &Config)
  replacing the old unexported addAllResourcesCmds(rootCmd).

- pkg/gen/gen_resources_cmds.go + resources_cmds.go.tpl: generator now
  emits the resources/ package instead of a single resources_cmds.go.
  OperationData carries Summary, IsPreview, VarName, and Params
  (map[string]*resource.ParamSpec) with Description, Required, Format,
  and merged enum/x-stripeEnum values.

- pkg/spec/spec.go: adds Summary to Operation and Description to Schema
  (the latter was already accepted by the parser but silently dropped).

- Deletes pkg/cmd/resources_cmds.go (8 012 lines; replaced by the new
  generated package).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
…degen

Two improvements to the generated resources/ package:

1. Change the file header from "This file is generated; DO NOT EDIT." to
   "Code generated by gen_resources_cmds.go; DO NOT EDIT." — the exact
   pattern GitHub's linguist looks for to auto-collapse generated files in
   PR diffs (the Go spec also documents this as the canonical form).

2. Add cleanGeneratedFiles() to the generator: before writing any new
   output it glob-deletes all *_gen.go files in resources/. This ensures
   that renamed or removed namespaces never leave orphaned spec files
   behind across generator runs. Hand-written files (resources_test.go)
   are unaffected since they don't match the *_gen.go pattern.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
The generateSpecFileContent function was 60 lines of fmt.Sprintf /
strings.Builder calls hand-rolling what text/template already does. This
replaces it with pkg/gen/resource_specs.go.tpl, consistent with how the
wiring file (resources_gen.go) is produced.

Side effects of the switch:
- Params are now emitted in alphabetical key order: text/template ranges
  over maps in sorted key order, whereas the previous builder iterated
  the map in random Go map order. Generated files are now deterministic
  across runs.
- The three template invocations (wiring, spec, stripe-version) now
  share a single templateFuncs FuncMap (ToCamel, quote, upper) instead
  of each constructing its own.
- varEntry fields are exported (VarName, Op) so the template can access
  them directly.
- generateSpecFileContent is deleted.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
The previous implementation built flag descriptions only for enum params
(listing possible values), leaving all other flags with empty usage text.
Replace that dead block with a single assignment so every flag gets the
description already stored on ParamSpec.

Add TestNewOperationCmd_FlagUsageFromDescription to verify the behaviour.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
Map iteration in Go is non-deterministic. generateSpecFiles builds the
[]varEntry slice for each (apiNs, nsName) group by ranging over nested
maps, so the order of `var X = resource.OperationSpec{...}` declarations
varied between runs, causing the CI diff check to fail.

Sort each entries slice by VarName before executing the template so the
generated files are identical regardless of map iteration order. Also
regenerate all specs_*_gen.go files with the now-stable ordering.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
The --dry-run tests added in master used the old positional-arg
signature for NewOperationCmd. Update them to use *OperationSpec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Committed-By-Agent: claude
Replace the 22 per-namespace specs_*_gen.go files with a single
specs_gen.go containing all OperationSpec variables sorted by name.
The per-namespace split was an artifact of how the coordinator was
structured and doesn't add navigational value for pure data declarations.

Also improve resources_test.go: replace the tautological struct
construction test with integration tests that verify generated specs
wire cobra flags correctly, covering POST, GET, and v2 operations, plus
a smoke test for the AddAllResourcesCmds coordinator.

Regenerate from the current OpenAPI spec (2026-03-25.dahlia).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Committed-By-Agent: claude
Remove Description, EnumSpec.Description, and Summary from the
generated spec structs and codegen. None of these fields were read
after being set. Removing them shrinks specs_gen.go by ~14k lines
and keeps the structs honest — only fields that are actually consumed
at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Committed-By-Agent: claude
Remove Description, EnumSpec.Description, and Summary from the
generated spec structs and codegen. None of these fields were read
after being set. Removing them shrinks specs_gen.go by ~14k lines
and keeps the structs honest — only fields that are actually consumed
at runtime.

Also removes Operation.Summary from pkg/spec/spec.go — it was parsed
from the OpenAPI spec but never consumed anywhere.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Committed-By-Agent: claude
@bernerd-stripe bernerd-stripe requested a review from a team as a code owner March 27, 2026 23:33
bernerd-stripe and others added 3 commits March 31, 2026 18:59
- Remove dead `descByValue` map in mergeEnumValues (built but never read)
- Remove `DenormalizeObject` and `denormalizedObject` helpers, which are
  no longer called after addDenormalizedParams replaced them in codegen
- Add `balance_settings` case to GetResourceCmdName to prevent the
  default +s rule from producing the user-visible command "balance_settingss"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Committed-By-Agent: claude
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Committed-By-Agent: claude
Copy link
Copy Markdown
Collaborator

@vcheung-stripe vcheung-stripe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Looks much cleaner and easier to extend.

++ for this:

The named OperationSpec vars are accessible at runtime, allowing subsequent PRs to power richer help text, validations, and other features using spec data directly.

I tested a few commands as a sanity check:

go run cmd/stripe/main.go customers
go run cmd/stripe/main.go customers list
go run cmd/stripe/main.go customers create
go run cmd/stripe/main.go customers create --help
go run cmd/stripe/main.go customers delete cus_UFd6MUb06oRtsX
go run cmd/stripe/main.go events resend evt_123

@bernerd-stripe bernerd-stripe merged commit 5f55525 into master Mar 31, 2026
18 checks passed
@bernerd-stripe bernerd-stripe deleted the bernerd/self-describing-operations branch March 31, 2026 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants