| Crates.io | cf-modkit-macros |
| lib.rs | cf-modkit-macros |
| version | 0.1.0 |
| created_at | 2026-01-25 13:59:41.468174+00 |
| updated_at | 2026-01-25 13:59:41.468174+00 |
| description | ModKit macros |
| homepage | |
| repository | https://github.com/hypernetix/hyperspot |
| max_upload_size | |
| id | 2068784 |
| size | 80,996 |
Procedural macros for the ModKit framework, focused on generating strongly-typed gRPC clients with built-in SecurityCtx propagation.
ModKit provides two macros for generating gRPC client implementations:
#[generate_clients] (RECOMMENDED) - Generate a gRPC client from an API trait definition with automatic SecurityCtx propagation#[grpc_client] - Generate a gRPC client with manual trait implementationgenerate_clientsThe 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:
UsersApi trait (unchanged)UsersApiGrpcClient - wraps the tonic client with:
The client fully implements the UsersApi trait with automatic method delegation.
// 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?;
#[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
}
All API traits used with these macros must follow strict signature rules:
async&self (not &mut self or self)Result<T, E> with two type parametersFor APIs that require authorization and access control:
async fn method_name(
&self,
ctx: &SecurityCtx,
req: RequestType,
) -> Result<ResponseType, ErrorType>;
The SecurityCtx parameter:
&self&SecurityCtx, not &mut SecurityCtx)SecurityCtx (from modkit_security::SecurityCtx or aliased)For system-internal APIs that don't require user authorization:
async fn method_name(
&self,
req: RequestType,
) -> Result<ResponseType, ErrorType>;
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>;
}
#[async_trait::async_trait]
pub trait SystemApi: Send + Sync {
async fn resolve_service(&self, name: String)
-> Result<Endpoint, SystemError>;
}
For secured APIs (with ctx: &SecurityCtx), the generated gRPC client:
SecurityCtx into gRPC metadata headers before sending the requestSecurityCtx from metadata and passes it to your serviceExample 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())
}
// ❌ 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>;
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())
}
}
All generated gRPC clients automatically use the standardized transport stack from modkit-transport-grpc, which provides:
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?;
For testing or custom channel setup:
let channel = Channel::from_static("http://localhost:50051")
.connect()
.await?;
let client = UsersApiGrpcClient::from_channel(channel);
The generated gRPC client requires:
Req implements Into<ProtoReq> where ProtoReq is the corresponding protobuf messageResp implements From<ProtoResp> where ProtoResp is the tonic response messageExample:
// 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).
generate_clients when possible - It provides the most automated experienceUsersApi → UsersApiGrpcClient)Arc<dyn YourTrait>grpc_client parameter"Ensure you provide the grpc_client parameter:
#[generate_clients(
grpc_client = "path::to::TonicClient<Channel>"
)]
All trait methods must be marked async.
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>;
Ensure you implement the required conversions between domain and proto types.