cf-modkit-macros

Crates.iocf-modkit-macros
lib.rscf-modkit-macros
version0.1.0
created_at2026-01-25 13:59:41.468174+00
updated_at2026-01-25 13:59:41.468174+00
descriptionModKit macros
homepage
repositoryhttps://github.com/hypernetix/hyperspot
max_upload_size
id2068784
size80,996
Artifizer (Artifizer)

documentation

README

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:

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

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

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:

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:

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

Valid Secured API Trait

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

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

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

// ❌ 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:

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

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:

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:

// 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., UsersApiUsersApiGrpcClient)
  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:

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

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

Commit count: 503

cargo fmt