# Rust 服务设计约束

用类型化 Rust 模型承载业务语义，并把 JSON、Serde、数据库和外部协议限制在适配器边界。

Hosted Service 负责构建和运行后端应用，但不会替应用决定内部代码质量。Rust 服务必须先定义类型所有权和
边界，再实现 HTTP、数据库、消息或 Artifact 适配器。

核心原则是：**对象承载语义，JSON 只承载边界表示。** JSON 进入服务后必须立即转换为明确的 Rust
对象；领域和应用逻辑不得继续处理无类型 JSON。

## 强制边界

JSON 只允许存在于以下边界：

1. HTTP 或消息入站：反序列化为请求 Contract DTO，并完成结构与语义校验。
2. HTTP 或消息出站：把确定的响应 Contract DTO 序列化一次后发送。
3. 数据库 JSON/TEXT 列：Repository 读取后立即反序列化为明确类型，写入前再从明确类型序列化。
4. 不可变 Artifact：Artifact Adapter 按声明的契约版本读写类型化内容。

进入应用层后，以下写法禁止作为业务状态、用例参数或转换机制：

```rust
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；无法共享时，
明确一个权威消费者模型，由生产者通过显式转换生成它。禁止复制字段相似但会独立演进的同名结构、枚举或
白名单。

## 目录和依赖方向

普通服务按职责组织，只有出现真实职责时才创建对应目录：

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

依赖方向必须指向稳定业务层：

```text
main / composition root
  -> inbound adapters -> application -> domain
  -> outbound adapters ----^          ^
  -> contracts
```

- `domain` 不依赖 Axum、SQLx、Reqwest、消息 SDK 或 `serde_json::Value`。
- `application` 只依赖领域类型和自身定义的 Port，不直接创建数据库连接池或 HTTP Client。
- Adapter 实现 Port，并负责 Contract、Record、第三方 Payload 与领域对象之间的显式转换。
- 领域 Entity 不为了传输方便直接派生 Serde；只有权威 Contract 类型才定义序列化外形。
- `main.rs` 只组装依赖和启动进程，不放业务规则，也不执行数据回填或临时修复。
- 不使用 `utils`、`common` 或 `models` 汇集跨层职责；文件应只有一个清晰角色和主要变化原因。

### 文件、函数与复杂度上限

目录边界仍是主要设计约束；行数和复杂度是防止职责重新堆回单文件的强制护栏。所有行数都按
`rustfmt` 后的源码计算，生产代码、单元测试和集成测试使用同一标准：

| 检查对象                 |  提醒阈值 |    硬上限 |
| -------------------- | ----: | -----: |
| 普通 Rust 源文件          | 600 行 | 1000 行 |
| `main.rs` / `lib.rs` | 300 行 |  500 行 |
| 单个函数或方法              |  60 行 |  100 行 |
| 函数认知复杂度              |    15 |     25 |

超过提醒阈值时，应在当前评审中说明职责为何仍然单一；超过硬上限时，必须先按领域、用例、Contract 或
Adapter 边界拆分，不能继续合入或构建发布制品。`main.rs` 和 `lib.rs` 只能作为组合根与公开模块入口，
所以使用更严格的文件上限。

生成代码只有在生成器、输出目录和不可手工维护的属性都明确时才可排除。测试、示例、迁移工具和
`src/bin` 不是例外；禁止用移动到测试模块、宏、`include!`、`utils`，或随意增加
`#[allow(clippy::too_many_lines)]` / `#[allow(clippy::cognitive_complexity)]` 绕过门禁。

存量项目已经超限时，仓库可以登记逐文件、逐函数的精确债务 baseline，但必须同时满足：

- baseline 只记录当前实测值，不提高硬上限，也不允许新增条目；
- 当前值增加时 CI 失败；当前值下降时必须同步降低 baseline，回到硬上限内后删除条目；
- baseline 是可删除的迁移账，不是永久豁免；新文件和新函数从第一天直接遵守硬上限。

仓库必须把同一个架构检查命令同时接入 Pull Request 必需检查和发布/打包质量门禁。`clippy.toml` 负责
Clippy 阈值，仓库脚本负责文件与函数长度及存量债务棘轮；只在开发者文档或评审模板里声明规则不算完成。

## 入站转换示例

HTTP Adapter 先把 JSON 解码成 Contract DTO，再显式转换成应用 Command：

```rust
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 再把它映射成稳定 `errorCode` 和 HTTP `200` 的 [CModel 错误响应](/integration/cmodel-error-model/)。领域层
不构造 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](/development/backend-development/database-migrations/)，不得在 Handler、Worker、
启动钩子或随服务交付的临时 CLI 中加入一次性修复、回填或生产 SQL。

## 测试与 CI 门禁

涉及 Contract、Serde、数据库 JSON、Artifact 或模型转换的变更，至少包含：

1. Contract 正向序列化和反序列化测试，固定字段名、Tag、必填性和版本。
2. 未知字段、未知变体、缺失字段、非法版本和错误嵌套的负向测试，并断言字段路径。
3. `TryFrom`、`From` 或显式编译器测试，覆盖每个业务变体与不变量。
4. 生产者和消费者共享 Contract 的契约测试或 Golden Fixture。
5. `Record <-> domain`、`Artifact bytes <-> Contract` 的集成测试。
6. `cargo fmt --check`、`cargo clippy --all-targets --all-features -- -D warnings` 和适用范围的
   `cargo test`。
7. 文件、函数长度和认知复杂度检查通过，且没有新增、扩大或遗留未收紧的 baseline 债务。

CI 应检查领域层和应用层是否出现动态 JSON。以下命令可用于定位，每个命中都必须删除，或证明它只位于
上述允许的不透明扩展边界：

```bash
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、单元测试、契约测试和集成测试全部通过后，才把变更合并到 Project 主线并发布新的 `Cargo package.version`。
