olai-codegen
Proto-driven code generator that transforms compiled protobuf descriptors into idiomatic Rust server and client code, resource registries, and optional Python/Node.js/TypeScript bindings.
Overview
The single source of truth is your .proto files. Annotate them with standard Google API extensions and run trestle generate to emit:
| Output | What is generated |
|---|---|
common/ |
Axum extractors, shared request/response types, mod.rs |
models/ |
labels.rs — resource enums and olai_store::Label impls |
server/ |
Handler traits (one async method per RPC) and Axum route handlers |
client/ |
HTTP client struct with typed request builder methods |
python/ |
PyO3 bindings and .pyi type stubs |
node/ |
NAPI-RS bindings |
node_ts/ |
TypeScript client |
Pipeline
.proto files
│
▼ buf build --as-file-descriptor-set
descriptor binary (.bin)
│
▼ trestle generate
Source files (Rust, Python, TypeScript, …)
Proto Annotations
The generator reads standard Google API extensions. No custom annotations are required.
| Annotation | Effect |
|---|---|
google.api.resource |
Registers a message as a managed resource type; drives resource enum variants and label generation |
google.api.http |
Maps each RPC to an HTTP method and URI pattern; drives route handlers and client methods |
google.api.field_behavior |
Marks fields as OUTPUT_ONLY, REQUIRED, OPTIONAL, IDENTIFIER, etc.; shapes generated extractors and builders |
google.api.resource_reference |
Declares parent-child relationships between resource types; drives hierarchical name composition |
Example proto fragment (see proto/ for full working examples):
message Catalog {
option (google.api.resource) = {
type: "example.io/Catalog"
pattern: "catalogs/{catalog}"
singular: "catalog"
plural: "catalogs"
};
string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY];
string comment = 2 [(google.api.field_behavior) = OPTIONAL];
}
service CatalogService {
rpc GetCatalog(GetCatalogRequest) returns (Catalog) {
option (google.api.http) = { get: "/catalogs/{name}" };
}
rpc CreateCatalog(CreateCatalogRequest) returns (Catalog) {
option (google.api.http) = { post: "/catalogs" body: "*" };
}
}
Usage
Installation
olai-codegen is a library; code generation is driven by the trestle CLI. Install it from
the workspace:
Compile descriptors
Config file (recommended)
A YAML config file is the preferred way to invoke trestle generate, particularly in multi-crate workspaces where output directories span crate boundaries. CLI flags override any value from the file. New projects scaffolded with trestle new use trestle.yaml (the older proto-gen.yaml name is still accepted).
# trestle.yaml
descriptors: api.bin
buf_gen: buf.gen.yaml # optional: auto-derives models_path_template from prost plugin output
generate:
output_common: crates/server/src/gen/common
output_models: crates/common/src/models # labels.rs lands in <output_models>/_gen/
output_server: crates/server/src/gen/server
output_client: crates/client/src/gen/client
context_type: my_crate::RequestContext
result_type: my_crate::Result
# Required when buf_gen is not set:
models_path_template: my_models::models::{service}::v1
models_path_crate_template: crate::models::{service}::v1
# Resource enum generation (requires olai-store)
generate_resource_enum: true
generate_store_integration: true
generate_object_conversions: true
error_type_path: crate::Error
# Optional: language bindings
python:
output: crates/python/src/gen
error_type: my_crate::PyError
result_type: my_crate::PyResult
typescript:
output: crates/node/src/gen
aggregate_client_name: MyClient
client_crate_name: my_client
Any field can be overridden at the command line:
CLI flags (all options)
trestle generate
| Flag | Env var | Description |
|---|---|---|
--config / -c |
— | YAML config file; CLI flags override |
--descriptors / -d |
UC_BUILD_DESCRIPTORS |
Compiled proto descriptor binary |
--context-type |
UC_BUILD_CONTEXT_TYPE |
Rust path for the request context type |
--result-type |
UC_BUILD_RESULT_TYPE |
Rust path for the Result alias |
--models-path-template |
UC_BUILD_MODELS_PATH_TEMPLATE |
External import path template ({service} is replaced per service) |
--models-path-crate-template |
UC_BUILD_MODELS_PATH_CRATE_TEMPLATE |
Intra-crate import path template |
--output-common |
UC_BUILD_OUTPUT_COMMON |
Output dir for common extractors (required) |
--output-models |
UC_BUILD_OUTPUT_MODELS |
Parent dir for resource labels |
--models-subdir |
UC_BUILD_MODELS_SUBDIR |
Subdirectory inside models dir (default: _gen) |
--output-server |
UC_BUILD_OUTPUT_SERVER |
Output dir for handler traits and route handlers |
--output-client |
UC_BUILD_OUTPUT_CLIENT |
Output dir for HTTP client |
--output-python |
UC_BUILD_OUTPUT_PYTHON |
Output dir for PyO3 bindings |
--output-node |
UC_BUILD_OUTPUT_NODE |
Output dir for NAPI-RS bindings |
--output-node-ts |
UC_BUILD_OUTPUT_NODE_TS |
Output dir for TypeScript client |
--python-typings-filename |
UC_BUILD_PYTHON_TYPINGS_FILENAME |
Name of the .pyi stub file |
trestle enrich-openapi
Merges proto-derived validation rules into an OpenAPI YAML spec.
| Flag | Description |
|---|---|
--config / -c |
YAML config file (uses enrich_openapi: section) |
--spec |
OpenAPI YAML file to enrich (default: openapi/openapi.yaml) |
--jsonschema-dir |
Directory of JSON Schema files from buf (default: openapi/jsonschema) |
--descriptors |
Proto descriptor for deduplication pass; omit to skip |
--camel-case |
Convert snake_case field names to camelCase |
The same top-level config file covers both subcommands:
descriptors: api.bin
enrich_openapi:
spec: openapi/openapi.yaml
jsonschema_dir: openapi/jsonschema
camel_case: true
Recommended Project Structure
The structure described below is what
trestle new databricks-app-rustproduces. Use the CLI to scaffold a fresh project in this shape and skip the manual setup:The rest of this section documents the same layout for users who'd rather wire things up by hand.
trestle generate writes generated files into output directories that can span multiple crates. The recommended layout separates concerns across four crates:
my-workspace/
├── Cargo.toml
├── proto/
│ └── my_service.proto # source of truth
├── api.bin # buf build output
├── trestle.yaml
└── crates/
├── common/ # shared model types
│ └── src/
│ └── models/
│ ├── mod.rs # hand-written: re-exports from _gen
│ └── _gen/
│ ├── labels.rs # generated: Resource enum, ObjectLabel, store impls
│ └── mod.rs # generated: include! prost output + re-exports
│
├── server/ # Axum service implementation
│ └── src/
│ ├── api/
│ │ ├── context.rs # hand-written: RequestContext (impl FromRequestParts)
│ │ └── error.rs # hand-written: Error type, Result alias
│ └── gen/
│ ├── common/ # generated: Axum extractors
│ └── server/ # generated: handler traits + route handlers
│
└── client/ # HTTP client
└── src/
└── gen/
├── common/ # generated: shared extractor types
└── client/ # generated: client struct + request builders
What you write vs. what is generated
Hand-written (once, then stable):
RequestContext— extracted from each Axum request (auth, request ID, etc.); must implementaxum::extract::FromRequestPartsError/Result<T>— your service error type and result alias; used in all generated handler and client signaturesparse_error_response— called by generated client code on non-2xx responses; signature:pub async fn parse_error_response(response: reqwest::Response) -> Errorcommon/src/models/mod.rs— re-exports from_genso the rest of the codebase imports from a stable path
Generated (overwritten on each run, do not edit):
common/src/models/_gen/labels.rs—Resourceenum,ObjectLabelenum,olai_store::Labelimpl,RESOURCE_DESCRIPTORScommon/src/models/_gen/mod.rs—include!directives into prost output +pub usere-exportsserver/src/gen/common/— Axum extractor structs for path/query/body parametersserver/src/gen/server/—*Handlertrait (oneasync fnper RPC) + Axum route handler functionsclient/src/gen/client/—*Clientstruct +with_*builder methods
Implementing a handler
The generated handler trait has one method per RPC. Implement it in your server crate:
use async_trait;
use CatalogHandler;
The generator emits a free Axum handler function per RPC (e.g. get_catalog,
create_catalog) but does not build the Router for you — you wire the routes by hand.
This is deliberate: only your app knows the URL prefixes to nest under, and a service often
mixes generated CRUD handlers with hand-written ones (custom streaming responses, bespoke
headers) that the generator can't see. Write one small router fn per service:
use ;
use *;
Then merge the per-service routers and nest them under your API prefix:
let handler = new;
let app = new
.nest;
Using the generated client
use CloudClient;
use CatalogClient;
let client = new;
let catalog = client.get_catalog.await?;
Cargo.toml dependencies
Add these to the crates that consume the generated code:
# server/Cargo.toml
[]
= "0.8"
= "0.10"
= "0.1"
= { = "../../crates/olai-store" } # if store integration enabled
# client/Cargo.toml
[]
= { = "../../crates/olai-http" }
= "2"
= "1"
# common/Cargo.toml — additional deps when generate_object_conversions = true
[]
= { = "../../crates/olai-store" }
= { = "1", = ["v4"] }
= "0.4"
= { = "0.26", = ["derive"] }
Library Usage
trestle generate handles the full workflow for most use cases. If you need to embed generation into a Rust program (e.g. a custom build tool), the library API exposes the same three-step pipeline:
use ;
use FileDescriptorSet;
use Message;
let descriptor_bytes = read?;
let fds = parse_from_bytes?;
let metadata = parse_file_descriptor_set?;
let config = CodeGenConfig ;
generate_code?;
Configuration Reference
CodeGenConfig
| Field | Type | Description |
|---|---|---|
context_type_path |
String |
Rust path for the request context type injected into handler methods |
result_type_path |
String |
Rust path for the Result alias used in handler signatures |
models_path_template |
String |
External import path for prost-generated models; {service} is replaced per service |
models_path_crate_template |
String |
Intra-crate import path (used inside generated server/client code) |
output |
CodeGenOutput |
Output directory configuration |
generate_resource_enum |
bool |
Emit Resource and ObjectLabel enums in labels.rs |
generate_store_integration |
bool |
Emit olai_store::Label impl and RESOURCE_DESCRIPTORS static |
error_type_path |
Option<String> |
Error type for TryFrom<Resource> impls; enables From/TryFrom generation |
generate_object_conversions |
bool |
Emit TryFrom<Object> and ResourceExt impls |
bindings |
Option<BindingsConfig> |
Language-binding configuration for Python/Node/TypeScript output |
models_gen_dir |
Option<String> |
Relative path to prost-generated gen/ directory |
resource_store_crate_name |
String |
Name of the store crate (default: olai_store) |
runtime |
Runtime |
Protobuf runtime the generated code is shaped for: Prost (default) or Buffa |
transport_type_path |
String |
HTTP transport type generated clients store and call (default: olai_http::CloudClient). See WASM / browser clients |
WASM / browser clients
Generated clients are decoupled from their HTTP transport: the client struct stores whatever
type transport_type_path names, and the method bodies only use a small surface — per-verb
builders (get/post/put/patch/delete) returning a builder with .json(..)/.query(..)/
.send(), whose response has .status()/.bytes(). [olai_http::CloudClient] (the default) and
[olai_http_wasm::WasmClient] both satisfy this.
Set the transport to the WASM client to emit a wasm32-unknown-unknown-buildable client for
browser front-ends. In trestle.yaml:
transport: wasm # friendly alias; or set transport_type_path: "olai_http_wasm::WasmClient"
The WASM client carries no request signing, ring, tokio, or cloud-credential discovery —
in the browser the session is managed by the browser (fetch is asked to include credentials,
so cookies / auth headers ride along on same-origin or CORS-with-credentials requests). The
cloud-only aggregate constructors (new_unauthenticated, new_with_token) are not emitted for a
WASM transport; construct the client with new(transport, base_url). Pair with runtime: buffa
for serde-native, dependency-light models.
JS/TS bindings (#[wasm_bindgen] + .d.ts)
Set output.wasm (or wasm.output in trestle.yaml) to also emit a #[wasm_bindgen] wrapper
layer for JS/browser consumption. Two files are generated:
bindings.rs— a#[wasm_bindgen]struct per service plus an aggregate root constructed from a base URL. Each RPC is anasyncmethod that takes/returns plain JS objects, marshalled withserde-wasm-bindgen(noBuffer/protobuf-es decode step). Compile this crate towasm32-unknown-unknownand package withwasm-pack.client.d.ts— TypeScript declarations: the aggregate class (new ExampleClient(baseUrl)), its per-service accessors, and each method typedmethod(request: ReqType): Promise<RespType>with request/response types imported from./models.
runtime: buffa # required: serde-native models cross the JS boundary as objects
transport: wasm
wasm:
output: ./web/src/gen # emits bindings.rs + client.d.ts
// after `wasm-pack build`
import from "./gen";
const client = ;
const cat = await client..; // browser attaches the session
The generated client crate must depend on wasm-bindgen, wasm-bindgen-futures,
serde-wasm-bindgen, and olai-http-wasm.
CodeGenOutput
| Field | Type | Description |
|---|---|---|
common |
PathBuf |
Required. Output dir for shared extractors and mod.rs |
models |
Option<PathBuf> |
Parent dir for labels.rs and model mod.rs |
models_subdir |
String |
Subdirectory inside models (default: _gen) |
server |
Option<PathBuf> |
Output dir for handler traits and Axum route handlers |
client |
Option<PathBuf> |
Output dir for HTTP client |
python |
Option<PathBuf> |
Output dir for PyO3 bindings |
node |
Option<PathBuf> |
Output dir for NAPI-RS bindings |
node_ts |
Option<PathBuf> |
Output dir for TypeScript client |
wasm |
Option<PathBuf> |
Output dir for WASM #[wasm_bindgen] bindings + client.d.ts. See JS/TS bindings |
python_typings_filename |
String |
Stub filename (default: client.pyi) |
Examples
The proto/ directory contains fully-annotated example protobuf definitions and a pre-compiled descriptor (example.bin). Integration tests in tests/ run the full pipeline against these examples and serve as executable documentation of the generated output.