Skip to content

feat: add GetX/SetX accessors to generated Go models#160

Open
erikmiller-gusto wants to merge 4 commits into
crossplane:mainfrom
erikmiller-gusto:main
Open

feat: add GetX/SetX accessors to generated Go models#160
erikmiller-gusto wants to merge 4 commits into
crossplane:mainfrom
erikmiller-gusto:main

Conversation

@erikmiller-gusto

@erikmiller-gusto erikmiller-gusto commented Jun 26, 2026

Copy link
Copy Markdown

Add accessor/setter methods to generated Go models

Problem

The Go model generator produces structs that expose only fields:

type XAccountScaffoldSpec struct {
    Parameters *XAccountScaffoldSpecParameters `json:"parameters,omitempty"`
    // ...
}

Because the generated types have no methods, consumers can't abstract over resources with interfaces or write generic code that operates on "any resource with field X". Every field is a pointer (the goRemoveRequired mutator strips required, so oapi-codegen emits everything as optional *T).

What this does

Generates GetX/SetX accessor methods for every field of every generated struct, so consumers can define their own interfaces and satisfy them structurally:

func (o *XAccountScaffold) GetSpec() *XAccountScaffoldSpec  { return o.Spec }
func (o *XAccountScaffold) SetSpec(v *XAccountScaffoldSpec) { o.Spec = v }

// consumer code:
type SpecAccessor interface {
    GetSpec() *v1alpha1.XAccountScaffoldSpec
    SetSpec(*v1alpha1.XAccountScaffoldSpec)
}
var _ SpecAccessor = &v1alpha1.XAccountScaffold{}

Testing

  • Unit test over *string / *[]T / *map[...] / *Struct fields, including alias-skipping.
  • Integration test asserting real CRD models and shared k8s ObjectMeta get accessors.
  • Compile gate: materializes the generated module to disk, adds a consumer that uses an accessor through an interface, and go builds it — proving the output compiles and the goal (interfaces over resources) actually works. Skips gracefully if no Go toolchain is present.

go test ./... → all passing. go build, gofmt, go vet clean.

@erikmiller-gusto
erikmiller-gusto requested review from a team, jcogilvie and tampakrap as code owners June 26, 2026 17:05
@erikmiller-gusto
erikmiller-gusto requested review from phisco and removed request for a team June 26, 2026 17:05
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 56d1a43a-4b30-4380-82a1-d326bc444d29

📥 Commits

Reviewing files that changed from the base of the PR and between 07a44e7 and 231e1a8.

📒 Files selected for processing (1)
  • internal/schemas/generator/go.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/schemas/generator/go.go

📝 Walkthrough

Walkthrough

The generator now supports optional Get<Field> and Set<Field> methods for Go models. Configuration and CLI commands control the feature, while tests cover generation, collision handling, aliases, pointer fields, and compilation.

Changes

Go model accessor feature

Layer / File(s) Summary
Feature flag and generator pipeline
internal/config/config.go, internal/schemas/generator/interface.go, internal/schemas/generator/go.go
The accessor flag is added to configuration, passed through generator options and OpenAPI generation paths, and applied to generated Go outputs.
Accessor emitter
internal/schemas/generator/accessors.go
Go source is parsed to generate getters and setters for eligible struct fields while skipping aliases, embedded fields, and existing method names.
CLI feature wiring
cmd/crossplane/main.go, cmd/crossplane/config/*, cmd/crossplane/function/*, cmd/crossplane/project/*
Configuration is bound at startup, the feature key is supported by config commands, and generation commands pass the flag into schema generator selection.
Accessor validation
internal/schemas/generator/accessors_test.go, cmd/crossplane/function/generate_test.go
Tests verify disabled and enabled generation, generated-model compilation, pointer and alias handling, collision avoidance, and updated command invocation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • crossplane/cli#24: Both changes update the shared generator interface and CLI generator-selection wiring.

Suggested reviewers: tampakrap

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change and stays within the length limit.
Description check ✅ Passed The description is directly related to adding accessors to generated Go models and their tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Breaking Changes ✅ Passed Only additions under cmd/**: a new optional config key/flag and config binding; no public fields/flags were removed or renamed, and no required public field was added.
Feature Gate Requirement ✅ Passed Go accessor generation is feature-gated via cfg.Features.GenerateGoModelAccessors and wired through AllLanguages/WithGoModelAccessors into generation paths.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/schemas/generator/accessors_test.go (1)

111-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Thanks for the thorough coverage here — would you be open to nudging these toward the repo's table-driven conventions?

These tests read well, and I appreciate the compile gate especially. To align with our test conventions, would it be worth giving the case tables a reason field and running them as subtests via t.Run(name, ...), and using cmp.Diff for the type comparisons in TestAddAccessors (e.g. on the want/got method maps)? That tends to give nicer failure output and matches the rest of the suite.

No action needed if you'd rather defer, but flagging since it's a documented convention. As per path instructions: "Enforce table-driven test structure ... args/want pattern, use cmp.Diff ... Check for proper test case naming and reason fields."

Also applies to: 226-282

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/accessors_test.go` around lines 111 - 143, The
tests here should be nudged into the repo’s table-driven style: update the cases
in TestGenerateFromCRDIncludesAccessors and the related TestAddAccessors
coverage to use named subtests with t.Run and include a reason field in each
table entry for clearer intent. For the method-map/type assertions in
TestAddAccessors, switch from direct comparisons to cmp.Diff on the want/got
values to improve failure output and match the suite’s conventions. Use the
existing test function names and method-map helpers as the anchor points when
refactoring.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/schemas/generator/accessors_test.go`:
- Around line 175-224: The compile gate in
TestGeneratedModelsCompileWithAccessors is not hermetic because the generated
module path dev.crossplane.io/models must be resolved externally during go
build. Update the generator that writes the materialized go.mod so the generated
module can resolve locally, ideally by adding a replace directive that points
dev.crossplane.io/models to the on-disk output directory, or otherwise make the
offline CI requirement explicit if that’s the intended mechanism. Use the
TestGeneratedModelsCompileWithAccessors flow and the generated go.mod output
from goGenerator{}.GenerateFromCRD as the places to verify the fix.

In `@internal/schemas/generator/accessors.go`:
- Around line 34-39: `addAccessors`/`writeStructAccessors` currently emit
`GetX`/`SetX` methods without checking for existing methods, which can collide
with `oapi-codegen`-generated accessors or union helpers. Update
`writeStructAccessors` to inspect the target type’s existing method set before
generating each accessor, and skip any `GetX`/`SetX` whose name is already
present on that struct. Keep the check localized around `writeStructAccessors`
so the generator remains defensive against `additionalProperties` and union-type
method names like `GetAdditionalProperties`, `As`, or `From`.

---

Nitpick comments:
In `@internal/schemas/generator/accessors_test.go`:
- Around line 111-143: The tests here should be nudged into the repo’s
table-driven style: update the cases in TestGenerateFromCRDIncludesAccessors and
the related TestAddAccessors coverage to use named subtests with t.Run and
include a reason field in each table entry for clearer intent. For the
method-map/type assertions in TestAddAccessors, switch from direct comparisons
to cmp.Diff on the want/got values to improve failure output and match the
suite’s conventions. Use the existing test function names and method-map helpers
as the anchor points when refactoring.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 412e1a53-fb0f-44e9-977e-1b7d63eed6e7

📥 Commits

Reviewing files that changed from the base of the PR and between d0fecff and de804fc.

📒 Files selected for processing (3)
  • internal/schemas/generator/accessors.go
  • internal/schemas/generator/accessors_test.go
  • internal/schemas/generator/go.go

Comment thread internal/schemas/generator/accessors_test.go
Comment thread internal/schemas/generator/accessors.go
@erikmiller-gusto
erikmiller-gusto force-pushed the main branch 2 times, most recently from fb2efaf to f0c8846 Compare June 26, 2026 19:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/crossplane/main.go`:
- Around line 129-130: The `build`, `run`, and `generate` command entrypoints
currently assume `cfg` is always injected by Kong, which could panic if they are
invoked programmatically or in tests; add a nil check at the start of each `Run`
method in `cmd/crossplane/project/build.go`, `cmd/crossplane/project/run.go`,
and `cmd/crossplane/function/generate.go`, and return a clear error before any
`cfg.Features` access when `cfg` is nil.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e91df577-976e-4c3f-b795-54476785f9e3

📥 Commits

Reviewing files that changed from the base of the PR and between de804fc and f0c8846.

📒 Files selected for processing (12)
  • cmd/crossplane/config/help/config.md
  • cmd/crossplane/config/set.go
  • cmd/crossplane/function/generate.go
  • cmd/crossplane/function/generate_test.go
  • cmd/crossplane/main.go
  • cmd/crossplane/project/build.go
  • cmd/crossplane/project/run.go
  • internal/config/config.go
  • internal/schemas/generator/accessors.go
  • internal/schemas/generator/accessors_test.go
  • internal/schemas/generator/go.go
  • internal/schemas/generator/interface.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/crossplane/config/help/config.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/schemas/generator/accessors.go

Comment thread cmd/crossplane/main.go
Generate pointer-in/pointer-out accessor methods for every field of every
struct in the generated Go models, so consumers can abstract over resources
with interfaces and generics. Folded into generateGo as a single AST
post-process step so it applies uniformly across all generation paths.

Gated behind a new features.generateGoModelAccessors config flag (off by
default), threaded from config through the project build/run and function
generate commands into the Go generator. Skips any GetX/SetX whose name
already exists on a type, so the accessors never collide with methods
oapi-codegen already emits (e.g. union or additionalProperties helpers).

Verified by a compile gate that builds the generated module with a consumer
that uses an accessor through an interface; the gate skips gracefully when
module dependencies cannot be resolved offline.

Signed-off-by: Erik Miller <erik.miller@gusto.com>

@adamwg adamwg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the contribution @erikmiller-gusto!

The core of this looks good to me. Leaving a few thoughts inline. My biggest concern is unifying the approach with #162 if possible, preferably also setting us up nicely to make the additional code generation non-optional in the future with minimal changes.

Comment on lines +22 to +23
Generate GetX/SetX accessor methods on generated Go models (off by default), so
generated resources can be used through interfaces and generics:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems reasonable to disable this by default initially in case some bug in the generation results in broken Go bindings for some cases. I do think we should plan to enable it by default at some point, so that there's only one code path to test. We haven't really defined what the feature lifecycle looks like for the CLI, but maybe for now we can just create an issue to remind us to come back to this a couple releases down the line.

// to avoid colliding with methods oapi-codegen already generated.
func writeStructAccessors(b *strings.Builder, fset *token.FileSet, typeName string, st *ast.StructType, skip map[string]bool) {
for _, field := range st.Fields.List {
// Skip embedded/anonymous fields; generated models don't use them.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we also skip unexported fields? Not sure there actually are any in our generated models, but seems like a good safeguard.

return out
}

func renderRecv(e ast.Expr) string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is effectively the same as receiverTypeName in accessors.go. Any reason to redefine it just for tests? Seems equally likely to have a bug in either definition :-).

Comment thread internal/schemas/generator/go.go Outdated
var generateGoMutex sync.Mutex //nolint:gochecknoglobals // Must be global.

func generateGo(s *spec3.OpenAPI, version string, mutators ...func(*spec3.OpenAPI)) (string, error) {
func generateGo(s *spec3.OpenAPI, version string, accessors bool, mutators ...func(*spec3.OpenAPI)) (string, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it make sense to handle the accessor generation in this PR and the runtime.Object method generation in #162 the same way? I.e., either pass a set of "code mutators" to generateGo and make it responsible for adding both sets of extra methods, or have the calling code generate the additional methods after the fact as in #162.

Totally fair if there's a reason that PR and this PR work differently, I was just surprised by it :-).

Apply accessor generation after Go post-processing at the call sites via a
new applyAccessors helper, rather than folding it into generateGo. This
mirrors the runtime.Object generation approach in crossplane#162 so both features use
the same "post-process the generated code after the fact" pattern, and makes
generateGo's signature identical across both, keeping the two changes easy to
reconcile. Generating accessors after fixK8sTypeNames/removeSelfImports also
means they reference the final type names.

Skip unexported struct fields when emitting accessors: generated models don't
currently have any, but an accessor for one would be useless to external
consumers and could produce oddly-cased method names.

Reuse receiverTypeName from accessors.go in the tests instead of redefining an
equivalent renderRecv helper.

Signed-off-by: Erik Miller <erik.miller@gusto.com>
Applying accessors in each generation loop pushed GenerateFromCRD past the
gocognit limit. Extract the shared-K8s-package loop body into a
generateSharedK8sPackage helper to bring it back under.

Signed-off-by: Erik Miller <erik.miller@gusto.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/schemas/generator/go.go`:
- Around line 1272-1277: Update the error wrapping in both accessor-generation
call sites in internal/schemas/generator/go.go at lines 1272-1277 and 1411-1416,
replacing the generic message with “failed to generate Go model accessors.” Keep
the existing applyAccessors error propagation unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ac59f45c-d434-40ba-b56c-998cb7463bc2

📥 Commits

Reviewing files that changed from the base of the PR and between f0c8846 and 07a44e7.

📒 Files selected for processing (12)
  • cmd/crossplane/config/help/config.md
  • cmd/crossplane/config/set.go
  • cmd/crossplane/function/generate.go
  • cmd/crossplane/function/generate_test.go
  • cmd/crossplane/main.go
  • cmd/crossplane/project/build.go
  • cmd/crossplane/project/run.go
  • internal/config/config.go
  • internal/schemas/generator/accessors.go
  • internal/schemas/generator/accessors_test.go
  • internal/schemas/generator/go.go
  • internal/schemas/generator/interface.go
🚧 Files skipped from review as they are similar to previous changes (10)
  • cmd/crossplane/config/set.go
  • internal/schemas/generator/interface.go
  • internal/config/config.go
  • cmd/crossplane/project/run.go
  • cmd/crossplane/config/help/config.md
  • cmd/crossplane/function/generate_test.go
  • cmd/crossplane/main.go
  • internal/schemas/generator/accessors.go
  • cmd/crossplane/function/generate.go
  • internal/schemas/generator/accessors_test.go

Comment thread internal/schemas/generator/go.go
Reword the wrapped error at both accessor call sites from the generic "failed
to add accessors" to "failed to generate Go model accessors".

Signed-off-by: Erik Miller <erik.miller@gusto.com>
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