Rust Architecture
用于规划和维护 Rust 项目结构。除固定技术词外,使用中文说明;按项目复杂度选择最小够用的结构,不为模板完整性创建空目录。
需要具体目录树示例时,读取 layouts。
架构概念
复杂应用只使用四个核心概念:domain、application、ports、adapters。
domain:纯业务核心,放实体、值对象、业务规则、状态流转、领域事件和纯校验逻辑。不得依赖外部框架、数据库、网络、文件系统、CLI、HTTP 或第三方 SDK。application:用例和流程编排,放应用服务、命令/查询对象、事务边界、权限/上下文检查、跨能力编排。可以依赖domain和ports,不得直接依赖具体外部实现。顶层目录使用application,不要用services或system作为层名;确实需要 service 组织形式时,放在application内部模块。ports:application需要的边界 trait,只为外部能力建立接口,例如数据库访问、对象存储、第三方服务、通知发送、时间、ID 生成、事务管理等。不要为普通 helper、纯内部函数或没有边界意义的逻辑建立 trait。adapters:外部世界的接入和实现。进入系统的 adapter 包括 HTTP 和 CLI;系统调用外部的 adapter 包括数据库、文件系统/对象存储、第三方服务等。adapters负责实现ports、转换外部类型、做错误映射和完成具体 IO。
Adapter 目录
只在项目确实使用对应能力时创建 adapter 目录,不创建空目录。
adapters/http:HTTP 入口 adapter。adapters/cli:CLI 入口 adapter,所有 CLI 使用clap。adapters/db:数据库 adapter。adapters/storage:文件系统或对象存储 adapter。adapters/external:第三方服务 adapter。
结构选择
- 简单 CLI 或小工具:优先使用
main.rs、cli.rs、core.rs、error.rs。main.rs只安装color-eyre、解析 CLI、调用core入口;cli.rs使用clap;core.rs放核心执行逻辑和纯业务逻辑;error.rs使用thiserror。不需要lib.rs,除非同时要作为 library 复用。 - 稍复杂 CLI 或小工具:使用
main.rs、cli.rs、commands/、core/、adapters/、error.rs。commands/放不同命令入口,adapters/放文件系统、网络、时间、外部命令等 IO 实现。 - 普通 library crate:使用
lib.rs、error.rs、types.rs,按需添加config.rs、client.rs。lib.rs控制 public API,不塞大量实现。library crate 不把color-eyre作为公共错误类型。 - 后端服务或复杂应用:使用
domain/、application/、ports/、adapters/、config.rs、error.rs、main.rs。main.rs负责组装 adapters、application 和配置。
Crate 拆分
当 adapters 中某些能力边界稳定、通用、可独立测试、依赖隔离价值高、未来可能复用时,可以提取成独立 crate。
- 适合拆分的能力包括第三方 API client、对象存储 client、通知 client、签名/验签、文件导入导出、验证码、proc macro 等。
- 拆出去的 crate 不依赖主 crate,不知道主业务流程。主 crate 通过
adapters把它接回ports中定义的边界 trait。 - 普通 capability crate 不套完整四层架构,保持
lib.rs、client.rs、config.rs、error.rs、types.rs等清晰边界;需要签名就加signing.rs,需要 provider 分发就加provider.rs。 - 如果独立 crate 本身需要 CLI 功能,使用
main.rs+lib.rs结构:CLI 入口安装color-eyre、解析clap参数并调用库逻辑,lib.rs暴露可复用能力。
错误处理
- 每个 crate 都要有自己的错误管理,优先使用
thiserror定义结构化错误。 color-eyre只用于应用入口、CLI、server startup 和顶层错误报告,不作为 library crate 的公共错误类型。- HTTP adapter 按 RESTful 语义返回状态码:成功使用
2xx,例如200 OK;失败使用非2xx,其中4xx表示客户端输入、权限、资源不存在等可预期错误,5xx表示服务端内部错误。 - HTTP 错误响应 body 使用稳定的大写字符串错误码和必要上下文字段:
{
"code": "PROBLEM_NOT_FOUND",
"problem_id": "123"
}
code 使用 SCREAMING_SNAKE_CASE,作为 API 稳定错误码。前端或客户端用 code 直接查错误文案模板,不把内部错误直接暴露给客户端,并在服务端记录详细错误。
- 错误文案 i18n 按语言拆文件,放在项目根目录的
locales/:
locales/
zh-CN/
errors.json
en-US/
errors.json
errors.json 内直接使用 API 错误码作为 key:
{
"PROBLEM_NOT_FOUND": "题目 {problem_id} 不存在"
}
格式化与 CI
- 修改 Rust 代码后,在 workspace 根目录只格式化本次修改涉及的 crate:
cargo fmt -p <package>。 - CI 检查必须包含本次修改涉及 crate 的
cargo clippy -p <package> --all-targets --all-features -- -D warnings。不要默认运行全 workspace clippy,除非本次修改确实影响 workspace 集成层或多个 crate 的公共契约。
测试规则
- 单元测试优先写在当前模块所在文件内,验证单个函数、模块、
domain规则、application分支、错误路径和 adapter 映射逻辑。 - 模块内单元测试存在 trait 边界时可用
mockall;mockall属性加在ports的 trait 上,例如#[cfg_attr(test, mockall::automock)]。不要手写mock_xxx.rs放到ports里。 - 集成测试写在
tests/目录下,每个文件对应一个明确业务流程、公开 API 或主要能力,文件名表达业务含义,例如order_flow.rs、callback_flow.rs、parser_roundtrip.rs、cli_check.rs、client_request.rs。 - 集成测试用于验证公开 API、模块协作、真实 adapter 或近真实环境。数据库、文件系统等关键依赖尽量使用真实或临时隔离环境;第三方服务使用 sandbox、mock server 或本地 fake service,不打生产环境。
tests/集成测试不能依赖#[cfg(test)]生成的Mock*。优先使用 fake adapter、in-memory adapter 或 mock server;确实需要复用测试工具时,使用显式 feature-gatedtest-support,不要默认把 mock 作为公共 API 暴露。- 测试函数在有多步 fallible 操作时优先返回
Result;简单断言测试可以直接使用assert。 - 只写有效测试,不写只验证 mock 配置、getter/setter 或框架默认行为的低价值测试。
Source: yIllusionSky/yaoyao-skills — distributed by TomeVault.