| name | derive-usage |
| description | Guide for using genies_derive procedural macros. Use when implementing DDD aggregates with |
Procedural Macros (genies_derive)
Overview
genies_derive 提供 7 个核心过程宏,用于简化 DDD + Dapr + Casbin 应用开发。纯过程宏库,无运行时依赖。
核心宏:
#[derive(Aggregate)] - 聚合根派生,实现 AggregateType/WithAggregateId/InitializeAggregate
#[derive(DomainEvent)] - 领域事件派生,实现 DomainEvent trait
#[derive(Config)] - 配置派生,支持 YAML + ENV 加载
#[derive(ConfigCore)] - 内部配置派生(避免循环依赖)
#[topic(...)] - Dapr topic 消费,Redis 幂等
#[remote(...)] - HTTP 请求包装,JWT 自动刷新
#[casbin] - 字段级权限控制,自动嵌套检测
Dependencies
[dependencies]
genies_derive = { workspace = true }
genies = { workspace = true }
Macro 1: Aggregate
聚合根派生,实现 DDD 聚合类型标识和 ID 访问。
Attributes
| Attribute | Required | Description |
|---|
#[aggregate_type("Name")] | No | 覆盖聚合类型名(默认:struct 名) |
#[id_field(field)] | No | 指定 ID 字段,生成 WithAggregateId |
#[initialize_with_defaults] | No | 生成 InitializeAggregate(需要 id_field) |
Example
use genies_derive::Aggregate;
use serde::{Deserialize, Serialize};
#[derive(Aggregate, Serialize, Deserialize, Default)]
#[aggregate_type("Order")]
#[id_field(id)]
#[initialize_with_defaults]
pub struct Order {
pub id: String,
pub status: String,
pub total: f64,
}
Macro 2: DomainEvent
领域事件派生,支持 struct 和 enum。
Attributes
| Attribute | Required | Description |
|---|
#[event_type("Name")] | No | 事件类型名(默认:struct/variant 名) |
#[event_type_version("V1")] | No | 版本(默认:"V0") |
#[event_source("service")] | No | 来源标识(默认:"") |
Struct Example
use genies_derive::DomainEvent;
use serde::{Deserialize, Serialize};
#[derive(DomainEvent, Serialize, Deserialize, Default)]
#[event_type("OrderCreated")]
#[event_type_version("V1")]
#[event_source("order-service")]
pub struct OrderCreatedEvent {
pub order_id: String,
pub customer_id: String,
}
Enum Example
#[derive(DomainEvent, Serialize, Deserialize)]
#[event_type_version("V1")]
pub enum OrderEvent {
#[event_type("OrderCreated")]
Created { order_id: String },
#[event_type("OrderShipped")]
Shipped { tracking: String },
}
Macro 3: Config
配置派生,从 YAML + 环境变量加载。
Field Attribute
| Attribute | Description |
|---|
#[config(default = "value")] | 默认值 |
Example
use genies_derive::Config;
use serde::{Deserialize, Serialize};
#[derive(Config, Debug, Deserialize, Serialize)]
pub struct AppConfig {
#[config(default = "localhost")]
pub host: String,
#[config(default = "8080")]
pub port: u16,
#[config(default = "t1,t2")]
pub topics: Vec<String>,
pub db_url: Option<String>,
}
let config = AppConfig::from_sources("application.yml")?;
Generated Methods
default() - 使用默认值创建
from_file(path) - 从 YAML 加载
from_sources(path) - YAML + ENV 综合加载(推荐)
load_env(&mut self) - 从环境变量覆盖
merge(&mut self, other) - 合并配置
validate(&self) - 验证配置
ENV Support
export host="prod.example.com"
export HOST="prod.example.com"
Macro 4: ConfigCore
与 Config 相同,但使用 genies_core::error::ConfigError。用于框架内部避免循环依赖。
Macro 5: #[topic]
Dapr topic 消费宏,自动生成 Salvo handler + Redis 幂等。
Attributes
| Attribute | Required | Default | Description |
|---|
name = "..." | No | aggregate type | Topic 名称 |
pubsub = "..." | No | "messagebus" | PubSub 组件名 |
metadata = "k=v,..." | No | - | Topic 元数据 |
Example
use genies_derive::topic;
use rbatis::executor::Executor;
#[topic(name = "order-events", pubsub = "messagebus")]
pub async fn handle_order_created(
tx: &mut dyn Executor,
event: OrderCreatedEvent,
) -> anyhow::Result<u64> {
Order::insert(tx, &order).await?;
Ok(1)
}
Generated Functions
handle_order_created(tx, event) - 业务逻辑
handle_order_created_hoop - Salvo handler(#[handler])
handle_order_created_dapr() - 返回 DaprTopicSubscription
handle_order_created_hoop_router() - Salvo Router
Idempotency Flow
Message → CloudEvent 解析 → event_type 匹配
↓
Redis key: {server}-{handler}-{event_type}-{id}
↓
SET NX (原子) → 处理 → SET CONSUMED
↓
失败自动 rollback,删除 key,Dapr 重发
Macro 6: #[remote]
feignhttp 请求包装,自动管理 Keycloak JWT Token(401 时自动刷新并重试)。类似 Java 的 @FeignClient。
目录结构
remote 模块独立于 DDD 四层架构,按外部服务分文件:
src/remote/
├── mod.rs # 模块导出
├── patient_service.rs # 患者服务远程调用
├── baseinfo_service.rs # 基础信息服务远程调用
└── his_service.rs # HIS 系统远程调用
在 lib.rs 中声明 pub mod remote;。
基本语法
use once_cell::sync::Lazy;
use genies_derive::remote;
use serde::{Deserialize, Serialize};
pub static BaseInfo: Lazy<String> = genies::config_gateway!("/baseinfo");
pub static Patient: Lazy<String> = genies::config_gateway!("/patient");
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CustomConfigModel {
pub id: Option<String>,
pub name: Option<String>,
}
参数注解
| 注解 | 说明 | 示例 |
|---|
#[query] | 查询参数(?key=value) | #[query] name: &str |
#[path] | 路径参数(/api/{id}) | #[path] id: &str |
#[body] | 请求体(JSON) | #[body] body: UserDTO |
GET 查询参数示例
#[remote]
#[get(url = BaseInfo, path = "/customconfig/depttypename")]
pub async fn findByDepartmentIdAndTypeName(
#[query] departmentId: &str,
#[query] typeName: &str,
) -> feignhttp::Result<Vec<CustomConfigModel>> { impled!() }
GET 路径参数示例
#[remote]
#[get(url = Patient, path = "/api/patient/id/{id}")]
pub async fn get_patient_by_id(
#[path] id: &str,
) -> feignhttp::Result<PatientInfo> { impled!() }
POST 请求体示例
#[remote]
#[post(url = Patient, path = "/api/patient/create")]
pub async fn create_patient(
#[body] patient: PatientCreateDTO,
) -> feignhttp::Result<String> { impled!() }
关键要点
config_gateway! 宏:genies::config_gateway!("/service-prefix") 生成 Lazy<String>,值为 ${gateway}/service-prefix,gateway 从 application.yml 配置读取
url + path 分离:url 引用 Lazy<String> 静态变量(服务基础路径),path 是具体端点路径
- 函数体:必须写
impled!()(feignhttp 宏要求)
- 参数类型:用
&str 而非 String
Generated
pub async fn get_patient_by_id_feignhttp(
#[header] Authorization: &str,
#[path] id: &str,
) -> feignhttp::Result<PatientInfo>
pub async fn get_patient_by_id(id: &str) -> feignhttp::Result<PatientInfo> {
}
与 Java FeignClient 的对照
| Java FeignClient | Rust/Genies #[remote] |
|---|
@FeignClient(name = "patient-service") | pub static Patient: Lazy<String> = genies::config_gateway!("/patient"); |
@GetMapping("/api/patient/{id}") | #[get(url = Patient, path = "/api/patient/{id}")] |
@RequestParam String name | #[query] name: &str |
@PathVariable String id | #[path] id: &str |
@RequestBody UserDTO body | #[body] body: UserDTO |
| Spring Security OAuth2 Token | #[remote] 自动管理 Keycloak Token |
Macro 7: #[casbin]
字段级权限控制,自动生成 Serialize + Writer。
Usage
use genies_derive::casbin;
use serde::Deserialize;
use salvo::oapi::ToSchema;
#[casbin]
#[derive(Deserialize, ToSchema)]
pub struct User {
pub id: u64,
pub name: String,
pub email: String,
pub address: Address,
pub accounts: Vec<BankAccount>,
}
Auto Nested Detection
宏自动识别非原始类型:
Address → 递归过滤
Option<Address> → 非 null 时递归
Vec<BankAccount> → 遍历每项递归
Generated
impl User {
pub fn casbin_filter(
value: &mut serde_json::Value,
enforcer: &casbin::Enforcer,
subject: &str,
) { ... }
}
impl salvo::writing::Writer for User {
}
casbin_filter 方法签名:
pub fn casbin_filter(value: &mut serde_json::Value, enforcer: &casbin::Enforcer, subject: &str)
value — 待过滤的 JSON Value(会被原地修改,deny 的字段被移除)
enforcer — Casbin Enforcer 引用
subject — 当前用户标识(对应 casbin_rules 中的 v0)
⚠️ 与 RespVO 配合使用
Writer trait 只在 handler 直接返回 Json<T> 或 T 时自动触发。当返回 Json<RespVO<T>> 时,RespVO 的 Writer 接管序列化,T 的 Writer 不会被调用,字段过滤不会自动生效。
手动调用模式:
use salvo::prelude::*;
use genies_core::RespVO;
#[endpoint]
async fn get_user(depot: &mut Depot) -> Json<RespVO<User>> {
let user = fetch_user().await;
let enforcer = depot.obtain::<std::sync::Arc<casbin::Enforcer>>().ok();
let subject = depot.get::<String>("subject").ok();
let mut value = serde_json::to_value(&user).unwrap();
if let (Some(e), Some(s)) = (&enforcer, &subject) {
User::casbin_filter(&mut value, e.as_ref(), s.as_str());
}
let filtered: User = serde_json::from_value(value).unwrap();
Json(RespVO::from(&Ok::<_, String>(filtered)))
}
参数类型转换:
e.as_ref() — Arc<Enforcer> → &Enforcer
s.as_str() — String → &str
Policy Examples
INSERT INTO casbin_rules (ptype,v0,v1,v2,v3)
VALUES ('p','bob','User.email','read','deny');
INSERT INTO casbin_rules (ptype,v0,v1,v2,v3)
VALUES ('p','guest','User.phone','read','deny');
Debug Mode
启用 debug_mode 打印生成代码:
[dependencies]
genies_derive = { path = "...", features = ["debug_mode"] }
Key Files