Repository navigation
feat: call composed modules over their REST binding; documented operations - #38
Closed
antoinetoussaint-byte wants to merge 3 commits into
Closed
antoinetoussaint-byte wants to merge 3 commits into
antoinetoussaint-byte wants to merge 3 commits into
Conversation
…tions A solution reaches a composed module only under the REST prefix it federates (/v1/<as>/*), so the module's generated gRPC/Connect client cannot be pointed at the gateway, and solutions were hand-building paths, query strings and wire structs for every module read. Gateway.Transcoded calls an RPC with the module's own generated messages over the method's google.api.http binding under the consumed prefix. Everything about the request comes from the descriptor: the caller names no path, and a method with no binding under the prefix is refused before anything is sent. Alongside it, what a handler needs to hand an owner's message on: Message[T] encodes a generated message as protobuf JSON inside a response, and FieldMask/Apply pass on only a declared allowlist of its fields, so an owner field added later reaches no browser until someone names it. Operation, Server.Operations and InterfaceDocument move the documented-route declaration and its Swagger 2.0 artifact into the runtime, so one list is the source of what a solution serves and what it publishes. A Message[T] field is described from its descriptor. Gateway.OrgID exposes the active organization ForModule mints in, so a handler scopes a module read to the same tenant its capability names. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A handler that must check a claimed owner against the capability it reads with had to mint its own Task Work Context (with an invented session) to see accounts' owner and actor. Gateway.WorkContextPrincipals returns the organization, owner and current actor accounts answered the ForModule mint with, reusing the cached capability. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
*Message[T] carries Message[T]'s methods, so the interface check matched it first and called the descriptor on a nil pointer. Unwrap pointers before it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Contributor
Author
antoinetoussaint-byte
deleted the
feat/transcoded-module-calls-and-documented-operations
branch
October 25, 2026 17:04
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.
What
Additive API, no breaking change:
Gateway.Transcoded(ctx, prefix, method, req, resp, opts...)calls one RPC of a composed module through the gateway, using the module's own generated request and response messages over the RPC'sgoogle.api.httpbinding.method(the generated"/pkg.Service/Method"constant) in the global registry, then picks the first binding (primary, then additional bindings) whose path lies under the consumedprefix. Path variables, query and body all come from the descriptor. Path values are escaped; set, non-path fields go into the query by proto name, or into the body the binding names./v1/thingagainst/v1/things/...);MaxResponseBytes(8 MiB default). A non-2xx answer is a*GatewayErrorwith the status.Gateway.OrgID()is the active org the gateway stamped, and the oneForModulemints in.Gateway.WorkContextPrincipals(ctx)returns the org, owner and current actor that accounts answered aForModulemint with. It reuses the cached capability. A handler checking a claimed owner no longer mints its own Task with an invented session. Covered byTestWorkContextPrincipalsReportsWhomAccountsIssuedFor.Message[T]/MessageOfencode a generated message as protobuf JSON inside a handler's response.encoding/jsonwould otherwise use Go struct tags.FieldMask/NewFieldMask/MustFieldMask/Applyform a declared allowlist over an owner's message. Unknown paths fail at declaration, and a field the owner adds later reaches no browser until someone names it.Operation,QueryParameter,Server.Operations,InterfaceInfo,InterfaceDocumentare the documented-route declaration and its Swagger 2.0 artifact. This is moved from a consuming solution's hand-written generator.Message[T]fields are described from the descriptor, with JSON names.Why
The gateway reaches a composed module only under the REST prefix the solution federates (
/v1/<as>/*). A module's generated gRPC or Connect client therefore cannot be pointed at it. Solutions were hand-building paths, query strings and wire structs for every module read, and hand-writing their own OpenAPI generator. This is the runtime-owner half of moving a consuming solution onto generated clients.Classification
Fix at the owner. The runtime owns "the gateway client a handler uses to read composed modules". It stays module-agnostic: nothing here names a module.
Verified
go vet ./...,gofmt, andgo test ./...pass. The new tests also pass with-race.transcode_test.goregisters a dynamic annotated service. It covers:*;FieldMask(nested and repeated pruning, input not modified, unknown or scalar-descent paths refused) andMessage[T]JSON.operations_test.gocovers descriptor-based definitions (JSON names, self-reference terminating in$ref), refusal of undocumented or double-handled operations, and route registration.Not verified
🤖 Generated with Claude Code