Skip to content

Conversation

@jason-lynch
Copy link
Member

@jason-lynch jason-lynch commented Feb 9, 2026

Summary

This PR addresses a few issues I ran into while trying to use our OpenAPI spec and our generated client code:

  • Goa's views do not generate separate types in either service.go or the OpenAPI spec. This makes it difficult for clients to know which subset of fields will be returned. Our "abbreviated" instance view also excluded some required fields on the base type, which causes errors for any generated client that validates server responses. I fixed this by adding a dedicated DatabaseSummary type. I ended up just using the base Instance type in DatabaseSummary.Instances because it addresses PLAT-426.
  • The generated types in service.go that are used by our client package were missing JSON struct tags. This makes it difficult to write a tool that reads/writes these structs as JSON using the client package, such CLIs or other automated API clients. The simplest solution was to add g.Meta("struct:tag:json", "<struct tag>") calls to every field. In order to make this easier to maintain and less error-prone, I wrote a small tool to make these edits. This tool runs as part of the make -C api generate target.
  • The instance error field was always non-nil, even when it was an empty string. This was just a missing condition where we were populating this field.
  • Some code generators expect a default response on every endpoint. This is best practice, but Goa doesn't have a built-in mechanism to add it. I've added another small plugin that adds default responses to every endpoint.

Changes

  • Replace Goa views with dedicated types
  • Add all instance fields to the list-databases response
  • Add JSON struct tags to all API types
  • Omit empty error string from the instance API responses
  • Add a Goa plugin to add default responses to all endpoints

Testing

To test the new instance fields:

# resync the API client
cp1-api-sync

# list databases
cp1-req list-databases

Notes for Reviewers

The API changes are additive-only, so the client package should be backward compatible with older Control Plane instances.

PLAT-426

@jason-lynch jason-lynch changed the title Feat/api client improvements feat: api client and spec improvements Feb 9, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Warning

Rate limit exceeded

@jason-lynch has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 59 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds a CLI to inject JSON struct tags, applies g.Meta("struct:tag:json", "...") annotations across API design files, introduces DatabaseSummary and updates ListDatabases to return summaries, updates server converters, and runs the tags tool as a pre-generation Makefile step.

Changes

Cohort / File(s) Summary
Build Integration
api/Makefile
Runs go run $(CURDIR)/tags $(CURDIR)/apiv1/design/*.go as a pre-generation step for generate-v1.
Tagging CLI
api/tags/main.go
New main package CLI that parses Goa DSL files and inserts/updates g.Meta("struct:tag:json","..."), adding ,omitempty for non-required fields.
API Design — Global
api/apiv1/design/api.go, api/apiv1/design/version.go
Added JSON tag metadata to APIError and VersionInfo; removed an explicit view use in one service result (g.Result(Database) used).
API Design — Database Surface
api/apiv1/design/database.go
Added DatabaseSummary; extensive struct:tag:json metadata across Database, specs, requests/responses, nested types, examples; many optional fields marked omitempty; ListDatabases switched to summaries.
API Design — Instance & Task
api/apiv1/design/instance.go, api/apiv1/design/task.go
Reworked Instance to per-field attributes with JSON tags; annotated connection/postgres/spock/status types and task types; list responses' tasks annotated and marked required.
API Design — Cluster / Host / Backup / Cancel
api/apiv1/design/cluster.go, api/apiv1/design/host.go, api/apiv1/design/backup.go, api/apiv1/design/cancel.go
Added JSON tag metadata for many exported fields across cluster, host, backup options; added descriptions and JSON tags to cancel payload fields.
Server Implementation
server/internal/api/apiv1/convert.go, server/internal/api/apiv1/post_init_handlers.go
Added instancesToAPI() and databaseToSummaryAPI(); databaseToAPI() uses instancesToAPI(); ListDatabases now populates []*api.DatabaseSummary instead of []*api.Database.
Plugins
api/plugins/add_openapi_default_response.go, api/plugins/disable_client_validation.go
Added plugin to inject default OpenAPI responses referencing APIError; renamed plugin entry to DisableClientValidation.
Changelog
changes/unreleased/*.yaml
Added entries documenting JSON tag generation, instance-field exposure, and ListDatabases change to DatabaseSummary.

Poem

🐇 I nibbled at fields, one by one,
I tucked tags where JSON met the sun,
Summaries lined up, trimmed and neat,
Instances sorted—order sweet,
A tiny hop, the schema's done.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: api client and spec improvements' clearly summarizes the main changes: adding JSON struct tags, creating DatabaseSummary type, and enhancing API compatibility for generated client code.
Description check ✅ Passed The PR description provides a comprehensive summary, detailed changes list, testing instructions, and includes the required changelog entry reference. It covers all major sections of the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/api-client-improvements

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@api/apiv1/design/database.go`:
- Around line 1340-1344: The ListDatabasesResponse currently marks the
"databases" attribute with `json:"databases,omitempty"` which will omit the key
when the slice is empty; update the ListDatabasesResponse definition so the
"databases" attribute does not use `omitempty` (i.e., remove the struct tag on
the databases attribute in the g.Type declaration for ListDatabasesResponse that
references DatabaseSummary) so clients receive {"databases": []} instead of {}
when there are no databases.

In `@server/internal/api/apiv1/convert.go`:
- Line 206: Comment is misleading: update the inline comment above the sort to
reflect the actual keys used (it currently says "Sort by node ID" but the code
sorts by NodeName then ID). Change the comment to clearly state the real sort
order (e.g., "Sort by NodeName, then ID ascending" or "Sort by node name, then
instance ID ascending") so it matches the sort implementation that uses NodeName
and ID.
🧹 Nitpick comments (3)
api/tags/main.go (3)

205-211: selectorMatches doesn't verify the receiver is g.

This matches any <ident>.Type(...) call, not just g.Type(...). In practice the design files are tightly scoped so this is unlikely to cause false positives, but adding a check on sel.X would make the tool more robust if the design files ever import additional packages.

Proposed fix
 func selectorMatches(call *ast.CallExpr, name string) bool {
 	sel, ok := call.Fun.(*ast.SelectorExpr)
 	if !ok {
 		return false
 	}
-	return sel.Sel.Name == name
+	ident, ok := sel.X.(*ast.Ident)
+	if !ok {
+		return false
+	}
+	return ident.Name == "g" && sel.Sel.Name == name
 }

246-255: Prefer strconv.Quote over manual quote wrapping.

The manual prefix/suffix check doesn't handle edge cases (e.g., a value that happens to start with " but isn't properly quoted, or values containing special characters). Since tagValue currently only produces simple strings this is safe today, but strconv.Quote would be strictly correct and simpler.

Proposed fix
 func stringLit(value string) *ast.BasicLit {
-	if !strings.HasPrefix(value, `"`) {
-		value = `"` + value
-	}
-	if !strings.HasSuffix(value, `"`) {
-		value = value + `"`
-	}
-
-	return &ast.BasicLit{Kind: token.STRING, Value: value}
+	return &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(value)}
 }

143-182: Attributes without a config closure are silently skipped.

If an attribute is defined without a FuncLit argument (e.g., g.Attribute("name", g.String)), the loop at lines 155–181 finds no FuncLit to attach the meta tag to. The tool silently produces no tag for that attribute.

This might be intentional (such attributes may not exist in practice), but consider logging a warning so the omission is visible during generation. Otherwise, a new field added without a closure would silently lack a JSON tag.

Reshapes the list-databases response for two main reasons:

- Goa's views do not become separate types in the OpenAPI output, which
  causes problems for every other generated client.
- To add more fields to the instances in the list to reduce the number
  of calls to get instance role and other information for all instances
  managed by the cluster.

PLAT-426
Standardizes the format of `Attribute` calls in the API design so that
we can more easily write tools to modify these calls.
@jason-lynch jason-lynch force-pushed the feat/api-client-improvements branch from 6378384 to 12885ad Compare February 9, 2026 17:22
Goa produces separate structs for the client, server, and programmatic
users. The latter structs do not have JSON tags, which makes it
difficult to use the client libraries in some use-cases, such as CLIs.

In order to make it easier to consistently maintain these tags, I've
added a tool that programmatically edits the API design files to add
tags to every attribute.
We were erroneously including the error field in the instance reponses,
even though it was empty, because it was non-nil. This fixes it so that
we only set error if it's both non-nil and non-empty.
@jason-lynch jason-lynch force-pushed the feat/api-client-improvements branch from 12885ad to 95506b9 Compare February 9, 2026 17:42
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@api/tags/main.go`:
- Around line 79-99: processResultType currently only processes Attributes
blocks and skips top-level g.Attribute/g.ErrorName calls; update it to mirror
processType by also calling extractCall(stmt, "Attribute", "ErrorName") for each
stmt in the func literal body and, when non-nil, invoke processAttributes(fset,
attr, required) (using the same required value from extractRequired(flit)),
returning any error; keep the existing second-pass handling of "Attributes"
blocks so both direct Attribute/ErrorName calls and Attributes(...) blocks are
processed.
🧹 Nitpick comments (2)
api/tags/main.go (1)

64-77: Consider preserving original file permissions.

os.WriteFile with hardcoded 0o644 will overwrite whatever permissions the file originally had. For a codegen tool this is generally fine, but if you want to be safe:

♻️ Suggested improvement
+	info, err := os.Stat(path)
+	if err != nil {
+		return fmt.Errorf("failed to stat file '%s': %w", path, err)
+	}
+
 	// Write to a string first in case of errors
 	var buf strings.Builder
 	err = format.Node(&buf, fset, f)
 	if err != nil {
 		return fmt.Errorf("failed to format file: %w", err)
 	}
 
-	err = os.WriteFile(path, []byte(buf.String()), 0o644)
+	err = os.WriteFile(path, []byte(buf.String()), info.Mode().Perm())
api/apiv1/design/database.go (1)

575-622: DatabaseSummary duplicates most of Database — consider using Goa's Extend DSL.

DatabaseSummary repeats five attributes verbatim from Database (id, tenant_id, created_at, updated_at, state, instances) with identical descriptions, examples, and tags. If any of these fields change (e.g., a new enum value for state), both types must be updated in lockstep.

Goa supports g.Extend(Database) to inherit attributes, after which you could remove spec from the required set or simply not re-declare it. This would reduce duplication and drift risk.

That said, if the team prefers explicit declarations for generated-type clarity, this is fine to keep as-is.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@api/plugins/add_openapi_default_response.go`:
- Around line 58-64: Remove the redundant Required field from the Schema when
setting the default response with a $ref: in
api/plugins/add_openapi_default_response.go locate the assignment to
operation.Responses["default"] creating an openapiv2.Response with a Schema that
has Ref set to "#/definitions/APIError" and delete the Required:
[]string{"name", "message"} entry so the Schema only contains the Ref (keep
Description and Schema structure intact); rely on the APIError definition for
required constraints.
- Around line 51-65: addDefaultV2 and addDefaultV3 assume operation.Responses is
non-nil and will panic on write; add a defensive nil-check in each function: if
operation.Responses is nil, initialize it to an empty map before
checking/assigning entries so writes to operation.Responses["default"] succeed;
update both addDefaultV2 (using openapiv2.Response) and addDefaultV3 (using
openapiv3.Response) to perform this initialization prior to reading or writing
operation.Responses.

@jason-lynch jason-lynch force-pushed the feat/api-client-improvements branch from 5abe16e to 5918fd3 Compare February 9, 2026 20:00
Some code generator features, such as ogen's "convenient error types"
require the `default` response. Including a `default` response is best
practice, but Goa doesn't have a built-in way to specify the default
response. This commit adds another Goa plugin that modifies the OpenAPI
files before they're templated.
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.

1 participant