olai-codegen 0.0.2

Proto-driven code generation for REST handlers, clients, and resource registries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
# 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):

```proto
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:

```bash
cargo install --path crates/trestle
trestle generate --help
```

### Compile descriptors

```bash
buf build --as-file-descriptor-set -o api.bin
```

### 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).

```yaml
# 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
```

```bash
trestle generate --config trestle.yaml
```

Any field can be overridden at the command line:

```bash
trestle generate --config trestle.yaml --descriptors path/to/other.bin
```

### 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:

```yaml
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-rust`
> produces. Use the CLI to scaffold a fresh project in this shape and skip the
> manual setup:
>
> ```bash
> trestle new my-app --template databricks-app-rust --profile dbx-emulator
> ```
>
> 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 implement `axum::extract::FromRequestParts`
- `Error` / `Result<T>` — your service error type and result alias; used in all generated handler and client signatures
- `parse_error_response` — called by generated client code on non-2xx responses; signature: `pub async fn parse_error_response(response: reqwest::Response) -> Error`
- `common/src/models/mod.rs` — re-exports from `_gen` so the rest of the codebase imports from a stable path

**Generated** (overwritten on each run, do not edit):

- `common/src/models/_gen/labels.rs` — `Resource` enum, `ObjectLabel` enum, `olai_store::Label` impl, `RESOURCE_DESCRIPTORS`
- `common/src/models/_gen/mod.rs` — `include!` directives into prost output + `pub use` re-exports
- `server/src/gen/common/` — Axum extractor structs for path/query/body parameters
- `server/src/gen/server/` — `*Handler` trait (one `async fn` per RPC) + Axum route handler functions
- `client/src/gen/client/` — `*Client` struct + `with_*` builder methods

### Implementing a handler

The generated handler trait has one method per RPC. Implement it in your server crate:

```rust
use async_trait::async_trait;
use gen::server::catalogs::CatalogHandler;

struct MyCatalogHandler { /* db pool, etc. */ }

#[async_trait]
impl CatalogHandler for MyCatalogHandler {
    async fn get_catalog(
        &self,
        request: GetCatalogRequest,
        context: RequestContext,
    ) -> Result<Catalog> {
        // ...
    }
}
```

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:

```rust
use axum::routing::{delete, get, patch, post};
use gen::server::catalogs::server::*;

fn catalogs_router<T, Cx>(handler: T) -> axum::Router
where
    T: CatalogHandler<Cx> + Clone + Send + Sync + 'static,
    Cx: axum::extract::FromRequestParts<T> + Send + 'static,
{
    axum::Router::new()
        .route("/catalogs", get(list_catalogs::<T, Cx>).post(create_catalog::<T, Cx>))
        .route(
            "/catalogs/{name}",
            get(get_catalog::<T, Cx>)
                .patch(update_catalog::<T, Cx>)
                .delete(delete_catalog::<T, Cx>),
        )
        .with_state(handler)
}
```

Then merge the per-service routers and nest them under your API prefix:

```rust
let handler = MyCatalogHandler::new();
let app = axum::Router::new()
    .nest("/api/2.1/unity-catalog", catalogs_router(handler.clone()).merge(schemas_router(handler)));
```

### Using the generated client

```rust
use olai_http::CloudClient;
use gen::client::catalogs::CatalogClient;

let client = CatalogClient::new(
    CloudClient::new(/* credentials */),
    url::Url::parse("https://api.example.com/")?,
);

let catalog = client.get_catalog(&GetCatalogRequest { name: "my-catalog".into() }).await?;
```

### Cargo.toml dependencies

Add these to the crates that consume the generated code:

```toml
# server/Cargo.toml
[dependencies]
axum = "0.8"
axum-extra = "0.10"
async-trait = "0.1"
olai-store = { path = "../../crates/olai-store" }  # if store integration enabled

# client/Cargo.toml
[dependencies]
olai-http = { path = "../../crates/olai-http" }
url = "2"
serde_json = "1"

# common/Cargo.toml — additional deps when generate_object_conversions = true
[dependencies]
olai-store = { path = "../../crates/olai-store" }
uuid = { version = "1", features = ["v4"] }
chrono = "0.4"
strum = { version = "0.26", features = ["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:

```rust
use olai_codegen::{
    parse_file_descriptor_set, generate_code,
    CodeGenConfig, CodeGenOutput,
};
use protobuf::descriptor::FileDescriptorSet;
use protobuf::Message;

let descriptor_bytes = std::fs::read("api.bin")?;
let fds = FileDescriptorSet::parse_from_bytes(&descriptor_bytes)?;
let metadata = parse_file_descriptor_set(&fds)?;

let config = CodeGenConfig {
    context_type_path: "crate::RequestContext".into(),
    result_type_path: "crate::Result".into(),
    models_path_template: "my_models::models::{service}::v1".into(),
    models_path_crate_template: "crate::models::{service}::v1".into(),
    output: CodeGenOutput {
        common: "src/gen/common".into(),
        server: Some("src/gen/server".into()),
        client: Some("src/gen/client".into()),
        ..Default::default()
    },
    ..Default::default()
};

generate_code(&metadata, &config)?;
```

## 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 |

### 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`:

```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 an `async` method that takes/returns plain JS objects, marshalled with
  `serde-wasm-bindgen` (no `Buffer`/protobuf-es decode step). Compile this crate to
  `wasm32-unknown-unknown` and package with `wasm-pack`.
- `client.d.ts` — TypeScript declarations: the aggregate class (`new ExampleClient(baseUrl)`), its
  per-service accessors, and each method typed `method(request: ReqType): Promise<RespType>` with
  request/response types imported from `./models`.

```yaml
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
```

```js
// after `wasm-pack build`
import { ExampleClient } from "./gen";
const client = new ExampleClient(window.location.origin);
const cat = await client.catalog().createCatalog({ name: "c1" });  // 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]#jsts-bindings-wasm_bindgen--dts |
| `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.