pma-rust
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRust Project Implementation Guide
Rust项目实现指南
Standard Rust stack and conventions for multi-crate workspace projects.
多crate工作区项目的标准Rust技术栈与规范。
Tech Stack
技术栈
| Category | Technology | Notes |
|---|---|---|
| Core | ||
| Language | Rust | edition 2024, nightly toolchain |
| Build | Cargo workspaces | multi-crate monorepo |
| Task runner | just | command runner |
| Async & HTTP | ||
| Runtime | Tokio | full features |
| HTTP server | Axum 0.8 | multipart, middleware, graceful shutdown |
| HTTP client | reqwest 0.12 | rustls-tls, no openssl |
| Data | ||
| ORM | Diesel 2 | r2d2 pool, feature-gated backend |
| Concurrent cache | DashMap 6 | lock-free concurrent HashMap |
| CLI | ||
| Argument parsing | clap 4 | derive macros, subcommands |
| Error Handling | ||
| Typed errors | thiserror 2 | per-crate error enums |
| Error propagation | anyhow 1.0 | boundary crossing |
| Serialization | ||
| Serde | serde 1.0 + serde_json + toml | derive |
| Linting | ||
| Format | rustfmt | edition 2024 |
| Lint | clippy + cargo-cranky | strict deny rules |
| Dependency audit | cargo-deny | license, ban, advisory |
| Security | ||
| TLS | rustls 0.23 | aws-lc-rs provider, no openssl/ring |
| Token comparison | subtle 2 | constant-time to prevent timing attacks |
| 分类 | 技术 | 说明 |
|---|---|---|
| 核心 | ||
| 语言 | Rust | 2024版本,nightly工具链 |
| 构建 | Cargo工作区 | 多crate单体仓库 |
| 任务运行器 | just | 命令运行工具 |
| 异步与HTTP | ||
| 运行时 | Tokio | 全功能版 |
| HTTP服务器 | Axum 0.8 | 支持多部分请求、中间件、优雅停机 |
| HTTP客户端 | reqwest 0.12 | 基于rustls-tls,不依赖openssl |
| 数据 | ||
| ORM | Diesel 2 | r2d2连接池,特性门控后端 |
| 并发缓存 | DashMap 6 | 无锁并发HashMap |
| 命令行工具 | ||
| 参数解析 | clap 4 | 派生宏,支持子命令 |
| 错误处理 | ||
| 类型化错误 | thiserror 2 | 每个crate独立定义错误枚举 |
| 错误传播 | anyhow 1.0 | 跨crate边界的错误传播 |
| 序列化 | ||
| Serde | serde 1.0 + serde_json + toml | 支持派生宏 |
| 代码检查 | ||
| 格式化 | rustfmt | 2024版本规范 |
| 代码检查 | clippy + cargo-cranky | 严格的禁止规则 |
| 依赖审计 | cargo-deny | 许可证检查、依赖封禁、安全 advisory |
| 安全 | ||
| TLS | rustls 0.23 | aws-lc-rs提供支持,不依赖openssl/ring |
| 令牌比较 | subtle 2 | 常量时间比较以防止时序攻击 |
Workspace Structure
工作区结构
Cargo.toml # [workspace] root
Cargo.lock
rust-toolchain # nightly-YYYY-MM-DD
rustfmt.toml
clippy.toml
Cranky.toml # cargo-cranky lint config
deny.toml # cargo-deny config
justfile # task runner
.cargo/
config.toml # rustflags
.github/
workflows/
ci.yml
docs/
architecture.md
changelog.md
task/
plan/
crates/
app/ # main binary crate
src/
main.rs
commands/
core/ # runtime services, state, DB
src/
services.rs # DI container
db/
protocols/
common/ # shared types, config, errors
src/
config/
error.rs
types/
helpers/
protocol-xxx/ # per-protocol crate
src/
lib.rs
db/ # Diesel ORM + migrations
src/
lib.rs
schema.rs
models.rs
migrations/
diesel.toml
tests/ # integration tests (TypeScript/Bun)Cargo.toml # [workspace] 根目录
Cargo.lock
rust-toolchain # nightly-YYYY-MM-DD
rustfmt.toml
clippy.toml
Cranky.toml # cargo-cranky 代码检查配置
deny.toml # cargo-deny 配置
justfile # 任务运行器配置
.cargo/
config.toml # rustflags配置
.github/
workflows/
ci.yml
docs/
architecture.md
changelog.md
task/
plan/
crates/
app/ # 主二进制crate
src/
main.rs
commands/
core/ # 运行时服务、状态、数据库
src/
services.rs # 依赖注入容器
db/
protocols/
common/ # 共享类型、配置、错误定义
src/
config/
error.rs
types/
helpers/
protocol-xxx/ # 特定协议crate
src/
lib.rs
db/ # Diesel ORM + 迁移脚本
src/
lib.rs
schema.rs
models.rs
migrations/
diesel.toml
tests/ # 集成测试(TypeScript/Bun)Workspace Cargo.toml
工作区Cargo.toml
toml
[workspace]
resolver = "2"
members = ["crates/*"]
default-members = ["crates/app"]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
[workspace.dependencies]toml
[workspace]
resolver = "2"
members = ["crates/*"]
default-members = ["crates/app"]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
[workspace.dependencies]Pin shared dependencies here
在此固定共享依赖版本
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
thiserror = "2"
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "time", "local-time", "ansi"] }
clap = { version = "4", features = ["derive"] }
axum = { version = "0.8", features = ["multipart"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
diesel = { version = "2", features = ["r2d2"] }
diesel_migrations = "2"
dashmap = "6"
subtle = "2"
[profile.release]
lto = true
panic = "abort"
strip = "debuginfo"
undefinedtokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
thiserror = "2"
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "time", "local-time", "ansi"] }
clap = { version = "4", features = ["derive"] }
axum = { version = "0.8", features = ["multipart"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
diesel = { version = "2", features = ["r2d2"] }
diesel_migrations = "2"
dashmap = "6"
subtle = "2"
[profile.release]
lto = true
panic = "abort"
strip = "debuginfo"
undefinedToolchain & Compiler Flags
工具链与编译器标志
rust-toolchain
rust-toolchain
nightly-YYYY-MM-DDPin to a specific nightly date for reproducibility.
nightly-YYYY-MM-DD固定到特定的nightly版本日期以保证构建可复现。
.cargo/config.toml
.cargo/config.toml
toml
[target.'cfg(all())']
rustflags = [
"--cfg", "tokio_unstable",
"-Zremap-cwd-prefix=/reproducible-cwd",
"--remap-path-prefix=$HOME=/reproducible-home",
"--remap-path-prefix=$PWD=/reproducible-pwd",
]- : enables tokio console + task IDs
tokio_unstable - Path remapping: reproducible builds across environments
toml
[target.'cfg(all())']
rustflags = [
"--cfg", "tokio_unstable",
"-Zremap-cwd-prefix=/reproducible-cwd",
"--remap-path-prefix=$HOME=/reproducible-home",
"--remap-path-prefix=$PWD=/reproducible-pwd",
]- : 启用tokio控制台与任务ID
tokio_unstable - 路径重映射:实现跨环境的可复现构建
Lint Configuration
代码检查配置
rustfmt.toml
rustfmt.toml
toml
imports_granularity = "Module"
group_imports = "StdExternalCrate"Import groups: std → external crates → local crates, module granularity.
toml
imports_granularity = "Module"
group_imports = "StdExternalCrate"导入分组标准:标准库 → 外部crate → 本地crate,按模块粒度分组。
clippy.toml
clippy.toml
toml
avoid-breaking-exported-api = false
allow-unwrap-in-tests = truetoml
avoid-breaking-exported-api = false
allow-unwrap-in-tests = trueCranky.toml (cargo-cranky)
Cranky.toml (cargo-cranky)
toml
[cranky]
deny = [
"unsafe_code",
"clippy::unwrap_used",
"clippy::expect_used",
"clippy::panic",
"clippy::indexing_slicing",
"clippy::dbg_macro",
]
allow = [
"clippy::result_large_err",
]Hard rule: , , , , and index slicing are all compile errors. Only site-level can bypass them (e.g., at startup where failure is fatal).
unsafeunwrapexpectpanic#[allow(...)]toml
[cranky]
deny = [
"unsafe_code",
"clippy::unwrap_used",
"clippy::expect_used",
"clippy::panic",
"clippy::indexing_slicing",
"clippy::dbg_macro",
]
allow = [
"clippy::result_large_err",
]硬性规则:、、、以及索引切片操作均为编译错误。仅可通过代码级别的绕过(例如在启动阶段,失败会导致程序终止的场景)。
unsafeunwrapexpectpanic#[allow(...)]deny.toml (cargo-deny)
deny.toml (cargo-deny)
toml
[bans]
deny = [
{ crate = "openssl-sys", use-instead = "rustls" },
]
[licenses]
allow = [
"MIT", "Apache-2.0", "ISC",
"BSD-2-Clause", "BSD-3-Clause",
"Zlib", "CC0-1.0",
]- Ban openssl: all TLS must use rustls + aws-lc-rs
- License allowlist: only permissive licenses
toml
[bans]
deny = [
{ crate = "openssl-sys", use-instead = "rustls" },
]
[licenses]
allow = [
"MIT", "Apache-2.0", "ISC",
"BSD-2-Clause", "BSD-3-Clause",
"Zlib", "CC0-1.0",
]- 禁用openssl:所有TLS通信必须使用rustls + aws-lc-rs
- 许可证白名单:仅允许使用宽松许可证的依赖
Error Handling
错误处理
Two-tier system:
- — each crate defines its own error enum with
thiserrorconversions:#[from]
rust
#[derive(thiserror::Error, Debug)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sea_orm::DbErr),
#[error("config error: {0}")]
Config(#[from] ConfigError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}- — for propagation across crate boundaries where typed error is not needed.
anyhow::Result<T>
采用双层错误处理体系:
- — 每个crate定义自己的错误枚举,并使用
thiserror实现转换:#[from]
rust
#[derive(thiserror::Error, Debug)]
pub enum AppError {
#[error("数据库错误: {0}")]
Database(#[from] sea_orm::DbErr),
#[error("配置错误: {0}")]
Config(#[from] ConfigError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}- — 用于跨crate边界的错误传播,无需定义类型化错误的场景。
anyhow::Result<T>
Secret Handling
敏感信息处理
Wrap secrets in a type that redacts output:
Secret<T>Debugrust
pub struct Secret<T>(T);
impl<T> std::fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("<secret>")
}
}Prevents accidental secret leakage in logs and error messages.
将敏感信息包装在类型中,实现输出时自动脱敏:
Secret<T>Debugrust
pub struct Secret<T>(T);
impl<T> std::fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("<secret>")
}
}防止敏感信息意外泄露到日志或错误信息中。
Architecture Patterns
架构模式
Services (DI Container)
服务(依赖注入容器)
rust
pub struct Services {
pub db: Arc<Mutex<DatabaseConnection>>,
pub config: Arc<Mutex<AppConfig>>,
pub state: Arc<Mutex<State>>,
}All services use for shared ownership. is cloned cheaply (all fields are ).
Arc<Mutex<T>>ServicesArcrust
pub struct Services {
pub db: Arc<Mutex<DatabaseConnection>>,
pub config: Arc<Mutex<AppConfig>>,
pub state: Arc<Mutex<State>>,
}所有服务均使用实现共享所有权。可以被低成本克隆(所有字段均为类型)。
Arc<Mutex<T>>ServicesArcProtocol Server Trait
协议服务器 trait
rust
pub trait ProtocolServer {
fn name(&self) -> &'static str;
fn run(self, address: ListenEndpoint) -> impl Future<Output = Result<()>> + Send;
}rust
pub trait ProtocolServer {
fn name(&self) -> &'static str;
fn run(self, address: ListenEndpoint) -> impl Future<Output = Result<()>> + Send;
}Trait Polymorphism
Trait 多态
Prefer over for zero-cost polymorphism at runtime.
enum_dispatchBox<dyn Trait>优先使用而非,实现运行时零成本多态。
enum_dispatchBox<dyn Trait>Deadlock Detection (Debug Only)
死锁检测(仅调试模式)
Wrap with a 5-second timeout in debug builds:
tokio::sync::Mutexrust
#[cfg(debug_assertions)]
pub async fn lock(&self) -> MutexGuard<'_, T> {
match tokio::time::timeout(Duration::from_secs(5), self.inner.lock()).await {
Ok(guard) => guard,
Err(_) => panic!("deadlock detected on mutex: {}", self.name),
}
}在调试构建中,为添加5秒超时包装:
tokio::sync::Mutexrust
#[cfg(debug_assertions)]
pub async fn lock(&self) -> MutexGuard<'_, T> {
match tokio::time::timeout(Duration::from_secs(5), self.inner.lock()).await {
Ok(guard) => guard,
Err(_) => panic!("检测到Mutex死锁: {}", self.name),
}
}Database (Diesel)
数据库(Diesel)
Crate Structure
Crate结构
crates/
db/ # @project/db
src/
lib.rs
schema.rs # diesel print-schema output
models.rs # Queryable/Insertable structs
migrations/
00000000000000_create_xxx/
up.sql
down.sql
diesel.tomlcrates/
db/ # @project/db
src/
lib.rs
schema.rs # diesel print-schema 输出
models.rs # Queryable/Insertable 结构体
migrations/
00000000000000_create_xxx/
up.sql
down.sql
diesel.tomldiesel.toml
diesel.toml
toml
[print_schema]
file = "src/schema.rs"
[migrations_directory]
dir = "src/migrations"toml
[print_schema]
file = "src/schema.rs"
[migrations_directory]
dir = "src/migrations"Feature Flags
特性标志
toml
[features]
default = ["sqlite"]
postgres = ["diesel/postgres"]
mysql = ["diesel/mysql"]
sqlite = ["diesel/sqlite"]toml
[features]
default = ["sqlite"]
postgres = ["diesel/postgres"]
mysql = ["diesel/mysql"]
sqlite = ["diesel/sqlite"]Connection Pool
连接池
Use for connection pooling:
diesel::r2d2rust
use diesel::r2d2::{self, ConnectionManager};
pub type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
pub fn establish_pool(database_url: &str) -> DbPool {
let manager = ConnectionManager::<SqliteConnection>::new(database_url);
r2d2::Pool::builder()
.max_size(10)
.build(manager)
.expect("failed to create pool")
}使用实现数据库连接池:
diesel::r2d2rust
use diesel::r2d2::{self, ConnectionManager};
pub type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
pub fn establish_pool(database_url: &str) -> DbPool {
let manager = ConnectionManager::<SqliteConnection>::new(database_url);
r2d2::Pool::builder()
.max_size(10)
.build(manager)
.expect("创建连接池失败")
}Migration
迁移
Run migrations at startup:
rust
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/migrations");
pub fn run_migrations(conn: &mut impl MigrationHarness<diesel::sqlite::Sqlite>) {
conn.run_pending_migrations(MIGRATIONS)
.expect("failed to run migrations");
}在应用启动时运行迁移脚本:
rust
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/migrations");
pub fn run_migrations(conn: &mut impl MigrationHarness<diesel::sqlite::Sqlite>) {
conn.run_pending_migrations(MIGRATIONS)
.expect("运行迁移脚本失败");
}CLI
命令行工具
bash
undefinedbash
undefinedInstall diesel CLI
安装diesel CLI
cargo install diesel_cli --no-default-features --features sqlite
cargo install diesel_cli --no-default-features --features sqlite
Setup (creates diesel.toml + migrations dir)
初始化(创建diesel.toml + 迁移目录)
diesel setup
diesel setup
Generate migration
生成迁移脚本
diesel migration generate create_users
diesel migration generate create_users
Run migrations
运行迁移脚本
diesel migration run
diesel migration run
Print schema
导出schema
diesel print-schema > src/schema.rs
undefineddiesel print-schema > src/schema.rs
undefinedConfiguration System
配置系统
TOML config with + for per-field defaults.
serde::Deserialize#[serde(default)]基于TOML格式的配置,使用 + 实现字段级默认值。
serde::Deserialize#[serde(default)]Config Structs
配置结构体
rust
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct AppConfig {
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub database: DatabaseConfig,
#[serde(default)]
pub logging: LoggingConfig,
}
#[derive(Debug, Deserialize)]
pub struct ServerConfig {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
}
fn default_host() -> String { "0.0.0.0".into() }
fn default_port() -> u16 { 3000 }
impl Default for ServerConfig {
fn default() -> Self {
Self { host: default_host(), port: default_port() }
}
}rust
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct AppConfig {
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub database: DatabaseConfig,
#[serde(default)]
pub logging: LoggingConfig,
}
#[derive(Debug, Deserialize)]
pub struct ServerConfig {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
}
fn default_host() -> String { "0.0.0.0".into() }
fn default_port() -> u16 { 3000 }
impl Default for ServerConfig {
fn default() -> Self {
Self { host: default_host(), port: default_port() }
}
}Loading
加载配置
rust
impl AppConfig {
pub fn load(path: &str) -> anyhow::Result<Self> {
let content = std::fs::read_to_string(path)?;
let config: AppConfig = toml::from_str(&content)?;
Ok(config)
}
/// Auto-generate default config if file does not exist, then exit.
pub fn ensure_exists(path: &str) -> anyhow::Result<()> {
if !std::path::Path::new(path).exists() {
let default = toml::to_string_pretty(&AppConfig::default())?;
std::fs::write(path, default)?;
eprintln!("generated default config at {path}, please edit and restart");
std::process::exit(0);
}
Ok(())
}
}rust
impl AppConfig {
pub fn load(path: &str) -> anyhow::Result<Self> {
let content = std::fs::read_to_string(path)?;
let config: AppConfig = toml::from_str(&content)?;
Ok(config)
}
/// 如果配置文件不存在则自动生成默认配置,然后退出程序。
pub fn ensure_exists(path: &str) -> anyhow::Result<()> {
if !std::path::Path::new(path).exists() {
let default = toml::to_string_pretty(&AppConfig::default())?;
std::fs::write(path, default)?;
eprintln!("已在 {path} 生成默认配置,请编辑后重启程序");
std::process::exit(0);
}
Ok(())
}
}Config Path
配置路径
Controlled by env var with fallback:
rust
let config_path = std::env::var("APP_CONFIG").unwrap_or_else(|_| "config.toml".into());
AppConfig::ensure_exists(&config_path)?;
let config = AppConfig::load(&config_path)?;通过环境变量控制配置路径,同时提供默认值:
rust
let config_path = std::env::var("APP_CONFIG").unwrap_or_else(|_| "config.toml".into());
AppConfig::ensure_exists(&config_path)?;
let config = AppConfig::load(&config_path)?;Config File Format
配置文件格式
toml
undefinedtoml
undefinedconfig.toml
config.toml
[server]
host = "0.0.0.0"
port = 3000
[database]
url = "sqlite://data.db"
max_connections = 10
[logging]
level = "info"
format = "text" # "text" or "json"
undefined[server]
host = "0.0.0.0"
port = 3000
[database]
url = "sqlite://data.db"
max_connections = 10
[logging]
level = "info"
format = "text" # "text" 或 "json"
undefinedConventions
规范
- Use TOML (not YAML) — Rust ecosystem standard, better type safety
- on every optional struct for graceful partial configs
#[serde(default)] - for per-field defaults using free functions
#[serde(default = "fn_name")] - Implement on config structs so
Defaulton parent works#[serde(default)] - Auto-generate config file on first run with sensible defaults
- Config path via env var (), default to
APP_CONFIGconfig.toml
- 使用TOML(而非YAML)—— Rust生态系统标准格式,类型安全性更好
- 所有可选结构体字段添加,实现优雅的部分配置加载
#[serde(default)] - 对于字段级默认值,使用绑定自由函数
#[serde(default = "fn_name")] - 为配置结构体实现trait,确保父结构体的
Default正常工作#[serde(default)] - 首次运行时自动生成带合理默认值的配置文件
- 通过环境变量()指定配置路径,默认路径为
APP_CONFIGconfig.toml
CLI (clap)
命令行工具(clap)
Use with derive macros for type-safe CLI argument parsing.
clap使用clap的派生宏实现类型安全的命令行参数解析。
Basic Structure
基础结构
rust
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "myapp", about = "Application description")]
pub struct Cli {
/// Config file path
#[arg(short, long, default_value = "config.toml")]
pub config: String,
/// Enable verbose logging
#[arg(short, long)]
pub verbose: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
/// Start the server
Run,
/// Check configuration validity
Check,
/// Print version info
Version,
/// Generate default config file
Init,
}rust
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "myapp", about = "应用描述")]
pub struct Cli {
/// 配置文件路径
#[arg(short, long, default_value = "config.toml")]
pub config: String,
/// 启用详细日志
#[arg(short, long)]
pub verbose: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
/// 启动服务器
Run,
/// 检查配置有效性
Check,
/// 打印版本信息
Version,
/// 生成默认配置文件
Init,
}Usage in main
在main函数中使用
rust
use clap::Parser;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Run => {
let config = AppConfig::load(&cli.config)?;
start_server(&config).await?;
}
Command::Check => {
let _config = AppConfig::load(&cli.config)?;
println!("config OK");
}
Command::Version => {
println!("{}", env!("CARGO_PKG_VERSION"));
}
Command::Init => {
AppConfig::ensure_exists(&cli.config)?;
}
}
Ok(())
}rust
use clap::Parser;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Run => {
let config = AppConfig::load(&cli.config)?;
start_server(&config).await?;
}
Command::Check => {
let _config = AppConfig::load(&cli.config)?;
println!("配置有效");
}
Command::Version => {
println!("{}", env!("CARGO_PKG_VERSION"));
}
Command::Init => {
AppConfig::ensure_exists(&cli.config)?;
}
}
Ok(())
}Conventions
规范
- Use on the top-level struct,
#[derive(Parser)]on the command enum#[derive(Subcommand)] - Use doc comments () for help text — clap derives
///from them--help - for flags,
#[arg(short, long)]for defaults#[arg(default_value = "...")] - Keep CLI struct in , re-export from
crates/app/src/cli.rsmain.rs - Config path as a global arg, subcommands for distinct operations (run, check, init, version)
- 顶层结构体使用,命令枚举使用
#[derive(Parser)]#[derive(Subcommand)] - 使用文档注释()编写帮助文本——clap会自动生成
///信息--help - 标志参数使用,带默认值的参数使用
#[arg(short, long)]#[arg(default_value = "...")] - 将CLI结构体放在中,从
crates/app/src/cli.rs导出main.rs - 配置路径作为全局参数,不同操作使用子命令实现(run、check、init、version)
Signal Handling
信号处理
Graceful shutdown with Tokio signal handlers:
rust
use tokio::signal;
pub async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => tracing::info!("received Ctrl+C, shutting down"),
_ = terminate => tracing::info!("received SIGTERM, shutting down"),
}
}使用Tokio信号处理器实现优雅停机:
rust
use tokio::signal;
pub async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("安装Ctrl+C处理器失败");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("安装SIGTERM处理器失败")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => tracing::info!("收到Ctrl+C信号,开始停机")
_ = terminate => tracing::info!("收到SIGTERM信号,开始停机")
}
}Usage in main
在main函数中使用
rust
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = AppConfig::load()?;
// Start server with graceful shutdown
let server = start_server(&config);
tokio::select! {
result = server => result?,
_ = shutdown_signal() => {
tracing::info!("graceful shutdown complete");
}
}
Ok(())
}rust
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = AppConfig::load()?;
// 启动服务器并支持优雅停机
let server = start_server(&config);
tokio::select! {
result = server => result?,
_ = shutdown_signal() => {
tracing::info!("优雅停机完成");
}
}
Ok(())
}Shutdown Checklist
停机检查清单
- Flush pending database writes
- Close connection pools
- Complete in-flight requests (with timeout)
- Flush tracing/log buffers
- 刷新待处理的数据库写入
- 关闭连接池
- 完成正在处理的请求(带超时)
- 刷新跟踪/日志缓冲区
HTTP Server (Axum)
HTTP服务器(Axum)
Router Structure
路由结构
rust
use axum::{Router, routing::{get, post}, middleware};
pub fn create_router(state: AppState) -> Router {
Router::new()
// Public routes
.route("/health", get(health))
// Protected API routes
.nest("/api/v1", api_routes()
.layer(middleware::from_fn(verify_token)))
.with_state(Arc::new(state))
}rust
use axum::{Router, routing::{get, post}, middleware};
pub fn create_router(state: AppState) -> Router {
Router::new()
// 公开路由
.route("/health", get(health))
// 受保护的API路由
.nest("/api/v1", api_routes()
.layer(middleware::from_fn(verify_token)))
.with_state(Arc::new(state))
}AppState
AppState
rust
pub struct AppState {
pub db: DbPool,
pub config: AppConfig,
}Pass as in handlers.
State(state): State<Arc<AppState>>rust
pub struct AppState {
pub db: DbPool,
pub config: AppConfig,
}在处理器中通过获取。
State(state): State<Arc<AppState>>Handler Pattern
处理器模式
rust
async fn get_item(
State(state): State<Arc<AppState>>,
Path(id): Path<i32>,
) -> Result<Json<Item>, AppError> {
let conn = &mut state.db.get()?;
let item = items::table.find(id).first(conn)?;
Ok(Json(item))
}rust
async fn get_item(
State(state): State<Arc<AppState>>,
Path(id): Path<i32>,
) -> Result<Json<Item>, AppError> {
let conn = &mut state.db.get()?;
let item = items::table.find(id).first(conn)?;
Ok(Json(item))
}Auth Middleware
认证中间件
Use for token comparison to prevent timing attacks:
subtle::ConstantTimeEqrust
use subtle::ConstantTimeEq;
async fn verify_token(
headers: HeaderMap,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let token = headers.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "));
match token {
Some(t) if t.as_bytes().ct_eq(expected.as_bytes()).into() => {
Ok(next.run(request).await)
}
_ => Err(StatusCode::UNAUTHORIZED),
}
}使用进行令牌比较,防止时序攻击:
subtle::ConstantTimeEqrust
use subtle::ConstantTimeEq;
async fn verify_token(
headers: HeaderMap,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let token = headers.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "));
match token {
Some(t) if t.as_bytes().ct_eq(expected.as_bytes()).into() => {
Ok(next.run(request).await)
}
_ => Err(StatusCode::UNAUTHORIZED),
}
}Graceful Shutdown with Axum
Axum优雅停机
rust
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, router)
.with_graceful_shutdown(shutdown_signal())
.await?;rust
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, router)
.with_graceful_shutdown(shutdown_signal())
.await?;Security Patterns
安全模式
Constant-Time Token Comparison
常量时间令牌比较
Always use for auth token, API key, and HMAC comparisons. Never use for secrets.
subtle::ConstantTimeEq==对于认证令牌、API密钥和HMAC比较,始终使用。绝不要使用比较敏感信息。
subtle::ConstantTimeEq==SSRF Protection
SSRF防护
When accepting user-provided URLs (webhooks, callbacks), validate the resolved IP:
rust
use std::net::IpAddr;
fn is_private_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => {
v4.is_private() || v4.is_loopback() || v4.is_link_local()
|| v4.is_broadcast() || v4.is_unspecified()
}
IpAddr::V6(v6) => v6.is_loopback() || v6.is_unspecified(),
}
}
fn validate_webhook_url(url: &str) -> anyhow::Result<()> {
let host = url::Url::parse(url)?.host_str()
.ok_or_else(|| anyhow::anyhow!("no host"))?
.to_string();
let addrs = std::net::ToSocketAddrs::to_socket_addrs(
&(host.as_str(), 443)
)?;
for addr in addrs {
anyhow::ensure!(!is_private_ip(addr.ip()), "private IP not allowed");
}
Ok(())
}当接收用户提供的URL(如webhook、回调地址)时,验证解析后的IP地址:
rust
use std::net::IpAddr;
fn is_private_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => {
v4.is_private() || v4.is_loopback() || v4.is_link_local()
|| v4.is_broadcast() || v4.is_unspecified()
}
IpAddr::V6(v6) => v6.is_loopback() || v6.is_unspecified(),
}
}
fn validate_webhook_url(url: &str) -> anyhow::Result<()> {
let host = url::Url::parse(url)?.host_str()
.ok_or_else(|| anyhow::anyhow!("URL无主机地址"))?
.to_string();
let addrs = std::net::ToSocketAddrs::to_socket_addrs(
&(host.as_str(), 443)
)?;
for addr in addrs {
anyhow::ensure!(!is_private_ip(addr.ip()), "禁止使用私有IP地址");
}
Ok(())
}Concurrent Cache (DashMap)
并发缓存(DashMap)
Use for in-memory caches shared across async tasks:
DashMaprust
use dashmap::DashMap;
pub struct Cache {
inner: DashMap<String, CachedItem>,
}
impl Cache {
pub fn get(&self, key: &str) -> Option<CachedItem> {
self.inner.get(key).map(|v| v.clone())
}
pub fn insert(&self, key: String, value: CachedItem) {
self.inner.insert(key, value);
}
}No needed — handles concurrent reads/writes internally.
MutexDashMap使用实现跨异步任务共享的内存缓存:
DashMaprust
use dashmap::DashMap;
pub struct Cache {
inner: DashMap<String, CachedItem>,
}
impl Cache {
pub fn get(&self, key: &str) -> Option<CachedItem> {
self.inner.get(key).map(|v| v.clone())
}
pub fn insert(&self, key: String, value: CachedItem) {
self.inner.insert(key, value);
}
}无需使用——内部已处理并发读写。
MutexDashMapLogging
日志
Multi-layer tracing setup with :
tracing-subscriber- Console: ANSI colors, env-filter, local-time
- JSON: structured JSON format for
--log-format json - Database: custom that captures events at INFO+ and writes to DB
Layer - Filter to only -prefixed targets for application logs
{crate_prefix}
基于的多层日志配置:
tracing-subscriber- 控制台:ANSI颜色、环境变量过滤、本地时间
- JSON:结构化JSON格式,用于参数
--log-format json - 数据库:自定义,捕获INFO及以上级别的事件并写入数据库
Layer - 仅过滤以为前缀的目标,仅输出应用日志
{crate_prefix}
CI Pipeline
CI流水线
ci.yml (PR + push to main)
ci.yml(PR + 推送到main分支)
Two-stage pipeline: format gate first, then parallel checks.
yaml
jobs:
# Gate 1: format must pass before anything else runs
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: dtolnay/rust-toolchain@<sha>
- run: cargo fmt --check
# Gate 2: parallel checks after format passes
clippy:
needs: [fmt]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: dtolnay/rust-toolchain@<sha>
- run: cargo cranky --all-targets -- -D warnings
test:
needs: [fmt]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: dtolnay/rust-toolchain@<sha>
- run: cargo test --all-features
deny:
needs: [fmt]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: EmbarkStudios/cargo-deny-action@<sha>Key: is the first gate — no point running clippy/test if formatting is broken.
fmt两阶段流水线:首先检查代码格式,然后并行执行其他检查。
yaml
jobs:
# 第一关:代码格式检查通过后才能执行其他任务
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: dtolnay/rust-toolchain@<sha>
- run: cargo fmt --check
# 第二关:代码格式检查通过后并行执行其他检查
clippy:
needs: [fmt]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: dtolnay/rust-toolchain@<sha>
- run: cargo cranky --all-targets -- -D warnings
test:
needs: [fmt]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: dtolnay/rust-toolchain@<sha>
- run: cargo test --all-features
deny:
needs: [fmt]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: EmbarkStudios/cargo-deny-action@<sha>关键设计:是第一关——如果代码格式不符合规范,执行clippy或测试毫无意义。
fmtCI Jobs Summary
CI任务汇总
| Job | Command | Gate |
|---|---|---|
| Format | | Must pass |
| Lint | | Must pass, zero warnings |
| Deny | | No banned deps, no license violations |
| Test | | Must pass |
| Build | | Must succeed |
| 任务 | 命令 | 要求 |
|---|---|---|
| 格式检查 | | 必须通过 |
| 代码检查 | | 必须通过,零警告 |
| 依赖审计 | | 无禁用依赖,无许可证违规 |
| 测试 | | 必须通过 |
| 构建 | | 必须成功 |
justfile Tasks
justfile任务
just
undefinedjust
undefinedCommon tasks
常用任务
check:
cargo cranky --all-features
cargo test --all-features
cargo fmt --check
cargo deny check
fmt:
cargo fmt
build:
cargo build --all-features --release
config-schema:
cargo run -p app -- config-schema > config-schema.json
openapi:
cargo run -p admin -- openapi > openapi.json
undefinedcheck:
cargo cranky --all-features
cargo test --all-features
cargo fmt --check
cargo deny check
fmt:
cargo fmt
build:
cargo build --all-features --release
config-schema:
cargo run -p app -- config-schema > config-schema.json
openapi:
cargo run -p admin -- openapi > openapi.json
undefinedConventions
规范汇总
| Area | Convention |
|---|---|
| Crate naming | |
| Imports | Module granularity, grouped std/external/local |
| Error types | Per-crate |
| No unsafe | Enforced by cranky deny rule |
| No unwrap/expect | Enforced by cranky; use |
| Shared state | |
| Secrets | |
| Token auth | |
| TLS | rustls + aws-lc-rs only; openssl banned |
| HTTP server | Axum 0.8 with middleware + graceful shutdown |
| Concurrent cache | DashMap — no Mutex needed for shared caches |
| Database | Diesel 2 + r2d2 pool; feature-gated backend (sqlite default) |
| CLI | clap 4 derive — |
| Config | TOML + |
| Shutdown | Tokio signal handler; SIGINT + SIGTERM graceful shutdown |
| Reproducible builds | Path remapping via rustflags |
| Release profile | |
| 领域 | 规范 |
|---|---|
| Crate命名 | |
| 导入 | 模块粒度,按标准库/外部crate/本地crate分组 |
| 错误类型 | 每个crate定义 |
| 禁用unsafe | 通过cranky的禁止规则强制执行 |
| 禁用unwrap/expect | 通过cranky强制执行;仅在启动阶段(失败会导致程序终止)使用 |
| 共享状态 | 通过 |
| 敏感信息 | 使用 |
| 令牌认证 | 使用 |
| TLS | 仅使用rustls + aws-lc-rs;禁用openssl |
| HTTP服务器 | Axum 0.8,支持中间件与优雅停机 |
| 并发缓存 | 使用DashMap——共享缓存无需Mutex |
| 数据库 | Diesel 2 + r2d2连接池;特性门控后端(默认sqlite) |
| 命令行工具 | clap 4派生宏—— |
| 配置 | TOML + |
| 停机 | Tokio信号处理器;支持SIGINT + SIGTERM优雅停机 |
| 可复现构建 | 通过rustflags实现路径重映射 |
| 发布配置 | |