Rust 服务设计约束
用类型化 Rust 模型承载业务语义,并把 JSON、Serde、数据库和外部协议限制在适配器边界。
Hosted Service 负责构建和运行后端应用,但不会替应用决定内部代码质量。Rust 服务必须先定义类型所有权和 边界,再实现 HTTP、数据库、消息或 Artifact 适配器。
核心原则是:对象承载语义,JSON 只承载边界表示。 JSON 进入服务后必须立即转换为明确的 Rust 对象;领域和应用逻辑不得继续处理无类型 JSON。
强制边界
JSON 只允许存在于以下边界:
- HTTP 或消息入站:反序列化为请求 Contract DTO,并完成结构与语义校验。
- HTTP 或消息出站:把确定的响应 Contract DTO 序列化一次后发送。
- 数据库 JSON/TEXT 列:Repository 读取后立即反序列化为明确类型,写入前再从明确类型序列化。
- 不可变 Artifact:Artifact Adapter 按声明的契约版本读写类型化内容。
进入应用层后,以下写法禁止作为业务状态、用例参数或转换机制:
serde_json::Value
Map<String, serde_json::Value>
json!({ /* 业务对象 */ })
value.get("field")
serde_json::to_value(source).and_then(serde_json::from_value::<Target>)禁止递归扫描键名、删除未知字段后重试、连续反序列化猜测结构,或用 Value -> Value 实现“编译”和
转换。serde_json::Value 只可用于协议明确声明的不透明扩展字段;该字段必须有名称、Owner 和严格
SemVer schemaVersion,业务逻辑不得读取内部键。
Serde 不是业务转换器
Serde 只负责“字节与同一个类型”之间的编码和解码。两个业务对象之间的转换必须由显式编译器、应用服务
或 TryFrom / From 完成,不能先转成 JSON 再转成另一个类型。
类型必须有唯一职责
每个结构体或枚举在创建前必须归入一个类别:
| 类别 | 职责 | 示例命名 | 不得承担的职责 |
|---|---|---|---|
| Entity / Aggregate | 业务身份、不变量和状态变化 | Order | HTTP、SQL 或任意 JSON 键 |
| Value Object | 已校验的领域值 | OrderId、Money | 裸字符串兼容和隐式默认 |
| Command / Query | 应用用例输入 | CreateOrder | 直接映射数据库行 |
| Contract DTO | 版本化跨进程协议 | CreateOrderRequest | 仓储状态和内部生命周期 |
| Persistence Record | 数据库列投影 | OrderRecord | 领域行为和跨服务输出 |
| Adapter Payload | 第三方或框架局部载荷 | VendorOrderPayload | 在应用层长期流转 |
数据库行类型以 Record 或 Row 结尾,并在 Repository 内转换成领域对象。HTTP 请求和响应只定义在
入站适配器或独立 Contract crate 中。服务不得为了省一次转换,直接把数据库 Record 或可变领域 Entity
作为跨服务响应输出。
同一个协议只能有一个权威 Contract。多个服务共同消费时,应提取独立 Contract crate;无法共享时, 明确一个权威消费者模型,由生产者通过显式转换生成它。禁止复制字段相似但会独立演进的同名结构、枚举或 白名单。
目录和依赖方向
普通服务按职责组织,只有出现真实职责时才创建对应目录:
src/
├── main.rs # 组合根:配置、依赖注入、路由、启动
├── config.rs # 类型化配置读取与校验
├── error.rs # 顶层错误映射
├── domain/ # Entity、Value Object、领域错误
├── application/ # Command、Query、用例服务和 Port trait
├── contracts/ # 服务拥有的版本化入站/出站 Contract
└── adapters/
├── inbound/http/ # 认证上下文、解码、调用用例、响应映射
└── outbound/
├── persistence/ # Record 与 Repository 实现
├── http/ # 外部服务客户端
├── messaging/ # 消息协议边界
└── artifact/ # Artifact 类型与存取
tests/
├── contract/
└── integration/依赖方向必须指向稳定业务层:
main / composition root
-> inbound adapters -> application -> domain
-> outbound adapters ----^ ^
-> contractsdomain不依赖 Axum、SQLx、Reqwest、消息 SDK 或serde_json::Value。application只依赖领域类型和自身定义的 Port,不直接创建数据库连接池或 HTTP Client。- Adapter 实现 Port,并负责 Contract、Record、第三方 Payload 与领域对象之间的显式转换。
- 领域 Entity 不为了传输方便直接派生 Serde;只有权威 Contract 类型才定义序列化外形。
main.rs只组装依赖和启动进程,不放业务规则,也不执行数据回填或临时修复。- 不使用
utils、common或models汇集跨层职责;文件应只有一个清晰角色和主要变化原因。
入站转换示例
HTTP Adapter 先把 JSON 解码成 Contract DTO,再显式转换成应用 Command:
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct CreateOrderRequest {
contract_version: SemanticVersion,
customer_id: String,
amount_cents: i64,
}
struct CreateOrder {
customer_id: CustomerId,
amount: Money,
}
impl TryFrom<CreateOrderRequest> for CreateOrder {
type Error = ContractError;
fn try_from(request: CreateOrderRequest) -> Result<Self, Self::Error> {
ensure_supported_contract(&request.contract_version)?;
Ok(Self {
customer_id: CustomerId::parse(request.customer_id)?,
amount: Money::from_cents(request.amount_cents)?,
})
}
}示例中的 SemanticVersion、CustomerId 和 Money 都是经校验的值对象,不是类型别名。转换失败返回稳定
错误类型;Adapter 再把它映射成 HTTP 状态和 CModel 错误响应。领域层
不构造 HTTP 状态码。
反序列化错误必须包含字段路径。可使用 serde_path_to_error 或框架等价能力,把缺失字段、未知字段、
错误枚举和错误嵌套映射成稳定错误码;不得把原始用户 Payload、Token 或 Secret 写入错误和日志。
契约、Serde 与版本演进
Serde 属性本身就是协议:rename、tag、alias、default、flatten、untagged、可选字段和
deny_unknown_fields 都会改变调用方可接受或收到的数据外形。
- 新的闭合内部协议默认拒绝未知字段。
- 不因一次解析失败临时增加
alias、default、大小写归一化或平行枚举。 - 可空与缺省必须分别建模;
Option<T>不能同时模糊表达两种语义。 - 开放扩展只能使用有 Owner、有 Schema、有严格 SemVer 版本的显式 extension 字段。
- 所有 Contract、Schema、Artifact 和迁移版本只接受严格 SemVer 2.0.0,不能使用
v1、1.2、日期或 构建号代替版本。 - 不兼容变更必须发布新的主版本,并同步盘点和验证生产者、消费者及持久化数据迁移。
历史数据兼容必须通过有 Owner、可审计、可安全重跑且有删除条件的类型化迁移完成。不能把历史格式的 递归修补、双读或无期限回退留在业务运行时。数据库变更必须走 数据库迁移 Artifact,不得在 Handler、Worker、 启动钩子或随服务交付的临时 CLI 中加入一次性修复、回填或生产 SQL。
测试与 CI 门禁
涉及 Contract、Serde、数据库 JSON、Artifact 或模型转换的变更,至少包含:
- Contract 正向序列化和反序列化测试,固定字段名、Tag、必填性和版本。
- 未知字段、未知变体、缺失字段、非法版本和错误嵌套的负向测试,并断言字段路径。
TryFrom、From或显式编译器测试,覆盖每个业务变体与不变量。- 生产者和消费者共享 Contract 的契约测试或 Golden Fixture。
Record <-> domain、Artifact bytes <-> Contract的集成测试。cargo fmt --check、cargo clippy --all-targets --all-features -- -D warnings和适用范围的cargo test。
CI 应检查领域层和应用层是否出现动态 JSON。以下命令可用于定位,每个命中都必须删除,或证明它只位于 上述允许的不透明扩展边界:
rg -n 'serde_json::(Value|Map)|json!\s*\(' src/domain src/application
rg -n '\.get\("[^\"]+"\)' src/domain src/application
rg -n '#\[serde\((alias|default|flatten|untagged)' src提交构建前检查
- 每个类型都已明确归类为 Entity、Value Object、Command/Query、Contract、Record 或 Adapter Payload。
- JSON 只在 Adapter、Repository 或 Artifact 边界出现,并且只反序列化或序列化一次。
- 业务转换由显式
TryFrom、From、编译器或应用服务完成。 - 没有 Record、Entity 和 Contract 同名混用,也没有复制权威模型。
- Serde Tag、必填字段、未知字段策略和严格 SemVer 版本都有契约测试。
- domain/application 没有动态 JSON、键扫描、递归修补或解析重试。
- 配置、数据库连接、凭据、Endpoint 和业务目录数据来自 Environment 或权威 Provider,没有硬编码进源码。
- 业务二进制、路由、任务、Worker、启动流程和随服务交付的 CLI 中没有一次性维护代码。
- 格式化、Clippy、单元测试、契约测试和集成测试全部通过后,才从精确 Git commit 创建 BuildJob。