feat: introduce REST-over-gRPC transcoding#546
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a build-time generated REST/JSON façade for existing gRPC services driven by google.api.http annotations, adding a small path-template parser crate, a runtime transcoding layer, a build-time generator, and accompanying examples/benchmarks.
Changes:
- Add
http_path_template(path-template parser),rest_over_grpc(runtime transcoder/adapters), andrest_over_grpc_build(code generator). - Add worked examples (
tonicbridge + custom/prost handler) and integration tests for value types, adapters, streaming, and OpenAPI output. - Add benchmark + correctness harness for the generated router/dispatcher and supporting workspace wiring (deps, spelling, clippy idents).
Reviewed changes
Copilot reviewed 107 out of 108 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Adds new primary-crate entries for REST-over-gRPC components. |
| CHANGELOG.md | Links new crate changelogs from the workspace changelog index. |
| Cargo.toml | Adds workspace dependencies for new crates and supporting libs (prost/tonic/etc.). |
| clippy.toml | Allows additional doc-valid identifiers (e.g., “OpenAPI”). |
| .spelling | Adds domain-specific terms used by the new crates/docs. |
| crates/http_path_template/Cargo.toml | Defines the new http_path_template crate metadata. |
| crates/http_path_template/CHANGELOG.md | Introduces the crate changelog for initial release. |
| crates/http_path_template/README.md | Adds crate README content and examples. |
| crates/http_path_template/favicon.ico | Adds crate branding asset (Git LFS). |
| crates/http_path_template/logo.png | Adds crate branding asset (Git LFS). |
| crates/http_path_template/src/lib.rs | Exposes the parser API and crate-level docs. |
| crates/http_path_template/src/error.rs | Defines ParseError and predicates for parse failures. |
| crates/http_path_template/src/segment.rs | Defines the parsed Segment AST. |
| crates/http_path_template/src/variable.rs | Defines {field.path=subtemplate} variable representation. |
| crates/rest_over_grpc/Cargo.toml | Defines the new rest_over_grpc runtime crate and feature flags. |
| crates/rest_over_grpc/CHANGELOG.md | Introduces the crate changelog for initial release. |
| crates/rest_over_grpc/favicon.ico | Adds crate branding asset (Git LFS). |
| crates/rest_over_grpc/logo.png | Adds crate branding asset (Git LFS). |
| crates/rest_over_grpc/examples/layered_service.rs | Example: serve transcoded REST via layered. |
| crates/rest_over_grpc/examples/tower_service.rs | Example: serve transcoded REST via tower. |
| crates/rest_over_grpc/examples/transcode_http_handler.rs | Example: call transcode_http directly without a Service. |
| crates/rest_over_grpc/src/axum_support.rs | axum integration (IntoResponse) for neutral response types. |
| crates/rest_over_grpc/src/binding.rs | Adds Binding type for captured path-variable bindings. |
| crates/rest_over_grpc/src/route_match.rs | Adds RouteMatch type with inline binding storage optimization. |
| crates/rest_over_grpc/src/status.rs | Adds neutral Status type with JSON-details support. |
| crates/rest_over_grpc/src/transcode/error.rs | Adds TranscodeError and status mapping for transcoding failures. |
| crates/rest_over_grpc/src/transcode/field_deserializer.rs | Adds internal deserializer used for body-field decoding. |
| crates/rest_over_grpc/src/transcode/percent.rs | Adds minimal percent-decoding helpers for path/query bindings. |
| crates/rest_over_grpc/src/transcode/request_body_kind.rs | Adds RequestBodyKind describing request-body mapping at runtime. |
| crates/rest_over_grpc/src/transcode/response_body_kind.rs | Adds ResponseBodyKind describing response-body mapping at runtime. |
| crates/rest_over_grpc/tests/adapter.rs | Integration tests for tower/layered HTTP adapters. |
| crates/rest_over_grpc/tests/axum_support.rs | Integration tests for axum IntoResponse integration. |
| crates/rest_over_grpc/tests/streaming_adapter.rs | Integration tests for streaming HTTP adapter and service wrapper. |
| crates/rest_over_grpc/tests/value_types.rs | Integration tests for always-available value types (Code, Status, HttpResponse). |
| crates/rest_over_grpc_build/Cargo.toml | Defines the new rest_over_grpc_build codegen crate and features. |
| crates/rest_over_grpc_build/CHANGELOG.md | Introduces the crate changelog for initial release. |
| crates/rest_over_grpc_build/favicon.ico | Adds crate branding asset (Git LFS). |
| crates/rest_over_grpc_build/logo.png | Adds crate branding asset (Git LFS). |
| crates/rest_over_grpc_build/examples/generate_service.rs | Example: generate service code from hand-built HttpRules. |
| crates/rest_over_grpc_build/src/binding.rs | Models additional HTTP bindings and lowers them into routes. |
| crates/rest_over_grpc_build/src/descriptor_error.rs | Adds descriptor decoding error type with predicates and Display. |
| crates/rest_over_grpc_build/src/descriptor_options.rs | Adds options for decoding services/types from descriptor sets. |
| crates/rest_over_grpc_build/src/generator_builder.rs | Adds builder for configuring generator options (Send bounds, tonic bridge, OpenAPI). |
| crates/rest_over_grpc_build/src/generator_output.rs | Defines per-service generated output container. |
| crates/rest_over_grpc_build/src/http_method.rs | Defines HttpMethod mapping and tokens for routing/codegen. |
| crates/rest_over_grpc_build/src/http_rule.rs | Defines HttpRule model and lowering into Routes. |
| crates/rest_over_grpc_build/src/proto/google/api/annotations.proto | Vendors google API annotations proto schema for consumers. |
| crates/rest_over_grpc_build/src/proto/google/api/http.proto | Vendors google API http proto schema for consumers. |
| crates/rest_over_grpc_build/src/request_body.rs | Models HttpRule.body as RequestBody with Display. |
| crates/rest_over_grpc_build/src/response_body.rs | Models HttpRule.response_body as ResponseBody with Display. |
| crates/rest_over_grpc_build/src/route.rs | Models lowered HTTP route components for router/dispatcher codegen. |
| crates/rest_over_grpc_build/src/service_method.rs | Models a service method and its lowered routes for codegen. |
| crates/rest_over_grpc_bench/Cargo.toml | Adds benchmarking/correctness crate for router/dispatcher behavior. |
| crates/rest_over_grpc_bench/bench_routes.rs | Provides shared large route table for generated router + matchit comparison. |
| crates/rest_over_grpc_bench/benches/rog_dispatch_cg.rs | Callgrind benchmark for end-to-end generated dispatcher. |
| crates/rest_over_grpc_bench/benches/rog_router_cg.rs | Callgrind benchmark for generated router vs matchit. |
| crates/rest_over_grpc_bench/build.rs | Generates routers from route tables at build time into OUT_DIR. |
| crates/rest_over_grpc_bench/src/lib.rs | Includes generated routers and exposes resolve functions for tests/benches. |
| crates/rest_over_grpc_bench/tests/bench_router.rs | Smoke tests verifying benchmark router resolves representative routes. |
| crates/rest_over_grpc_bench/tests/coverage.rs | Routing correctness tests for tricky matching/backtracking cases. |
| crates/rest_over_grpc_custom_example/Cargo.toml | Defines the custom/prost handler example crate (OpenAPI enabled). |
| crates/rest_over_grpc_custom_example/build.rs | Builds prost+pbjson types and generates REST trait/dispatcher + OpenAPI. |
| crates/rest_over_grpc_custom_example/examples/client_streaming_upload.rs | Example: compose non-transcoded streaming upload with transcoded JSON routes. |
| crates/rest_over_grpc_custom_example/examples/custom_body_handling.rs | Example: custom body policy (caps/415/413) around generated dispatch. |
| crates/rest_over_grpc_custom_example/proto/google/api/annotations.proto | Vendors google API annotations schema for the example. |
| crates/rest_over_grpc_custom_example/proto/google/api/http.proto | Vendors google API http schema for the example. |
| crates/rest_over_grpc_custom_example/proto/library.proto | Example proto annotated with google.api.http rules. |
| crates/rest_over_grpc_custom_example/src/lib.rs | Implements generated REST service trait directly (no tonic bridge). |
| crates/rest_over_grpc_custom_example/tests/adapter.rs | End-to-end tests driving generated dispatcher via HTTP adapter. |
| crates/rest_over_grpc_custom_example/tests/e2e.rs | End-to-end transcoding tests (path/query/body, headers, details). |
| crates/rest_over_grpc_custom_example/tests/openapi.rs | Validates generated OpenAPI 3.1 document content/structure. |
| crates/rest_over_grpc_example/Cargo.toml | Defines the tonic-bridge example crate and build deps. |
| crates/rest_over_grpc_example/build.rs | Generates tonic messages/server trait, pbjson serde, and REST bridge/dispatch. |
| crates/rest_over_grpc_example/examples/basic_dispatch.rs | Example: call generated dispatch directly. |
| crates/rest_over_grpc_example/examples/custom_fallback.rs | Example: try_dispatch for custom routing fallback behavior. |
| crates/rest_over_grpc_example/examples/streaming_response.rs | Example: end-to-end streaming to wire via dispatch_streaming. |
| crates/rest_over_grpc_example/examples/tower_service.rs | Example: serve tonic-bridged dispatcher through RestService (tower). |
| crates/rest_over_grpc_example/proto/google/api/annotations.proto | Vendors google API annotations schema for the example. |
| crates/rest_over_grpc_example/proto/google/api/http.proto | Vendors google API http schema for the example. |
| crates/rest_over_grpc_example/proto/greeter.proto | Example proto (unary + server-streaming) annotated for REST transcoding. |
| crates/rest_over_grpc_example/src/lib.rs | Implements tonic server trait and re-exports generated dispatcher. |
| crates/rest_over_grpc_example/tests/bridge.rs | End-to-end tests verifying the tonic bridge dispatches and maps statuses. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Codecov Report❌ Patch coverage is
❌ Your project check has failed because the head coverage (99.7%) is below the target coverage (100.0%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #546 +/- ##
========================================
- Coverage 100.0% 99.7% -0.3%
========================================
Files 356 401 +45
Lines 27388 34141 +6753
========================================
+ Hits 27388 34065 +6677
- Misses 0 76 +76 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| /// ); | ||
| /// ``` | ||
| #[derive(Debug)] | ||
| pub struct ParseError { |
There was a problem hiding this comment.
Why not ohno? This appears to be hand-rolling part of ohno functionality for no obvious reason. We have an error library, let's use it.
|
|
||
| //! Generating a REST service from `google.api.http` rules. | ||
| //! | ||
| //! Builds [`HttpRule`]s (normally read straight from proto annotations by |
There was a problem hiding this comment.
I am confused by this example. Why is it saying how things are done "normally"? Is this an "abnormal" example? To an uninformed reader the context of this example is hard to identity: "generating a REST service from 'google.api.http' rules" feels like a "normal" thing to do but this statement suggests to me I am doing it "abnormally" somehow. More clarity on what is special in this example would be desirable.
| // Render the generated Rust for display; a build script would instead | ||
| // write the code to files under `OUT_DIR` with `Generator::write`. | ||
| for service in generated { | ||
| let mut code = service.r#trait().clone(); |
There was a problem hiding this comment.
Having to write r#trait() suggests some rather suboptimal naming. Might be better to avoid reserved keywords in public APIs.
|
|
||
| //! Serving REST/JSON through the `layered` adapter. | ||
| //! | ||
| //! The repository's `layered::Service` is an `async fn`-with-`&self` alternative |
There was a problem hiding this comment.
"The repository's" is cumbersome wording. Let's speak of the layered package, not of "the repository" - packages can move between repositories and readers might not always have context to understand what "the repository" refers to.
|
|
||
| #[tokio::main(flavor = "current_thread")] | ||
| async fn main() { | ||
| // `layered::Service` needs no `Clone` and takes `&self`, so the service is |
There was a problem hiding this comment.
Unclear what the impact of this comment is. Why are we document things that are not? Same for the comment a few lines below.
It almost sounds as if this was made as a "comparison" but no comparand is in sight.
| //! // request and encoding the reply. Returns a `TranscodeResponse` (a buffered | ||
| //! // unary reply or a server-streaming frame stream); `None` when no route | ||
| //! // matches. | ||
| //! async fn try_transcode(&self, method: &str, target: &str, |
There was a problem hiding this comment.
I'm not sure about the stringiness here (over URI parts or similar), but maybe that comes from having to be compatible with a range of crates?
There was a problem hiding this comment.
Correct, this is the only lingua franca there is at this level of the stack.
| //! composed alongside the transcoded JSON routes. See | ||
| //! `rest_over_grpc_examples`'s `examples/handling/client_streaming_upload.rs`. | ||
| //! | ||
| //! - **The request body is buffered as one JSON `&[u8]`.** The generated |
There was a problem hiding this comment.
Nit, it's not the &[u8] that does buffering, but rather what it's exposed as?
| // Licensed under the MIT License. | ||
|
|
||
| //! The serving layer: bridges the `http` / `http-body` ecosystem to the neutral | ||
| //! transcoder signature. Gated on the `serving` feature (on by default, and |
There was a problem hiding this comment.
This is what the docrs attributes are for, this shouldn't be documented as text.
| @@ -0,0 +1,84 @@ | |||
| // Copyright 2024 Google LLC | |||
There was a problem hiding this comment.
Is the idea that every user has to place exactly these files besides their project for the main annotations to work?
| @@ -0,0 +1,124 @@ | |||
| // Copyright (c) Microsoft Corporation. | |||
There was a problem hiding this comment.
I'm not very fond of most examples. There is a lot going on in both the library and the examples, and it's hard to know where to even start looking, everything is sort of related to everything else somehow, concept wise. Basic transcode is small enough to start wrapping my head around things, but even then I have to dive into LibraryServce, the generated "/tonic_bridge/library.rs" ...
Like is there a way to have basic examples of fundamental concepts only, say, 1/3 as large?
Expose an existing gRPC API as a traditional REST/JSON API in the same
process, driven by `google.api.http` annotations. The gRPC service is
implemented once; the REST layer is generated at build time and transcodes
requests and responses with minimal runtime overhead. The generated router is
built on a standalone, zero-allocation static router (`routerama`) that is also
useful on its own.
Crates added:
- `routerama`: dependency-free runtime for a compile-time static HTTP router —
zero-allocation method + path matching returning the matched tag and captured
path variables (with a SIMD segment scanner).
- `routerama_macros`: the `routes!` procedural macro that generates a static
router inline from a named route enum, with no `build.rs` required.
- `routerama_build`: build-time code generator that lowers a route set into a
`resolve` function (a nested `match` over path segments — a compile-time trie),
reporting genuinely ambiguous routes as a `compile_error!`.
- `http_path_template`: dependency-free parser for the `google.api.http`
path-template grammar (literals, `*`, `**`, `{field=sub}` variables, `:verb`),
with an optional extended grammar for intra-segment prefix/suffix parameters.
- `rest_over_grpc`: framework-neutral transcoding crate spanning the runtime,
the serving adapters, and the build-time code generator:
- `transcoding` / `handling`: decode an HTTP request into a protobuf message,
dispatch it to the gRPC handler, and encode the reply as JSON, mapping
`Code` / `Status` to HTTP status codes.
- `serving` (default, feature-gated): one-shot `serve_http*` helpers plus a
`RestService` adapter for the `tower` and `layered` ecosystems (hyper, axum,
…), serving both unary and server-streaming RPCs through one response type.
Request bodies are buffered with an optional `with_max_body_bytes` cap
(`413 Payload Too Large` on overflow), and read failures surface as
`400 Bad Request` rather than an empty body.
- Server-streaming responses are forwarded frame by frame as a JSON array,
NDJSON, or SSE (negotiated from `Accept`); a unary reply is buffered, both
through the same serving path.
- `build` / `build-openapi`: a `build.rs` code generator that turns
`google.api.http`-annotated gRPC services (read from compiled descriptors)
into a static REST router, service traits, a dispatcher, an optional `tonic`
bridge, and an optional OpenAPI 3.1 specification.
- `rest_over_grpc_examples`: worked examples organized by architecture layer,
covering both the generated `tonic` bridge and a hand-written service that
implements the generated trait directly.
- `rest_over_grpc_tests`: end-to-end tests and benchmarks for the generated
router and dispatcher.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expose an existing gRPC API as a traditional REST/JSON API in the same
process, driven by
google.api.httpannotations. The gRPC service isimplemented once; the REST layer is generated at build time and transcodes
requests and responses with minimal runtime overhead. The generated router is
built on a standalone, zero-allocation static router (
routerama) that is alsouseful on its own.
Crates added:
routerama: dependency-free runtime for a compile-time static HTTP router —zero-allocation method + path matching returning the matched tag and captured
path variables (with a SIMD segment scanner).
routerama_macros: theroutes!procedural macro that generates a staticrouter inline from a named route enum, with no
build.rsrequired.routerama_build: build-time code generator that lowers a route set into aresolvefunction (a nestedmatchover path segments — a compile-time trie),reporting genuinely ambiguous routes as a
compile_error!.http_path_template: dependency-free parser for thegoogle.api.httppath-template grammar (literals,
*,**,{field=sub}variables,:verb),with an optional extended grammar for intra-segment prefix/suffix parameters.
rest_over_grpc: framework-neutral transcoding crate spanning the runtime,the serving adapters, and the build-time code generator:
transcoding/handling: decode an HTTP request into a protobuf message,dispatch it to the gRPC handler, and encode the reply as JSON, mapping
Code/Statusto HTTP status codes.serving(default, feature-gated): one-shotserve_http*helpers plusRestService/StreamingRestServiceadapters for thetowerandlayeredecosystems (hyper, axum, …). Request bodies are buffered with anoptional
with_max_body_bytescap (413 Payload Too Largeon overflow),and read failures surface as
400 Bad Requestrather than an empty body.streaming: server-streaming responses forwarded frame by frame as a JSONarray, NDJSON, or SSE.
build/build-openapi: abuild.rscode generator that turnsgoogle.api.http-annotated gRPC services (read from compiled descriptors)into a static REST router, service traits, a dispatcher, an optional
tonicbridge, and an optional OpenAPI 3.1 specification.
rest_over_grpc_examples: worked examples organized by architecture layer,covering both the generated
tonicbridge and a hand-written service thatimplements the generated trait directly.
rest_over_grpc_tests: end-to-end tests and benchmarks for the generatedrouter and dispatcher.