foundation_connectrpc 0.0.3

ConnectRPC for the EWE platform — Connect/gRPC/gRPC-Web on foundation crates
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
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
# foundation_connectrpc

ConnectRPC for the EWE platform — the [Connect protocol](https://connectrpc.com/docs/protocol),
gRPC, and gRPC-Web implemented natively on the platform's foundation crates. An
independent Rust port with [connect-go](https://github.com/connectrpc/connect-go)
as the design reference — **not** a wrapper around any existing crate. There is no
tower, hyper, or tokio here: HTTP and wire types come from `foundation_netio` /
`foundation_http`, execution from valtron, errors from `foundation_errstacks`.

Speak **Connect**, **gRPC**, or **gRPC-Web** — one router, one set of handlers,
one client type — over JSON, protobuf (buffa), or Arrow.

## Status

Built feature-by-feature (see `specifications/41-connectrpc`). What is proven
end-to-end today, over a real loopback socket:

| Area | State |
|---|---|
| Connect protocol (unary + streaming) | ✅ working |
| gRPC-Web protocol | ✅ working |
| gRPC protocol over HTTP/2 (h2c) — unary + all streaming | ✅ working |
| HTTP/1.1 transport (client `H1Transport` + server `ConnectRpcServe`) | ✅ working |
| HTTP/2 transport (client `H2Transport` + server `ConnectRpcServeH2`) | ✅ working |
| Codecs: protobuf (buffa), JSON, Arrow | ✅ working |
| Compression: gzip (+ optional zstd, brotli) | ✅ working |
| Auth middleware (`auth` feature) | ✅ working |
| Code generation: proto (build.rs / protoc) + code-first macro | ✅ working |
| gRPC status trailers (client-visible) | ⏳ trailers consumed by h2 pump, not forwarded |
| HTTP/3, WebSocket transports | ⏳ pending |

The [`examples/`](examples/) directory holds runnable, self-contained programs,
each standing up a server and calling it over a real TCP socket:

| Example | Protocol | Transport | Modes |
|---|---|---|---|
| [`unary_echo`]examples/unary_echo/ | Connect | H1Transport (HTTP/1.1) | unary |
| [`server_streaming`]examples/server_streaming/ | Connect | H1Transport (HTTP/1.1) | server-stream |
| [`h2_echo`]examples/h2_echo/ | Connect | H2Transport (h2c) | unary, server-stream, client-stream, bidi |
| [`grpc_echo`]examples/grpc_echo/ | gRPC | H2Transport (h2c) | unary, server-stream, client-stream, bidi |
| [`codecs`]examples/codecs/ | Connect | H1Transport | unary (multi-codec) |
| [`code_first_service`]examples/code_first_service/ | Connect (code-first) | H1Transport | unary |
| [`code_first_streaming`]examples/code_first_streaming/ | Connect (code-first) | H1Transport | server-stream |

## Quick start

```toml
[dependencies]
foundation_connectrpc = "0.0.1"   # default = ["rpc_multi"]
```

Run the bundled examples:

```sh
# Connect protocol over HTTP/1.1
cargo run -p foundation_connectrpc --example unary_echo
cargo run -p foundation_connectrpc --example server_streaming

# Connect + gRPC over HTTP/2 cleartext (all four RPC modes)
cargo run -p foundation_connectrpc --example h2_echo
cargo run -p foundation_connectrpc --example grpc_echo

# Code-first services (proc macro)
cargo run -p foundation_connectrpc --example code_first_service
```

## The four RPC kinds

Handlers are plain async closures/functions (Decision 04). `Ctx` is passed **by
value** (owned, cheap-`Clone`, Arc-backed) and every RPC-surface signature returns
`ConnectResult<T>` (`= Result<T, ErrorTrace<ConnectError>>`).

| Kind | Handler signature |
|---|---|
| **Unary** | `Fn(Ctx, Request<Req>) -> ConnectResult<Response<Res>>` |
| **Server-streaming** | `Fn(Ctx, Request<Req>) -> ConnectResult<impl Stream<Item = ConnectResult<Res>>>` |
| **Client-streaming** | `Fn(Ctx, RequestStream<Req>) -> ConnectResult<Response<Res>>` |
| **Bidi-streaming** | `Fn(Ctx, RequestStream<Req>) -> ConnectResult<impl Stream<Item = ConnectResult<Res>>>` |

Full-duplex bidi needs a full-duplex transport — `H2Transport` (HTTP/2 cleartext
or TLS) enables it; unary, server-streaming, and client-streaming work over both
HTTP/1.1 and HTTP/2.

## Server

Register procedures on a `Router`, freeze it into a `ConnectRpcServe`, and serve
it from a `foundation_http` server. See `examples/unary_echo/` for the full
runnable version.

```rust
use std::sync::Arc;
use foundation_connectrpc::{
    ConnectRpcServe, HandlerOptions, IdempotencyLevel, ProcedureCodecs,
    Request, Response, Router,
};

let mut router = Router::new();
router.unary(
    "/demo.EchoService/Echo",                          // procedure path (leading slash)
    ProcedureCodecs::<EchoMessage, EchoMessage>::defaults(), // proto + JSON codecs
    |_ctx, req: Request<EchoMessage>| async move { Ok(Response::new(req.msg)) },
    HandlerOptions::new().with_idempotency(IdempotencyLevel::NoSideEffects),
);
let serve = Arc::new(ConnectRpcServe::new(router.into_handler()));
// hand `serve` to a foundation_http HttpServer (see the example)
```

`Router` also has `.server_stream(...)`, `.client_stream(...)`, and
`.bidi_stream(...)`. `HandlerOptions` carries per-procedure interceptors,
size limits, idempotency, compression, and pipe depth.

For HTTP/2, use `ConnectRpcServeH2` with `HttpApp::new_h2_serve()` instead:

```rust
use foundation_connectrpc::ConnectRpcServeH2;
use foundation_http::shared::serve::h2::H2Serve;

let rpc: Arc<dyn H2Serve> = Arc::new(ConnectRpcServeH2::new(router.into_handler()));
let mut app = HttpApp::new_h2_serve();
app.route_any_h2("/my.Svc/Method", rpc);
```

## Client

```rust
use std::sync::Arc;
use std::time::Duration;
use foundation_connectrpc::{
    Client, ClientOptions, Ctx, H1Transport, ProcedureCodecs, Request,
};
use foundation_connectrpc::transport::Transport;
use foundation_netio::simple_http::client::SimpleHttpClient;

// HTTP/1.1 transport (Connect + gRPC-Web)
let transport: Arc<dyn Transport> = Arc::new(H1Transport::new(SimpleHttpClient::from_system()));

// HTTP/2 cleartext transport (Connect + gRPC, all four modes including bidi)
// let transport: Arc<dyn Transport> = Arc::new(H2Transport::new());

let client: Client<EchoMessage, EchoMessage> = Client::new(
    transport,
    "http://127.0.0.1:8080/demo.EchoService/Echo",   // base URL + procedure path
    ProcedureCodecs::<EchoMessage, EchoMessage>::defaults(),
    ClientOptions::new(),                             // .with_connect() / .with_grpc() / .with_grpc_web() / .with_codec("json") / .with_timeout(..)
)?;

let ctx = Ctx::background().with_deadline(Duration::from_secs(10));
let resp = client.unary(ctx, Request::new(EchoMessage { /* .. */ })).await?;
```

`Client` also exposes `.server_stream()`, `.client_stream()`, and `.bidi_stream()`,
returning stream handles you drain with `.receive().await`.

### Protocol selection

The only difference between Connect and gRPC from user code is
`ClientOptions`. The server-side `Router` dispatches both automatically:

```rust
// Connect (the default):
ClientOptions::new().with_connect()

// gRPC over HTTP/2:
ClientOptions::new().with_grpc().with_codec("json")

// gRPC-Web over HTTP/1.1:
ClientOptions::new().with_grpc_web()
```

## Codecs

The **per-procedure `ProcedureCodecs` table is the single codec authority**
(Decision 02) — codec names are the wire Content-Type tokens; there is no
request-time registry.

| Codec | Wire name | Type bounds |
|---|---|---|
| `ProtoCodec` | `proto` | `buffa::Message + Serialize + DeserializeOwned` |
| `JsonCodec` | `json` | `Serialize + DeserializeOwned` (**serde only — no protobuf**) |
| `ArrowCodec` (feature `arrow`) | `arrow` | `ToArrow + FromArrow` |

### Constructing codec tables

| Expression | Codecs installed | When to use |
|---|---|---|
| `ProcedureCodecs::defaults()` | `(ProtoCodec, JsonCodec)` | Proto-generated or hand-written `buffa::Message` types. **`proto` is the default wire codec.** |
| `ProcedureCodecs::of((JsonCodec,))` | `(JsonCodec,)` | JSON-only — **only serde needed**, no buffa, no protobuf. |
| `ProcedureCodecs::only(JsonCodec)` | `(JsonCodec,)` | Same as above, alternate spelling. |
| `ProcedureCodecs::of((JsonCodec, ArrowCodec))` | `(JsonCodec, ArrowCodec)` | JSON + Arrow columnar. |

Proto message types get their `Message` impl from codegen (there is no derive).
For hand-written proto types, the buffa surface is re-exported as
`foundation_connectrpc::buffa` (incl. `buffa::bytes`) so you need no direct
buffa dependency.

### Client codec selection

The client's default wire codec is `proto`. To use a different codec (or when
the server only registers a JSON-only table), specify it explicitly:

```rust
ClientOptions::new().with_codec("json")
```

## Protocols

`ClientOptions` selects the protocol; the router serves all of them at once.

| Protocol | Selector | Transport | State |
|---|---|---|---|---|
| Connect | `ClientOptions::new().with_connect()` (default) | H1Transport or H2Transport | ✅ |
| gRPC-Web | `.with_grpc_web()` | H1Transport | ✅ |
| gRPC | `.with_grpc()` | H2Transport (HTTP/2 required) | ✅ unary + streaming |

---

## Code generation

There are three ways to define a ConnectRPC service. All three produce the
**same runtime surface** — procedure path constants, a service trait, a
registration function, an unimplemented handler, and a typed client — so
proto-first and code-first users converge on identical server and client code.

| Mode | Input | Mechanism | Output |
|---|---|---|---|
| **1 — build.rs** | `.proto` files | `build.rs` helper (prost-build + codegen) | `.rs` file in `OUT_DIR` |
| **2 — protoc plugin** | `.proto` files | `protoc` with `--plugin=protoc-gen-connect-ewe` | `.rs` file on disk |
| **3 — code-first** | Rust trait | `#[service]` proc macro | In-file expansion (no separate file) |

The proto codegen (Modes 1–2) lives in the **build-time companion crate**
[`foundation_connectrpc_codegen`](../foundation_connectrpc_codegen) — the same
split as `tonic` / `tonic-build`. Add it under `[build-dependencies]`. The
code-first macro (Mode 3) ships in this crate.

---

### Mode 1 — `build.rs`

Add `foundation_connectrpc_codegen` as a build dependency **and**
`foundation_connectrpc` as a regular dependency, then write a `build.rs` that
compiles your `.proto` files and generates both message types (via prost-build)
and service stubs in one pass:

```toml
# Cargo.toml
[dependencies]
foundation_connectrpc = "0.0.1"

[build-dependencies]
foundation_connectrpc_codegen = "0.0.1"
```

```rust
// build.rs
fn main() {
    foundation_connectrpc_codegen::build::Config::new()
        .files(&["proto/greet.proto"])
        .includes(&["proto/"])
        .compile()
        .unwrap();
}
```

**What this produces:**

1. **Message types** — prost-build compiles your `.proto` messages into Rust
   structs with `buffa::Message` impls. The output filename follows the
   **protobuf package name**: a proto with `package connectrpc.greet.v1;`
   produces `connectrpc.greet.v1.rs`.
2. **Service stubs** — the codegen writes `_connectrpc.rs` (always this exact
   name), containing the service trait, registration function, typed client,
   and procedure constants (see [Generated artifacts]#generated-artifacts
   below for the full list).

Both files are written to `OUT_DIR` — a directory **set automatically by
Cargo** during compilation (typically
`target/debug/build/<your-crate>-<hash>/out/`). You never configure it; Cargo
sets the `OUT_DIR` environment variable when running `build.rs`, and
`env!("OUT_DIR")` reads it back during compilation of your crate.

**Include the generated code** in your crate:

```rust
// src/lib.rs (or any module)
//
// Service stubs — always named _connectrpc.rs.
include!(concat!(env!("OUT_DIR"), "/_connectrpc.rs"));
// Message types — filename matches the proto package (dots preserved).
// For `package connectrpc.greet.v1;` in greet.proto, the filename is:
include!(concat!(env!("OUT_DIR"), "/connectrpc.greet.v1.rs"));
```

> **Note:** The generated code references runtime types by their real crate path
> (`foundation_connectrpc::Ctx`, `foundation_connectrpc::Router`, …), so your crate
> just needs `foundation_connectrpc` as a regular dependency — **no alias**.

> **How to find the prost-build filename:** If you're unsure what prost-build
> named its output, run `cargo build` once then list the directory:
> ```sh
> ls target/debug/build/<your-crate>-*/out/
> ```
> You'll see your package-named `.rs` file(s) alongside `_connectrpc.rs`.

---

### Mode 2 — protoc plugin

`protoc` can run multiple plugins in one invocation. The binary
`protoc-gen-connect-ewe` generates *service* code; pair it with `--prost_out`
(from prost-build's `protoc-gen-prost`) for *message* types, and you get both
from a single command.

Install the plugins once:

```sh
cargo install foundation_connectrpc_codegen   # provides protoc-gen-connect-ewe
cargo install protoc-gen-prost                # provides protoc-gen-prost
```

Then run `protoc`:

```sh
protoc \
    --prost_out=src/gen \
    --connect-ewe_out=src/gen \
    --proto_path=proto \
    proto/greet.proto
```

This one `protoc` call produces two files in `src/gen/`:

| Plugin | Flag | Generates | Output file |
|---|---|---|---|
| `protoc-gen-prost` | `--prost_out` | Message structs with `buffa::Message` impls | `<proto-package>.rs` (e.g. `connectrpc.greet.v1.rs`) |
| `protoc-gen-connect-ewe` | `--connect-ewe_out` | Service trait, client, registration, procedure constants | `_connectrpc.rs` |

Include both in your crate:

```rust
// src/lib.rs
pub mod gen {
    // Service stubs.
    pub mod _connectrpc;
    // Message types — prost uses dots in filenames, which `mod` can't name
    // directly, so use `include!` inside a module block.
    pub mod connectrpc_greet_v1 {
        include!("connectrpc.greet.v1.rs");
    }
}
```

> **Service-only:** If you already have message types from another source,
> omit `--prost_out`:
> ```sh
> protoc --connect-ewe_out=src/gen --proto_path=proto proto/greet.proto
> ```

The generated `_connectrpc.rs` is identical to what Mode 1 produces.

---

### Mode 3 — code-first (proc macro)

No `.proto` file at all. Define your service as a plain Rust trait and annotate
it with `#[service]`. The macro expands **in the same file** — it does not write a
separate `.rs` file. Everything is generated at compile time as part of normal
proc-macro expansion.

The generated code refers to the runtime crate by its **real name**
(`foundation_connectrpc::…`) — so there is **no crate alias**. Import the `service`
macro and the types you use:

```rust
use foundation_connectrpc::{service, Ctx, Request, Response, ConnectResult};

#[service(package = "demo.greet.v1", codecs(json))]
pub trait GreetService {
    async fn greet(
        &self,
        _ctx: Ctx,
        _req: Request<GreetRequest>,
    ) -> ConnectResult<Response<GreetResponse>>;
}
```

#### Attribute arguments

| Argument | Required? | Default | Description |
|---|---|---|---|
| `package = "..."` | **Yes** | — | Protobuf-style package name (e.g. `"acme.greet.v1"`). |
| `codecs(...)` | No | `json` | Comma-separated codec families: `json`, `proto`, `arrow`. |

#### What the macro generates

The annotation on `GreetService` expands into **all of the following items**
in the same module where the trait is defined:

| # | Generated item | Example (for `GreetService`) | Purpose |
|---|---|---|---|
| R1 | **Procedure path constants** | `pub mod procedure { pub const GREET: &str = "/demo.greet.v1.GreetService/Greet"; }` | Paths you pass to `Router` registration and `HttpApp` routing. |
| R2 | **Service name constant** | `pub const GREETSERVICE_NAME: &str = "demo.greet.v1.GreetService";` | Fully-qualified service name. |
| R3 | **The service trait** | `pub trait GreetService: Send + Sync + 'static { ... }` | Your trait, with `Send + Sync + 'static` bounds added, default `unimplemented` bodies, and `async fn` desugared to `-> impl Future<Output = ...> + Send`. |
| R4 | **Registration function** | `pub fn register_greet_service(router: &mut Router, service: Arc<S>)` where `S: GreetService` | Registers every RPC method on a `Router` in one call. |
| R5 | **Unimplemented handler** | `pub struct UnimplementedGreetServiceHandler;` + `impl GreetService for UnimplementedGreetServiceHandler {}` | A stub that returns `ConnectError::unimplemented` for every method — useful as a placeholder during development. |
| R6 | **Typed client struct** | `pub struct GreetServiceClient { greet: Client<GreetReq, GreetResp>, ... }` | A client with one async method per RPC. Its `new(transport, base_url, options)` constructor builds one `Client<Req, Res>` per method. |
| R7 | **Client trait** | `pub trait GreetServiceClientExt: Send + Sync + 'static { ... }` | A trait mirroring the client methods — enables mocking and dependency injection in tests. |
| R8 | **Descriptor macro** | `#[macro_export] macro_rules! greet_service_rpc_definitions { (common) => { ... }, (server) => { ... }, (client) => { ... } }` | Captures the expansion in filtered arms for cross-crate re-generation via `generate!`. |

**Name derivation — every generated name follows mechanically from the trait's
`Ident`.** There are three conversion rules, all standard Rust convention:

| Trait name | → snake_case | → PascalCase composition | → SCREAMING_SNAKE_CASE |
|---|---|---|---|
| `GreetService` | `greet_service` | `GreetService` + `Client` → `GreetServiceClient` | `GREETSERVICE_NAME` |
| `MyApi` | `my_api` | `MyApi` + `ClientExt` → `MyApiClientExt` | `MYAPI_NAME` |
| `GetURL` | `get_url` | `GetURL` + `Handler` → `UnimplementedGetURLHandler` | `GETURL_NAME` |

Where each convention is used:

| Convention | Applied to |
|---|---|
| **snake_case** | Registration function (`register_greet_service`), descriptor macro (`greet_service_rpc_definitions`), client field names (`greet: Client<…>`) |
| **PascalCase** | Trait name (unchanged), client struct (`GreetServiceClient`), client trait (`GreetServiceClientExt`), unimplemented handler (`UnimplementedGreetServiceHandler`) |
| **SCREAMING_SNAKE_CASE** | Service name constant (`GREETSERVICE_NAME`), procedure path constants (`GREET`) |

> **`{ server, client }` in `generate!` — what these labels mean:**
>
> The `generate!` macro was designed so that a consuming crate could ask for
> *only* the artifacts it needs rather than always getting all 8. The labels
> map to the R1-R8 items from the table above:
>
> | Label | Artifacts it selects | Typical consumer |
> |---|---|---|
> | `server` | R1 (procedure constants), R2 (name), R3 (trait), R4 (registration fn), R5 (unimplemented handler) | The crate that implements the service |
> | `client` | R1 (procedure constants), R6 (typed client struct), R7 (client trait) | A crate that only calls the service |
>
> **Today's behavior:** Filtering is not yet implemented. The macro parses
> `{ server, client }` but always emits every artifact regardless. The
> recommended practice is to write `{ server, client }` when you need both
> sides, or just `{ server }` / `{ client }` when your crate only plays one
> role — that way your intent is recorded for when filtering lands.

#### Implementing the service — same crate

The generated trait, registration function, and client all appear in the same
module. Implement the trait on your own type and call the registration function:

```rust
// ── In the same module where #[service] was applied ──

struct Greeter;

impl GreetService for Greeter {
    async fn greet(
        &self,
        _ctx: Ctx,
        req: Request<GreetRequest>,
    ) -> ConnectResult<Response<GreetResponse>> {
        Ok(Response::new(GreetResponse {
            message: format!("Hello, {}!", req.msg.name),
        }))
    }
}

// Wire it up:
let mut router = Router::new();
register_greet_service(&mut router, Arc::new(Greeter));
let serve = Arc::new(ConnectRpcServe::new(router.into_handler()));

// Use the typed client:
let client = GreetServiceClient::new(
    transport,
    &base_url,
    ClientOptions::new().with_codec("json"),
)?;
let resp = client.greet(ctx, Request::new(GreetRequest { name: "world".into() })).await?;
```

See `examples/code_first_service/` for the full runnable version.

#### Implementing the service — another crate (cross-crate)

When the trait is defined in one crate (e.g. an API crate) and implemented in
another (e.g. a server crate), you need to re-materialise the generated
artifacts in the consuming crate. This is what `generate!` does.

**Step 1 — the API crate** depends only on `foundation_connectrpc`. Annotate a
`pub trait` with `#[service]`:

```rust
// my-api/src/lib.rs
use foundation_connectrpc::{service, Ctx, Request, Response, ConnectResult};

#[service(package = "my.api.v1", codecs(json))]
pub trait MyApi {
    async fn do_thing(
        &self,
        ctx: Ctx,
        req: Request<MyRequest>,
    ) -> ConnectResult<Response<MyResponse>>;
}
```

**What Crate A exports:** Alongside the trait and the in-crate generated items
(see [the artifact table](#what-the-macro-generates)), the `#[service]`
attribute also emits a hidden `#[macro_export]` descriptor macro with
**filtered arms** so `generate!` only emits what you ask for:

```
#[macro_export]
macro_rules! my_api_rpc_definitions {
    (common) => { /* service name const + procedure paths */ };
    (server) => { /* trait + register fn + unimplemented handler */ };
    (client) => { /* typed client struct + client trait */ };
    ()        => { /* everything — fallback for empty `{}` */ };
}
```

The macro name is derived from the trait name: `MyApi` → snake_case →
`my_api` + `_rpc_definitions`. Crate B will invoke it through `generate!`.

**Step 2 — the server crate** depends on **both** `my-api` and
`foundation_connectrpc`. Use `generate!` to re-expand the descriptor macro into a
module of your choosing:

```rust
// my-server/src/main.rs
use foundation_connectrpc::generate;

// Re-expand the service artifacts into a `my_svc` module.
// `my_api::my_api_rpc_definitions` — the descriptor macro exported by Crate A.
// `{ server, client }` — ask for both server-side and client-side artifacts
// (see the note under "What the macro generates" above for what each label selects).
generate!(my_api::my_api_rpc_definitions => mod my_svc {
    server, client
});
```

**What lands in `my_svc`:** The same 8 items the `#[service]` attribute
generates — re-expanded afresh, so the trait, client, registration fn, and
procedure constants all resolve within the consuming crate's dependency context.
Everything is namespaced under the module you chose (`my_svc`):

| Item | Generated name (from trait `MyApi`) |
|---|---|
| Procedure constants | `my_svc::procedure::DO_THING` |
| Service name constant | `my_svc::MYAPI_NAME` |
| Service trait | `my_svc::MyApi` |
| Registration function | `my_svc::register_my_api` |
| Unimplemented handler | `my_svc::UnimplementedMyApiHandler` |
| Typed client | `my_svc::MyApiClient` |
| Client trait | `my_svc::MyApiClientExt` |

> **Name derivation rule:** Every generated name is deterministic — it's the
> trait name (`MyApi`) converted via standard Rust conventions:
>   - snake_case (`to_snake_case("MyApi")` → `my_api`) for functions and the
>     descriptor macro
>   - PascalCase for struct/trait/enum names (`MyApi` + `Client` → `MyApiClient`)
>   - SCREAMING_SNAKE_CASE for constants (`MYAPI_NAME`)
>
> You never guess these names; they follow mechanically from the trait's
> `Ident`.

**Step 3 — implement and wire up:**

```rust
// Continuing in my-server/src/main.rs

use my_svc::{MyApi, MyApiClient, register_my_api};

struct MyServer;

impl MyApi for MyServer {
    async fn do_thing(
        &self,
        ctx: foundation_connectrpc::Ctx,
        req: foundation_connectrpc::Request<MyRequest>,
    ) -> foundation_connectrpc::ConnectResult<foundation_connectrpc::Response<MyResponse>> {
        // ...
    }
}

// Server:
let mut router = foundation_connectrpc::Router::new();
register_my_api(&mut router, Arc::new(MyServer));

// Client:
let client = MyApiClient::new(transport, &base_url, ClientOptions::new().with_codec("json"))?;
let resp = client.do_thing(ctx, Request::new(MyRequest { /* ... */ })).await?;
```

> **Why not just `use` the items from Crate A?** The descriptor macro approach
> means Crate B *regenerates* the items rather than sharing them. This avoids
> Crate A needing to re-export every generated artifact (which would pollute its
> public API with client structs and registration fns it doesn't need), and
> ensures the generated code's `foundation_connectrpc::` paths resolve against Crate B's
> own dependency graph.
>
> Under the hood: `#[service]` wraps everything in a `macro_rules!` that
> captures the full token stream. `generate!` invokes that macro inside a
> `pub mod <name> { ... }` block. No files are touched — it's pure compile-time
> proc-macro expansion.

#### Streaming methods in code-first

All four RPC kinds are supported. The macro classifies each method by inspecting
its parameter and return types syntactically:

```rust
#[service(package = "demo.v1", codecs(json))]
pub trait StreamingService {
    // Unary: Request<T> → ConnectResult<Response<T>>
    async fn unary(&self, ctx: Ctx, req: Request<Req>) -> ConnectResult<Response<Res>>;

    // Server-streaming: Request<T> → ConnectResult<impl Stream<Item = ConnectResult<T>>>
    async fn server_stream(&self, ctx: Ctx, req: Request<Req>)
        -> ConnectResult<impl Stream<Item = ConnectResult<Res>>>;

    // Client-streaming: impl Stream<Item = ConnectResult<T>> → ConnectResult<Response<T>>
    async fn client_stream(&self, ctx: Ctx, reqs: impl Stream<Item = ConnectResult<Req>>)
        -> ConnectResult<Response<Res>>;

    // Bidi-streaming: impl Stream<Item = ...> → ConnectResult<impl Stream<Item = ...>>
    async fn bidi(&self, ctx: Ctx, reqs: impl Stream<Item = ConnectResult<Req>>)
        -> ConnectResult<impl Stream<Item = ConnectResult<Res>>>;
}
```

> **Note:** For server-streaming and bidi-streaming methods the code-first macro
> rewrites `impl Stream` return types to `Pin<Box<dyn Stream + Send>>` to satisfy
> `Send` bounds on the Router's handler futures. Your impl returns
> `Ok(Box::pin(stream))` — see the `code_first_streaming` example.

#### Codec selection in code-first

The `codecs(...)` argument controls which `ProcedureCodecs` expression is
generated in the registration function and client constructor:

| `codecs(...)` | Generated `ProcedureCodecs` expression | Message type requirements |
|---|---|---|
| `codecs(json)` (default) | `ProcedureCodecs::of((JsonCodec,))` | `Serialize + DeserializeOwned` |
| `codecs(proto)` | `ProcedureCodecs::defaults()` | `buffa::Message + Serialize + DeserializeOwned` |
| `codecs(arrow)` | `ProcedureCodecs::only(ArrowCodec)` | `ToArrow + FromArrow` |
| `codecs(json, arrow)` | `ProcedureCodecs::of((JsonCodec, ArrowCodec))` | serde + arrow bounds |

---

### Generated artifacts — unified reference

All three modes generate the same set of artifacts. Here they are for a service
named `GreetService` in package `demo.greet.v1` with one RPC method `Greet`:

```
// ── R1: Procedure path constants ──────────────────────────────────────────
pub mod procedure {
    /// Procedure path — always includes a leading slash.
    pub const GREET: &str = "/demo.greet.v1.GreetService/Greet";
}

// ── R2: Service name constant ─────────────────────────────────────────────
pub const GREETSERVICE_NAME: &str = "demo.greet.v1.GreetService";

// ── R3: Service trait ─────────────────────────────────────────────────────
pub trait GreetService: Send + Sync + 'static {
    async fn greet(
        &self,
        ctx: foundation_connectrpc::Ctx,
        req: foundation_connectrpc::Request<GreetRequest>,
    ) -> foundation_connectrpc::ConnectResult<foundation_connectrpc::Response<GreetResponse>>;
}

// ── R4: Registration function ─────────────────────────────────────────────
pub fn register_greet_service<S: GreetService>(
    router: &mut foundation_connectrpc::Router,
    service: std::sync::Arc<S>,
) {
    // Calls router.unary(procedure::GREET, codecs, handler, opts) for each method
}

// ── R5: Unimplemented handler ─────────────────────────────────────────────
pub struct UnimplementedGreetServiceHandler;
impl GreetService for UnimplementedGreetServiceHandler {}

// ── R6: Typed client ──────────────────────────────────────────────────────
pub struct GreetServiceClient {
    greet: foundation_connectrpc::Client<GreetRequest, GreetResponse>,
}
impl GreetServiceClient {
    pub fn new(
        transport: Arc<dyn foundation_connectrpc::Transport>,
        base_url: &str,
        options: foundation_connectrpc::ClientOptions,
    ) -> foundation_connectrpc::ConnectResult<Self>;

    pub async fn greet(
        &self,
        ctx: foundation_connectrpc::Ctx,
        request: foundation_connectrpc::Request<GreetRequest>,
    ) -> foundation_connectrpc::ConnectResult<foundation_connectrpc::Response<GreetResponse>>;
}

// ── R7: Client trait ──────────────────────────────────────────────────────
pub trait GreetServiceClientExt: Send + Sync + 'static {
    async fn greet(
        &self,
        ctx: foundation_connectrpc::Ctx,
        request: foundation_connectrpc::Request<GreetRequest>,
    ) -> foundation_connectrpc::ConnectResult<foundation_connectrpc::Response<GreetResponse>>;
}
impl GreetServiceClientExt for GreetServiceClient { /* delegates to inner Client */ }

// ── R8: Descriptor macro (Mode 3 only) ────────────────────────────────────
#[macro_export]
macro_rules! greet_service_rpc_definitions { (common) => { ... }; (server) => { ... }; (client) => { ... }; }
```

> **Note:** The generated code uses the crate's **real path** as its prefix
> throughout (`foundation_connectrpc::Ctx`, `foundation_connectrpc::Router`, …), so
> the consuming crate just needs `foundation_connectrpc` as a dependency — no alias.

---

## Authentication (feature `auth`)

Port of authn-go's middleware onto `foundation_auth`. An async `AuthFunc` produces
a concrete `AuthInfo`; `AuthzInterceptor`, `JwtAuthenticator`,
`CompositeAuthenticator`, and `PerProcedureAuth` compose over it. Requires the
`auth` feature (on by default via `rpc`).

## Feature flags

| Feature | Effect |
|---|---|
| `rpc` | Full single-threaded library surface: all codecs + auth + all compressors |
| `rpc_multi` | `rpc` + the multi-threaded valtron executor (**default**) |
| `arrow` | Arrow columnar codec (`foundation_arrow`) |
| `auth` | Auth middleware (`foundation_auth`) |
| `zstd`, `brotli` | Extra compressors (gzip is always available) |

For a lean build, `default-features = false` plus `features = ["rpc"]` drops the
multi executor; dropping `rpc` in favour of individual flags drops
`foundation_arrow` / `foundation_auth`.

## Error model

Every RPC-surface signature returns `ConnectResult<T> = Result<T, ErrorTrace<ConnectError>>`
(Decision 03). `ConnectError` carries a Connect `Code`; domain layers below the
surface keep typed errors (`CodecError`, `CompressionError`, `TransportError`)
mapped in at the boundary. The full `errstacks` trace is preserved for logging and
telemetry.

## Built on

`foundation_core` (valtron execution + async readiness), `foundation_http` /
`foundation_netio` (HTTP server/client + wire types), `foundation_errstacks`
(error traces), `buffa` (protobuf), optional `foundation_arrow` (columnar) and
`foundation_auth` (authn).

## License

Apache-2.0