Generate one exact contract across every Goa output - #3971
Draft
raphael wants to merge 78 commits into
Draft
Conversation
# Conflicts: # grpc/codegen/protobuf.go # grpc/codegen/service_data.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Goa now chooses every generated package name, import, file, validation function, conversion function, and transport helper once for the complete generation run. Service, HTTP, gRPC, JSON-RPC, CLI, OpenAPI, example, and plugin output all use those same recorded choices.
The failure that started this work declared a validation function under one name and called another. Separate generator passes had created separate name scopes for the same Go package. Each pass saw a different set of conflicts, so each answer looked valid by itself while the combined source did not compile. The same split ownership affected union types, imports, transport conversions, examples, plugins, and repeated generation in one process.
codegen/ARCHITECTURE.mdis the full design and upgrade contract. It includes the complete exported-API migration table, generated-source changes, mixed-version behavior, and rollback effects. This description highlights the contracts reviewers should understand first.One generation run owns every answer
A generation run now has one saved plan: a typed record of the prepared design and every decision needed to write source.
Rendering cannot discover a declaration, allocate another name, rebuild transport analysis, or change the design. This keeps work that is known from the design inside generation instead of emitting runtime branches or parsing generator-made names later.
Ownership is explicit:
codegen.Generationowns the prepared roots, generated packages, names, imports, and output paths for one run.service.Planowns service types, endpoints, clients, errors, interceptors, views, unions, validators, and service conversions..protodeclarations, protobuf Go names, presence, metadata, validation, and conversion.Repeated, concurrent, reversed-order, multi-root, multi-transport, and plugin-assisted runs now produce the same complete answer.
Generated contracts that become exact
OneOf unions
Copies of one authored
OneOfshare a declaration only when their emitted definitions match. Separately authored unions stay separate even when their branches happen to be equal. Public union names are exact; a true collision stops generation and asks the design author to give the declarations distinctTypeNamevalues.Compiler-created branch types use names such as
ValueBranchText. HTTP union names describe the body that owns them, for exampleValueRequestBody,ValueStreamingBody,ValueResponseBody, andValueDetailedResponseBody. Relocated unions are written by their owning package inunions.go.Generated branch fields are private. Callers use
New...,Set...,As...,Kind, andValidate. Selecting a branch replaces the previous selection, and a failed JSON decode leaves the prior valid value unchanged.Required unions reject no selection, typed nil wrappers, and selected nil message, bytes, or
Anyvalues. A selected nonnil empty message remains valid. JSON and protobuf union data do not change.Protobuf and gRPC
Required singular booleans, numbers, strings, enums, bytes, and their aliases use proto3 presence. Generated Go scalar fields become pointers; bytes and bytes aliases remain
[]byte;Anyand other messages remain pointers. Goa service fields keep their existing value layout. Protobuf field numbers and binary tags do not change.Generated clients and servers validate protobuf messages before converting them. Omitted required fields now return precise validation errors instead of silently becoming service zero values. Explicit
false,0, empty string, empty bytes, and protobuf null remain valid when supplied.Defaults now follow presence. An absent protobuf input receives its authored default. An explicit zero, empty bytes, or protobuf null remains explicit. Service-to-protobuf conversion never adds defaults; it sends exactly what service code returned.
Each selected gRPC result view has its own conversion and validation. Fields omitted by that view are not required. Dynamic server streams send the selected view before the first message, and clients use it before decoding. Default validators keep names such as
ValidateShowResponse; another view uses a stable name such asValidateShowResponseTiny. Equal validators share one declaration instead of receiving discovery-order suffixes.Repeated and map wrappers no longer claim protobuf can distinguish omitted from empty. Nil map-value wrappers decode as empty collections instead of panicking, while authored length and item rules still run. Validators that provably do nothing are not emitted.
gRPC metadata conversion uses the designed type. Bytes use their actual string contents rather than Go slice display text, floating-point values use the designed width, and response encoders use the real result variable.
Generation checks
protoc-gen-go v1.36.12andprotoc-gen-go-grpc v1.6.2before files are written. These are the exact tools covered by the generated-module tests.HTTP
Incoming JSON arrays declared with
ArrayOfRequireduse pointer elements for primitives and primitive aliases so[null]is rejected. Valid JSON converts to the same service value slices, and outgoing bodies remain value slices.Multipart decoders now fill the generated request body, validate it, and only then build the service payload. Nested validation errors keep their complete field and array-index paths. Exclusive maximum validation rejects the maximum itself and values above it.
A map assigned to the complete query string now reads raw keys such as
?a=1&b=2, matching generated clients. Float query values use Go's shortest round-trip text. Generated clients close bodies they fully consume and return read and close errors; a deliberately returned raw body remains open for the caller.SSE writes primitive values as raw event text, distinguishes an omitted optional value from a present empty string, returns write and flush failures, and decodes retry values into the designed integer type. A variable result view is fixed before the first event; unknown or changing views fail precisely.
An empty successful WebSocket stream now performs the upgrade, sends a normal close frame, and closes once instead of returning before the handshake.
JSON-RPC
JSON-RPC now supports two honest method shapes: one request and response over HTTP, or one request followed by server results over explicit server-sent events. Design validation rejects client streams, bidirectional streams, WebSocket streaming, server streams without
ServerSentEvents(), and methods that define bothResultandStreamingResult.Generated SSE implementations use the transport-independent service stream methods
Send,SendWithContext, andClose. Clients useRecvorRecvWithContext. Each result is a JSON-RPC notification. A request with an ID ends with one terminal response:result: nullfor success or the returned JSON-RPC error. A notification receives no terminal response.Request handling now follows JSON-RPC 2.0 for omitted, null, empty-string, string, and numeric IDs; invalid objects; leading whitespace; empty and mixed batches; notifications; explicit
result: null; invalid parameters; internal failures; acceptable JSON/SSE media types; and rejection of streams inside batches. Clients reject unknown event names and notifications for another method instead of silently skipping them. Body reads, closes, and batch writes return their failures.A caller-selected view uses this method result inside JSON-RPC's standard top-level
resultmember:{ "view": "detailed", "body": { "...": "..." } }This envelope is generated only when the caller chooses among views. Fixed-view and unviewed methods retain their body shape. The envelope is valid generic JSON-RPC; a protocol layered on JSON-RPC, including MCP, must still use that protocol's required result schema.
API errors, interceptors, CLI, examples, and OpenAPI
An API-level error is a reusable definition, not an error returned by every endpoint. A service or method selects it with name-only
Error("busy"), which preserves its type, validation, defaults, description, andTemporary,Timeout, andFaultsettings. Supplying another argument defines a separate local error.Generated interceptor information changes from
*LoggingInfo, a pointer to a public struct with private fields, to the read-onlyLoggingInfointerface with the same public accessors. Goa emits a private implementation specialized for the exact method and call kind, so payload, result, send, and receive accessors do not inspect method names or switch on runtime types.Generated commands execute the endpoint, receive streams, print values, and return endpoint, stream, output, and close errors. gRPC complete-message flags decode protobuf JSON. Example values belong to the declaration that authored them, so an earlier example cannot consume shared random state and alter a later one.
OpenAPI now emits consistent base64 byte examples, empty security scope arrays rather than JSON null, independent server-variable values, designed descriptions, correctly filtered security definitions and examples, and schemas that match selected views and SSE data.
Plugin compatibility
The released four-argument registration functions,
Genfunc, replaceableGenerators, and the exportedService,Transport,OpenAPI, andExamplefunctions remain available. Released callback ordering and repeated callback names remain supported. Built-in functions join the shared plan; externalGenfuncvalues run after names are final.Common generated-name fields used by existing templates remain, including
MountHandler,HandlerInit, constructors, codecs, validators, multipart helpers, SSE names, WebSocket names, and gRPC names. Plugins that edit ordinary values or files should largely continue to work.A plugin that adds a package declaration or chooses a generated name must use
PluginFactoryand declare it duringPlugin.Plan. A preparation plugin that adds services must attach them to the owning root and callEvaluateAttachedServices. Public helpers that manually ran plugin callbacks or rebuilt private service or transport analysis are removed because they would create a second set of decisions.Several exported planning and template-data structures changed. Some gained declaration records or private state, some no longer compare with
==, and positional literals must become named literals.codegen/ARCHITECTURE.mdlists every changed exported API and its replacement.Upgrade, mixed versions, and rollback
Updating the Goa module does not change an already compiled program. Changes take effect when code is regenerated. There is no persisted-data migration.
Regenerate the entire
gentree together; declarations and callers from different generations are not compatible.goa examplepreserves handwritten starter files, so update those separately.Coordinate client and server deployment and rollback for:
Required protobuf presence keeps the binary schema compatible, but an old client cannot prove that a required zero or empty primitive was present. A new server can reject that omission. Regenerate both sides where required zero or empty values matter.
Other intentional source changes affect direct union field access, interceptor info pointers, multipart decoder signatures, incoming
ArrayOfRequiredtransport literals, protobuf scalar fields, direct calls to removed empty validators or combined conversions, generated command starters, gRPC protobuf-JSON CLI scripts, and JSON-RPC WebSocket APIs.JSON-RPC WebSocket has no compatibility mode. Migrate those methods to unary JSON-RPC over HTTP, explicit JSON-RPC SSE server streaming, ordinary HTTP WebSocket, or gRPC before upgrading.
The detailed table in
codegen/ARCHITECTURE.mdalso covers compact HTTP float text, gRPC metadata text, whole-query maps, exact output-path failures, stricter design validation, OpenAPI snapshot changes, and every exported generator-library change.Review this first
codegen/ARCHITECTURE.mdfor the lifecycle, ownership rules, and complete migration contract.codegen/generation.go,codegen/generated_types.go, andcodegen/generator/plan.gofor run and package ownership.codegen/service/plan.goandcodegen/service/generated_package.gofor service declarations and names.codegen/union.go,codegen/validation.go, and the service/HTTP union templates for exclusive union behavior.grpc/codegen/protobuf_catalog.go,grpc/codegen/service_data.go, andcodegen/go_transform.gofor protobuf presence, defaults, views, and conversions.jsonrpc/types.goandjsonrpc/codegen/{plan,server,client,sse,viewed_result}.gofor protocol and streaming behavior.codegen/generator/plugin_public_integration_test.gofor released plugin compatibility.Validation performed
go test ./...passes on the complete code change, including generated temporary modules and all service, gRPC, HTTP, OpenAPI, and JSON-RPC packages.make lintreports0 issueson the final branch.git diff --checkpasses.origin/v3was used to check the migration table. It reports 109 incompatible declarations, all grouped incodegen/ARCHITECTURE.mdwith their required change.The separate Goa examples repository, external plugin repositories, Goa-AI, and AURA have not yet been regenerated against this branch. This PR is draft until those downstream checks are complete and their results are added here.