cf-modkit-macros 0.1.0

ModKit macros
Documentation
# ModKit Macros

Procedural macros for the ModKit framework, focused on generating strongly-typed gRPC clients with built-in SecurityCtx propagation.

## Overview

ModKit provides two macros for generating gRPC client implementations:

1. **`#[generate_clients]`** (RECOMMENDED) - Generate a gRPC client from an API trait definition with automatic SecurityCtx propagation
2. **`#[grpc_client]`** - Generate a gRPC client with manual trait implementation

## Quick Start

### Recommended: Using `generate_clients`

The `generate_clients` macro is applied to your API trait and automatically generates a strongly-typed gRPC client with full method delegation and automatic SecurityCtx propagation:

```rust
use modkit_macros::generate_clients;
use modkit_security::SecurityCtx;

#[generate_clients(
    grpc_client = "modkit_users_v1::users_service_client::UsersServiceClient<tonic::transport::Channel>"
)]
#[async_trait::async_trait]
pub trait UsersApi: Send + Sync {
    async fn get_user(&self, ctx: &SecurityCtx, req: GetUserRequest) 
        -> Result<UserResponse, UsersError>;
    
    async fn list_users(&self, ctx: &SecurityCtx, req: ListUsersRequest) 
        -> Result<Vec<UserResponse>, UsersError>;
}
```

This generates:

- The original `UsersApi` trait (unchanged)
- `UsersApiGrpcClient` - wraps the tonic client with:
  - Automatic proto ↔ domain type conversions
  - Automatic SecurityCtx propagation via gRPC metadata
  - Standard transport stack (timeouts, retries, metrics, tracing)

The client fully implements the `UsersApi` trait with automatic method delegation.

#### Usage

```rust
// Connect to gRPC service
let client = UsersApiGrpcClient::connect("http://localhost:50051").await?;

// SecurityCtx is automatically propagated via gRPC metadata
let ctx = SecurityCtx::for_user(user_id);
let user = client.get_user(&ctx, GetUserRequest { id: "123" }).await?;

// Or with custom configuration
let config = GrpcClientConfig::new("users_service")
    .with_connect_timeout(Duration::from_secs(5))
    .with_rpc_timeout(Duration::from_secs(15));
    
let client = UsersApiGrpcClient::connect_with_config(
    "http://localhost:50051",
    &config
).await?;
```

### Alternative: Manual `#[grpc_client]`

If you need more control, you can use the `grpc_client` macro which generates the struct and helpers, but requires manual trait implementation:

```rust
use modkit_macros::grpc_client;

#[grpc_client(
    api = "crate::contracts::UsersApi",
    tonic = "modkit_users_v1::users_service_client::UsersServiceClient<tonic::transport::Channel>",
    package = "modkit.users.v1"
)]
pub struct UsersGrpcClient;

// You must manually implement the trait
#[async_trait::async_trait]
impl UsersApi for UsersGrpcClient {
    async fn get_user(&self, req: GetUserRequest) -> anyhow::Result<UserResponse> {
        let mut client = self.inner_mut();
        let request = tonic::Request::new(req.into());
        let response = client.get_user(request).await?;
        Ok(response.into_inner().into())
    }
    // ... other methods
}
```

## API Requirements

All API traits used with these macros must follow strict signature rules:

1. **Async methods**: All trait methods must be `async`
2. **Standard receiver**: Methods must use `&self` (not `&mut self` or `self`)
3. **Result return type**: Methods must return `Result<T, E>` with two type parameters
4. **Parameter patterns**: Methods must use one of two patterns:

### Pattern 1: Secured API (with SecurityCtx)

For APIs that require authorization and access control:

```rust
async fn method_name(
    &self,
    ctx: &SecurityCtx,
    req: RequestType,
) -> Result<ResponseType, ErrorType>;
```

The `SecurityCtx` parameter:
- Must be the **first** parameter after `&self`
- Must be an immutable reference (`&SecurityCtx`, not `&mut SecurityCtx`)
- The type must be named `SecurityCtx` (from `modkit_security::SecurityCtx` or aliased)

### Pattern 2: Unsecured API (without SecurityCtx)

For system-internal APIs that don't require user authorization:

```rust
async fn method_name(
    &self,
    req: RequestType,
) -> Result<ResponseType, ErrorType>;
```

### Valid Secured API Trait

```rust
use modkit_security::SecurityCtx;

#[async_trait::async_trait]
pub trait MyApi: Send + Sync {
    async fn get_item(&self, ctx: &SecurityCtx, req: GetItemRequest) 
        -> Result<ItemResponse, MyError>;
    
    async fn list_items(&self, ctx: &SecurityCtx, req: ListItemsRequest) 
        -> Result<Vec<ItemResponse>, MyError>;
}
```

### Valid Unsecured API Trait

```rust
#[async_trait::async_trait]
pub trait SystemApi: Send + Sync {
    async fn resolve_service(&self, name: String) 
        -> Result<Endpoint, SystemError>;
}
```

### How SecurityCtx Propagates

For secured APIs (with `ctx: &SecurityCtx`), the generated gRPC client:

1. **Client-side**: Serializes the `SecurityCtx` into gRPC metadata headers before sending the request
2. **Server-side**: The gRPC server extracts the `SecurityCtx` from metadata and passes it to your service
3. **Automatic**: No manual header management required

Example generated code:

```rust
async fn get_user(&self, ctx: &SecurityCtx, req: GetUserRequest) 
    -> Result<UserResponse, UsersError> 
{
    let mut client = self.inner.clone();
    let mut request = tonic::Request::new(req.into());
    
    // Automatically attach SecurityCtx to gRPC metadata
    modkit_transport_grpc::attach_secctx(request.metadata_mut(), ctx)?;
    
    let response = client.get_user(request).await?;
    Ok(response.into_inner().into())
}
```

### Invalid API Traits

```rust
// ❌ NOT async
fn get_item(&self, req: GetItemRequest) -> anyhow::Result<ItemResponse>;

// ❌ Multiple parameters after request
async fn get_item(&self, ctx: &SecurityCtx, id: String, name: String) 
    -> anyhow::Result<ItemResponse>;

// ❌ Wrong parameter order (request before ctx)
async fn get_item(&self, req: GetItemRequest, ctx: &SecurityCtx) 
    -> anyhow::Result<ItemResponse>;

// ❌ Mutable SecurityCtx reference
async fn get_item(&self, ctx: &mut SecurityCtx, req: GetItemRequest) 
    -> anyhow::Result<ItemResponse>;

// ❌ Not returning Result
async fn get_item(&self, req: GetItemRequest) -> ItemResponse;

// ❌ Mutable receiver
async fn get_item(&mut self, req: GetItemRequest) -> anyhow::Result<ItemResponse>;
```

## Generated Code Structure

Given a trait `UsersApi`, the `generate_clients` macro generates:

```rust
// Original trait (unchanged)
#[async_trait::async_trait]
pub trait UsersApi: Send + Sync {
    async fn get_user(&self, req: GetUserRequest) -> anyhow::Result<UserResponse>;
}

// gRPC client struct
pub struct UsersApiGrpcClient {
    inner: UsersServiceClient<tonic::transport::Channel>,
}

impl UsersApiGrpcClient {
    /// Connect with default configuration
    pub async fn connect(uri: impl Into<String>) -> anyhow::Result<Self> { /* ... */ }
    
    /// Connect with custom configuration
    pub async fn connect_with_config(
        uri: impl Into<String>,
        cfg: &GrpcClientConfig
    ) -> anyhow::Result<Self> { /* ... */ }
    
    /// Create from an existing channel
    pub fn from_channel(channel: tonic::transport::Channel) -> Self { /* ... */ }
}

#[async_trait::async_trait]
impl UsersApi for UsersApiGrpcClient {
    async fn get_user(&self, req: GetUserRequest) -> anyhow::Result<UserResponse> {
        let mut client = self.inner.clone();
        let request = tonic::Request::new(req.into());
        let response = client.get_user(request).await?;
        Ok(response.into_inner().into())
    }
}
```

## Transport Stack

All generated gRPC clients automatically use the standardized transport stack from `modkit-transport-grpc`, which provides:

- **Configurable timeouts**: Separate timeouts for connection establishment and individual RPC calls
- **Retry logic**: Automatic retry with exponential backoff for transient failures
- **Metrics collection**: Built-in Prometheus metrics for monitoring
- **Distributed tracing**: OpenTelemetry integration for request tracing

### Default Configuration

- Connect timeout: 10 seconds
- RPC timeout: 30 seconds
- Max retries: 3 attempts
- Base backoff: 100ms
- Max backoff: 5 seconds
- Metrics and tracing: Enabled

### Custom Configuration

```rust
use modkit_transport_grpc::client::GrpcClientConfig;

let config = GrpcClientConfig::new("my_service")
    .with_connect_timeout(Duration::from_secs(5))
    .with_rpc_timeout(Duration::from_secs(15))
    .with_max_retries(5)
    .without_metrics();

let client = UsersApiGrpcClient::connect_with_config(
    "http://localhost:50051",
    &config
).await?;
```

### Bypassing the Transport Stack

For testing or custom channel setup:

```rust
let channel = Channel::from_static("http://localhost:50051")
    .connect()
    .await?;

let client = UsersApiGrpcClient::from_channel(channel);
```

## Type Conversions

The generated gRPC client requires:

- Each request type `Req` implements `Into<ProtoReq>` where `ProtoReq` is the corresponding protobuf message
- Each response type `Resp` implements `From<ProtoResp>` where `ProtoResp` is the tonic response message

Example:

```rust
// Domain type
pub struct GetUserRequest {
    pub id: String,
}

// Conversion to protobuf
impl From<GetUserRequest> for proto::GetUserRequest {
    fn from(req: GetUserRequest) -> Self {
        proto::GetUserRequest { id: req.id }
    }
}

// Response conversion
impl From<proto::UserResponse> for UserResponse {
    fn from(proto: proto::UserResponse) -> Self {
        UserResponse {
            id: proto.id,
            name: proto.name,
        }
    }
}
```

If these conversions are missing, the code will not compile (by design).

## Best Practices

1. **Use `generate_clients` when possible** - It provides the most automated experience
2. **Keep API traits focused** - Each trait should represent a cohesive set of operations
3. **Use descriptive names** - Client structs are named after your trait (e.g., `UsersApi``UsersApiGrpcClient`)
4. **Implement type conversions** - Ensure domain types convert to/from protobuf
5. **Leverage trait objects** - Enables polymorphism via `Arc<dyn YourTrait>`

## Troubleshooting

### "generate_clients requires `grpc_client` parameter"

Ensure you provide the `grpc_client` parameter:

```rust
#[generate_clients(
    grpc_client = "path::to::TonicClient<Channel>"
)]
```

### "API methods must be async"

All trait methods must be marked `async`.

### "API methods must have exactly one parameter (besides &self)"

If you have multiple parameters, wrap them in a request struct:

```rust
// Instead of this:
async fn update(&self, id: String, name: String) -> Result<(), Error>;

// Use this:
#[derive(Clone)]
pub struct UpdateRequest {
    pub id: String,
    pub name: String,
}

async fn update(&self, req: UpdateRequest) -> Result<(), Error>;
```

### Missing Into/From implementations

Ensure you implement the required conversions between domain and proto types.

## See Also

- [ModKit Documentation]../../docs/
- [API Guidelines]../../guidelines/API_GUIDELINE.md
- [Module Creation]../../guidelines/NEW_MODULE.md