# irpc: The rpc_requests Macro The `#[rpc_requests]` attribute macro is the primary way to define an irpc protocol. It generates the boilerplate for channel typing, message wrapping, and service trait implementations. ## Basic Usage ```rust use irpc::{channel::{mpsc, oneshot}, rpc_requests, Client, WithChannels}; use serde::{Deserialize, Serialize}; #[rpc_requests(message = ComputeMessage)] #[derive(Debug, Serialize, Deserialize)] enum ComputeProtocol { /// Unary RPC: one request, one response #[rpc(tx=oneshot::Sender)] #[wrap(Multiply)] Multiply(i64, i64), /// Bidirectional streaming #[rpc(tx=mpsc::Sender, rx=mpsc::Receiver)] #[wrap(Sum)] Sum, } ``` This single macro invocation generates: 1. **Wrapper structs** (from `#[wrap]`): `Multiply` and `Sum` struct types 2. **`Channels` impls**: For each variant's inner type, specifying `Tx` and `Rx` 3. **`Service` impl**: `impl Service for ComputeProtocol { type Message = ComputeMessage; }` 4. **`RemoteService` impl** (rpc feature): Maps protocol variants + QUIC streams to messages 5. **`ComputeMessage` enum**: Wraps each request in `WithChannels` 6. **`From` conversions**: Between inner types, `ComputeProtocol`, and `ComputeMessage` ## Macro Arguments ### Top-level (on the enum) | Argument | Required | Description | |---|---|---| | `message = Name` | Recommended | Name of the generated message enum. Also generates `Service` and `RemoteService` impls. | | `alias = "Suffix"` | Optional | Generates type aliases like `MultiplyMsg = WithChannels` | | `rpc_feature = "feat"` | Optional | Feature-gates the `RemoteService` impl with `#[cfg(feature = "feat")]` | | `no_rpc` | Optional | Skips generating `RemoteService` impl entirely | | `no_spans` | Optional | Skips span-related code (for use without the `spans` feature) | ### Per-variant #### `#[rpc(tx=Type, rx=Type)]` Specifies channel types for each request: - `tx` — response channel type (server → client). Defaults to `NoSender`. - `rx` — update channel type (client → server). Defaults to `NoReceiver`. Valid types: - `oneshot::Sender` — single response - `mpsc::Sender` — streaming response - `oneshot::Receiver` — not valid as tx (use for rx pattern) - `mpsc::Receiver` — streaming updates (client → server) - `NoSender` / `NoReceiver` — no channel in that direction #### `#[wrap(TypeName, derive(Traits))]` Generates a struct from the variant's fields: - `TypeName` — name of the generated struct - Optional visibility prefix (e.g., `pub(crate) TypeName`) - `derive(...)` — additional derive macros beyond the default `Serialize, Deserialize, Debug` If `#[wrap]` is not used, each variant must have exactly one unnamed field (a named type). ## Generated Code Walkthrough Given this input: ```rust #[rpc_requests(message = StoreMessage)] #[derive(Debug, Serialize, Deserialize)] enum StoreProtocol { #[rpc(tx=oneshot::Sender)] #[wrap(GetRequest, derive(Clone))] Get(String), #[rpc(tx=oneshot::Sender<()>)] #[wrap(SetRequest)] Set { key: String, value: String }, } ``` The macro generates: ### 1. Wrapper Structs ```rust #[derive(Debug, Serialize, Deserialize, Clone)] pub GetRequest(pub String); #[derive(Debug, Serialize, Deserialize)] pub SetRequest { pub key: String, pub value: String } ``` The variants are rewritten to use these: ```rust enum StoreProtocol { Get(GetRequest), Set(SetRequest), } ``` ### 2. Channels Implementations ```rust impl Channels for GetRequest { type Tx = oneshot::Sender; type Rx = NoReceiver; } impl Channels for SetRequest { type Tx = oneshot::Sender<()>; type Rx = NoReceiver; } ``` ### 3. Message Enum ```rust #[doc = "Message enum for [`StoreProtocol`]"] #[allow(missing_docs)] #[derive(Debug)] pub enum StoreMessage { Get(WithChannels), Set(WithChannels), } ``` ### 4. Service Implementation ```rust impl Service for StoreProtocol { type Message = StoreMessage; } ``` ### 5. RemoteService Implementation (rpc feature) ```rust impl RemoteService for StoreProtocol { fn with_remote_channels( self, rx: noq::RecvStream, tx: noq::SendStream, ) -> Self::Message { match self { StoreProtocol::Get(msg) => { StoreMessage::from(WithChannels::from((msg, tx, rx))) } StoreProtocol::Set(msg) => { StoreMessage::from(WithChannels::from((msg, tx, rx))) } } } } ``` ### 6. From Conversions ```rust // Inner type → Protocol enum impl From for StoreProtocol { ... } impl From for StoreProtocol { ... } // WithChannels → Message enum impl From> for StoreMessage { ... } impl From> for StoreMessage { ... } ``` ### 7. parent_span Method (spans feature) ```rust impl StoreMessage { pub fn parent_span(&self) -> tracing::Span { let span = match self { StoreMessage::Get(inner) => inner.parent_span_opt(), StoreMessage::Set(inner) => inner.parent_span_opt(), }; span.cloned().unwrap_or_else(|| tracing::Span::current()) } } ``` ## Interaction Pattern Mapping The `#[rpc]` attribute maps directly to gRPC-like patterns: | Pattern | `tx` type | `rx` type | Example | |---|---|---|---| | **Unary RPC** | `oneshot::Sender` | `NoReceiver` | Get by key, return value | | **Server streaming** | `mpsc::Sender` | `NoReceiver` | List all items | | **Client streaming** | `oneshot::Sender` | `mpsc::Receiver` | Upload items, get count | | **Bidirectional** | `mpsc::Sender` | `mpsc::Receiver` | Chat, live updates | | **Notify (fire & forget)** | `NoSender` | `NoReceiver` | Log event | ## Client Methods Generated by Patterns The `Client` methods correspond to channel types: ```rust // Unary RPC: tx=oneshot::Sender, rx=NoReceiver client.rpc(Get { key: "x" }).await // → Result // Server streaming: tx=mpsc::Sender, rx=NoReceiver client.server_streaming(List, 16).await // → Result> // Client streaming: tx=oneshot::Sender, rx=mpsc::Receiver client.client_streaming(SetMany, 4).await // → Result<(mpsc::Sender, oneshot::Receiver)> // Bidirectional: tx=mpsc::Sender, rx=mpsc::Receiver client.bidi_streaming(Sum, 4, 4).await // → Result<(mpsc::Sender, mpsc::Receiver)> // Notify: tx=NoSender, rx=NoReceiver client.notify(Log { msg: "hi" }).await // → Result<()> ``` ## Manual Protocol Definition (Without Macro) You can define protocols manually instead of using the macro: ```rust use irpc::{channel::{mpsc, none::NoReceiver, oneshot}, Channels, Service, WithChannels}; use serde::{Deserialize, Serialize}; // 1. Define request types #[derive(Debug, Serialize, Deserialize)] struct Get { key: String } #[derive(Debug, Serialize, Deserialize)] struct Set { key: String, value: String } // 2. Implement Channels for each type impl Channels for Get { type Tx = oneshot::Sender>; type Rx = NoReceiver; } impl Channels for Set { type Tx = oneshot::Sender<()>; type Rx = NoReceiver; } // 3. Define protocol enum #[derive(derive_more::From, Serialize, Deserialize, Debug)] enum StorageProtocol { Get(Get), Set(Set), } // 4. Define message enum #[derive(derive_more::From)] enum StorageMessage { Get(WithChannels), Set(WithChannels), } // 5. Implement Service impl Service for StorageProtocol { type Message = StorageMessage; } // 6. Implement RemoteService (rpc feature) impl RemoteService for StorageProtocol { fn with_remote_channels(self, rx: noq::RecvStream, tx: noq::SendStream) -> Self::Message { match self { StorageProtocol::Get(msg) => WithChannels::from((msg, tx, rx)).into(), StorageProtocol::Set(msg) => WithChannels::from((msg, tx, rx)).into(), } } } ``` This manual approach gives full control but requires more boilerplate. The macro generates all of this automatically.