Skip to content

feat: call composed modules over their REST binding; documented operations - #38

Closed
antoinetoussaint-byte wants to merge 3 commits into
mainfrom
feat/transcoded-module-calls-and-documented-operations
Closed

antoinetoussaint-byte wants to merge 3 commits into
mainfrom
feat/transcoded-module-calls-and-documented-operations

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

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's google.api.http binding.
    • It resolves 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 consumed prefix. 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.
    • It refuses before sending anything:
      • a method with no binding under the prefix (including a string-prefix-only match such as /v1/thing against /v1/things/...);
      • a method with no binding at all;
      • a multi-segment variable;
      • an empty or dot-segment path value;
      • a request or response of the wrong type;
      • a prefix that is not a clean absolute path.
    • The response is protojson-decoded with unknown fields discarded and bounded by MaxResponseBytes (8 MiB default). A non-2xx answer is a *GatewayError with the status.
  • Gateway.OrgID() is the active org the gateway stamped, and the one ForModule mints in.
  • Gateway.WorkContextPrincipals(ctx) returns the org, owner and current actor that accounts answered a ForModule mint with. It reuses the cached capability. A handler checking a claimed owner no longer mints its own Task with an invented session. Covered by TestWorkContextPrincipalsReportsWhomAccountsIssuedFor.
  • Message[T] / MessageOf encode a generated message as protobuf JSON inside a handler's response. encoding/json would otherwise use Go struct tags.
  • FieldMask / NewFieldMask / MustFieldMask / Apply form 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, InterfaceDocument are 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, and go test ./... pass. The new tests also pass with -race.
  • transcode_test.go registers a dynamic annotated service. It covers:
    • binding selection under the prefix, escaping, query encoding (repeated and nested), and int64 decode with an unknown field present;
    • body *;
    • every refusal, asserting that nothing reached the server;
    • the 403 status kept, the response bound, and the bearer presented.
  • The same file covers FieldMask (nested and repeated pruning, input not modified, unknown or scalar-descent paths refused) and Message[T] JSON.
  • operations_test.go covers descriptor-based definitions (JSON names, self-reference terminating in $ref), refusal of undocumented or double-handled operations, and route registration.

Not verified

  • Against a live gateway. The consuming solution's branch exercises it against the documents module's generated descriptors.
  • No tag or release.

🤖 Generated with Claude Code

…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>
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

Superseded by #39, which contains all three of this PR's commits and is now based on main. Lands with #39.

@antoinetoussaint-byte
antoinetoussaint-byte deleted the feat/transcoded-module-calls-and-documented-operations branch October 25, 2026 17:04
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