Skip to content

feat(client): generate the Go types and typed clients from the OpenAPI spec - #50

Draft
grubmeshi wants to merge 18 commits into
mainfrom
feature/openapi-spec-hal
Draft

grubmeshi wants to merge 18 commits into
mainfrom
feature/openapi-spec-hal

Conversation

@grubmeshi

@grubmeshi grubmeshi commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

🤖 Written by AI agent

Draft for a first look. It is a prototype of a generator that writes the client's Go types and a thin typed client from meshStack's OpenAPI spec.

Why

The client's DTOs are written by hand today. meshStack now derives its OpenAPI spec from its controllers (meshcloud-internal: meshfed-release#11378), so the spec states every field, its type, whether it is required and in which direction. The generator turns that into Go, so the client follows the API without hand-written copies.

What it does

  • client/openapi/generator is a library. A repository calls it from its own small main.go with a Go config, which selects operations by kind, action and version and can map a schema to its own Go type (for example types.SecretOrAny). The CLI's config is in client/internal/generate, run by go generate.
  • One package per kind and version, named after the kind: client/meshworkspace/v2 is package meshworkspace, with Request, Response and a Client. Types that several packages use go to client/meshobject.
  • Client has one method per action. List returns iter.Seq2[T, error], and pages.ReadAll collects it.
  • A path parameter is named after the key it takes: Get(ctx, uuid uuid.UUID), Get(ctx, name string), with the key's description in the doc comment. A composite key such as {metadata.ownedByWorkspace}.{metadata.name} becomes one parameter per field (workspace, project string), and the client joins them, so a caller passes fields it already has and never builds the identifier format itself.
  • A package of a deprecated API version, and its types and methods, carry a Deprecated: paragraph from the spec's lifecycle data, as does a deprecated operation or schema. staticcheck reports each use.
  • The generated clients call the public client/meshapi (Api, Call, Path, HttpClient), so other repositories can generate their own packages; Config.Runtime names another runtime, and client.Client.HttpClient is what their NewClient takes. client/internal keeps the flat client's helpers.
  • HAL entity models become the plain type with a Links field, with Href and Expand. Page wrappers are read by the generic pager.
  • Required fields are values. Every other field is a pointer with omitzero, so a request can send false where the server default is true. Slices and maps are never pointers.
  • Enums are typed string constants with an enum.Of registry. A discriminated oneOf becomes the discriminator field plus one pointer per variant.

This PR generates meshworkspace/v2, meshbuildingblock/v2 and meshbuildingblockrun/v1, and the workspace, building block and run commands use them. The old flat client types stay, because the Terraform provider imports them.

How to look at it

  • The generated files: client/*/v*/generated.go and client/meshobject/generated.go.
  • The golden test of the generator: client/openapi/generator/testdata.
  • The generator downloads the dev spec by default. The dev site serves the derived spec only after meshfed-release#11378 is deployed, so these files come from that PR's spec: go run ./internal/generate -spec <file> in client/.

Open

  • No tfsdk tags yet; the Terraform provider still uses the flat types.

🤖 Generated with Claude Code

@meshcloud-gh-actions

meshcloud-gh-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Coverage of the acceptance run against the meshStack backend, on a4ad326e2d388eb5612173d799769e3300cccf00.

Scope Coverage
Unit tests 78.0%
Acceptance tests 46.8%
Combined 79.5%
Uncovered functions
client/api_key.go:55: meshApiKeyClient.Create 0.0%
client/api_key.go:59: meshApiKeyClient.Read 0.0%
client/api_key.go:63: meshApiKeyClient.Update 0.0%
client/api_key.go:71: meshApiKeyClient.Delete 0.0%
client/api_key_permissions.go:20: ApiKeyPermissions.AllCodes 0.0%
client/api_key_permissions.go:33: ApiKeyPermissions.WorkspaceCodes 0.0%
client/api_key_permissions.go:295: AllApiKeyPermissions 0.0%
client/api_key_permissions.go:299: WorkspacePermissionCodes 0.0%
client/building_block_definition.go:71: MeshBuildingBlockDefinitionApprovalPolicies.NothingRequiresApproval 0.0%
client/building_block_definition.go:82: DisabledSchedule 0.0%
client/building_block_definition.go:89: MeshBuildingBlockDefinitionSchedule.IsDisabled 0.0%
client/building_block_definition.go:93: MeshBuildingBlockDefinitionSpec.HasNeutralPolicies 0.0%
client/building_block_definition.go:97: MeshBuildingBlockDefinitionSpec.WithNeutralPolicies 0.0%
client/building_block_definition.go:166: meshBuildingBlockDefinitionClient.List 0.0%
client/building_block_definition.go:173: meshBuildingBlockDefinitionClient.Read 0.0%
client/building_block_definition.go:177: meshBuildingBlockDefinitionClient.Create 0.0%
client/building_block_definition.go:181: meshBuildingBlockDefinitionClient.Update 0.0%
client/building_block_definition.go:185: meshBuildingBlockDefinitionClient.Delete 0.0%
client/building_block_definition_version.go:100: MeshBuildingBlockType.TagInputTargets 0.0%
client/building_block_definition_version.go:245: meshBuildingBlockDefinitionVersionClient.Create 0.0%
client/building_block_definition_version.go:254: meshBuildingBlockDefinitionVersionClient.Update 0.0%
client/building_block_definition_version_implementation.go:80: MeshBuildingBlockDefinitionImplementation.InferType 0.0%
client/building_block_definition_version_implementation.go:94: MeshBuildingBlockDefinitionImplementation.MarshalJSON 0.0%
client/building_block_definition_version_implementation.go:107: *MeshBuildingBlockDefinitionImplementation.UnmarshalJSON 0.0%
client/building_block_run.go:76: meshBuildingBlockRunClient.GetLogs 0.0%
... and 151 more

covdata func names a method without its receiver, so an entry can belong to an implementation nothing selects rather than to a function the tests never reached. Open the file and line before reading one as a coverage gap.

@grubmeshi
grubmeshi force-pushed the feature/openapi-spec-hal branch 2 times, most recently from cdd9406 to 8a4ad52 Compare October 23, 2026 11:51
grubmeshi and others added 16 commits October 25, 2026 22:35
The generator downloads the same documents as meshstack api-docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…PI docs

The derived spec refers to its responses and parameters as components and declares
parameters on the path, which the reader missed. Schema now carries allOf, oneOf,
discriminator, enum, nullable, readOnly, writeOnly and additionalProperties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generated clients leave kind and apiVersion out of their types, so Api adds them to a
meshObject body and checks them on the answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A repository configures the generator in Go, with a TypeMapper for what the spec leaves open,
and selects operations by kind, action and version. A schema that only one kind and version
reaches lands in that kind's package, one that several reach in a shared package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hBuildingBlockRun v1

go generate ./client downloads the API docs of develop; -spec names another document. The
TypeMapper gives a building block input value the SecretOrAny the Terraform provider converts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…client

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ated packages

A run lock now counts as unfinished only while the run is IN_PROGRESS: the API docs list
IN_PROGRESS, SUCCEEDED and FAILED for a run, and no PENDING.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt/meshapi

Another module's generated clients cannot import client/internal. Config.Runtime names the
package for a module that brings its own, and client.Client.HttpClient is what their NewClient
takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fter their key

A generated package, type or method of a deprecated version or operation carries a
"Deprecated:" paragraph, which staticcheck reports at each use. A path parameter takes the
name of the key it holds, as Get(ctx, uuid) or Get(ctx, name), and a template key such as
{metadata.ownedByWorkspace}.{metadata.name} takes one parameter per field, which the client
joins. The packages are regenerated from the latest spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ife or as outdated

The "Deprecated:" paragraph of a generated package, type or method reads "removed after
<date>." for a version with an end of life, and "a newer version is available." otherwise.
The API docs no longer carry x-deprecated-description or a "Deprecated:" description prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… schema

A query parameter of format uuid is a uuid.UUID, and an enum is its typed string with
constants: a component's own type, or one the package names after the filter and the
parameter, such as ListFilterSort. Every field is omitzero, so a zero UUID stays out of
the query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ListFilter of meshworkspace/v2, meshbuildingblock/v2 and meshbuildingblockrun/v1 takes
UUIDs as uuid.UUID and enum values as typed constants, such as the sort keys a list accepts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ckRun v1 from the current spec

UUID fields such as definitionUuid, latestRunUuid and the run's metadata.uuid
are uuid.UUID. Building block definition version refs and building block refs
lose name. The target ref is one MeshBuildingBlockTargetRef for both directions,
with required name or uuid. Its kind and the run's implementation type are
string, since the spec now types each variant's discriminator inline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@grubmeshi
grubmeshi force-pushed the feature/openapi-spec-hal branch from 8a4ad52 to 5ac48d7 Compare October 25, 2026 21:37
grubmeshi and others added 2 commits October 26, 2026 13:15
A property or query parameter the spec marks deprecated gets a Go "Deprecated:"
paragraph, so staticcheck reports its use. The text is the API's "**Deprecated:**"
paragraph, of the package's API version for a filter, else "see the API documentation.".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parentBuildingBlocks, the parent refs' buildingBlockUuid and definitionUuid,
status.status and the status filter are deprecated, as is authorType of
meshobject.MeshObjectAuthor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

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