pma-rust

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Rust Project Implementation Guide

Rust项目实现指南

Standard Rust stack and conventions for multi-crate workspace projects.
多crate工作区项目的标准Rust技术栈与规范。

Tech Stack

技术栈

CategoryTechnologyNotes
Core
LanguageRustedition 2024, nightly toolchain
BuildCargo workspacesmulti-crate monorepo
Task runnerjustcommand runner
Async & HTTP
RuntimeTokiofull features
HTTP serverAxum 0.8multipart, middleware, graceful shutdown
HTTP clientreqwest 0.12rustls-tls, no openssl
Data
ORMDiesel 2r2d2 pool, feature-gated backend
Concurrent cacheDashMap 6lock-free concurrent HashMap
CLI
Argument parsingclap 4derive macros, subcommands
Error Handling
Typed errorsthiserror 2per-crate error enums
Error propagationanyhow 1.0boundary crossing
Serialization
Serdeserde 1.0 + serde_json + tomlderive
Linting
Formatrustfmtedition 2024
Lintclippy + cargo-crankystrict deny rules
Dependency auditcargo-denylicense, ban, advisory
Security
TLSrustls 0.23aws-lc-rs provider, no openssl/ring
Token comparisonsubtle 2constant-time to prevent timing attacks
分类技术说明
核心
语言Rust2024版本,nightly工具链
构建Cargo工作区多crate单体仓库
任务运行器just命令运行工具
异步与HTTP
运行时Tokio全功能版
HTTP服务器Axum 0.8支持多部分请求、中间件、优雅停机
HTTP客户端reqwest 0.12基于rustls-tls,不依赖openssl
数据
ORMDiesel 2r2d2连接池,特性门控后端
并发缓存DashMap 6无锁并发HashMap
命令行工具
参数解析clap 4派生宏,支持子命令
错误处理
类型化错误thiserror 2每个crate独立定义错误枚举
错误传播anyhow 1.0跨crate边界的错误传播
序列化
Serdeserde 1.0 + serde_json + toml支持派生宏
代码检查
格式化rustfmt2024版本规范
代码检查clippy + cargo-cranky严格的禁止规则
依赖审计cargo-deny许可证检查、依赖封禁、安全 advisory
安全
TLSrustls 0.23aws-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"
undefined
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"
undefined

Toolchain & Compiler Flags

工具链与编译器标志

rust-toolchain

rust-toolchain

nightly-YYYY-MM-DD
Pin 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",
]
  • tokio_unstable
    : enables tokio console + task IDs
  • 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_unstable
    : 启用tokio控制台与任务ID
  • 路径重映射:实现跨环境的可复现构建

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 = true
toml
avoid-breaking-exported-api = false
allow-unwrap-in-tests = true

Cranky.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:
unsafe
,
unwrap
,
expect
,
panic
, and index slicing are all compile errors. Only site-level
#[allow(...)]
can bypass them (e.g., at startup where failure is fatal).
toml
[cranky]
deny = [
    "unsafe_code",
    "clippy::unwrap_used",
    "clippy::expect_used",
    "clippy::panic",
    "clippy::indexing_slicing",
    "clippy::dbg_macro",
]
allow = [
    "clippy::result_large_err",
]
硬性规则
unsafe
unwrap
expect
panic
以及索引切片操作均为编译错误。仅可通过代码级别的
#[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:
  1. thiserror
    — each crate defines its own error enum with
    #[from]
    conversions:
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),
}
  1. anyhow::Result<T>
    — for propagation across crate boundaries where typed error is not needed.
采用双层错误处理体系:
  1. thiserror
    — 每个crate定义自己的错误枚举,并使用
    #[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),
}
  1. anyhow::Result<T>
    — 用于跨crate边界的错误传播,无需定义类型化错误的场景。

Secret Handling

敏感信息处理

Wrap secrets in a
Secret<T>
type that redacts
Debug
output:
rust
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>
类型中,实现
Debug
输出时自动脱敏:
rust
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
Arc<Mutex<T>>
for shared ownership.
Services
is cloned cheaply (all fields are
Arc
).
rust
pub struct Services {
    pub db: Arc<Mutex<DatabaseConnection>>,
    pub config: Arc<Mutex<AppConfig>>,
    pub state: Arc<Mutex<State>>,
}
所有服务均使用
Arc<Mutex<T>>
实现共享所有权。
Services
可以被低成本克隆(所有字段均为
Arc
类型)。

Protocol 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
enum_dispatch
over
Box<dyn Trait>
for zero-cost polymorphism at runtime.
优先使用
enum_dispatch
而非
Box<dyn Trait>
,实现运行时零成本多态。

Deadlock Detection (Debug Only)

死锁检测(仅调试模式)

Wrap
tokio::sync::Mutex
with a 5-second timeout in debug builds:
rust
#[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),
    }
}
在调试构建中,为
tokio::sync::Mutex
添加5秒超时包装:
rust
#[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.toml
crates/
  db/                               # @project/db
    src/
      lib.rs
      schema.rs                     # diesel print-schema 输出
      models.rs                     # Queryable/Insertable 结构体
      migrations/
        00000000000000_create_xxx/
          up.sql
          down.sql
    diesel.toml

diesel.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
diesel::r2d2
for connection pooling:
rust
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::r2d2
实现数据库连接池:
rust
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
undefined
bash
undefined

Install 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
undefined
diesel print-schema > src/schema.rs
undefined

Configuration System

配置系统

TOML config with
serde::Deserialize
+
#[serde(default)]
for per-field defaults.
基于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
undefined
toml
undefined

config.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"
undefined

Conventions

规范

  • Use TOML (not YAML) — Rust ecosystem standard, better type safety
  • #[serde(default)]
    on every optional struct for graceful partial configs
  • #[serde(default = "fn_name")]
    for per-field defaults using free functions
  • Implement
    Default
    on config structs so
    #[serde(default)]
    on parent works
  • Auto-generate config file on first run with sensible defaults
  • Config path via env var (
    APP_CONFIG
    ), default to
    config.toml
  • 使用TOML(而非YAML)—— Rust生态系统标准格式,类型安全性更好
  • 所有可选结构体字段添加
    #[serde(default)]
    ,实现优雅的部分配置加载
  • 对于字段级默认值,使用
    #[serde(default = "fn_name")]
    绑定自由函数
  • 为配置结构体实现
    Default
    trait,确保父结构体的
    #[serde(default)]
    正常工作
  • 首次运行时自动生成带合理默认值的配置文件
  • 通过环境变量(
    APP_CONFIG
    )指定配置路径,默认路径为
    config.toml

CLI (clap)

命令行工具(clap)

Use
clap
with derive macros for type-safe CLI argument parsing.
使用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
    #[derive(Parser)]
    on the top-level struct,
    #[derive(Subcommand)]
    on the command enum
  • Use doc comments (
    ///
    ) for help text — clap derives
    --help
    from them
  • #[arg(short, long)]
    for flags,
    #[arg(default_value = "...")]
    for defaults
  • Keep CLI struct in
    crates/app/src/cli.rs
    , re-export from
    main.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
State(state): State<Arc<AppState>>
in handlers.
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
subtle::ConstantTimeEq
for token comparison to prevent timing attacks:
rust
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::ConstantTimeEq
进行令牌比较,防止时序攻击:
rust
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
subtle::ConstantTimeEq
for auth token, API key, and HMAC comparisons. Never use
==
for secrets.
对于认证令牌、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
DashMap
for in-memory caches shared across async tasks:
rust
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
Mutex
needed —
DashMap
handles concurrent reads/writes internally.
使用
DashMap
实现跨异步任务共享的内存缓存:
rust
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);
    }
}
无需使用
Mutex
——
DashMap
内部已处理并发读写。

Logging

日志

Multi-layer tracing setup with
tracing-subscriber
:
  • Console: ANSI colors, env-filter, local-time
  • JSON: structured JSON format for
    --log-format json
  • Database: custom
    Layer
    that captures events at INFO+ and writes to DB
  • Filter to only
    {crate_prefix}
    -prefixed targets for application logs
基于
tracing-subscriber
的多层日志配置:
  • 控制台:ANSI颜色、环境变量过滤、本地时间
  • JSON:结构化JSON格式,用于
    --log-format json
    参数
  • 数据库:自定义
    Layer
    ,捕获INFO及以上级别的事件并写入数据库
  • 仅过滤以
    {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:
fmt
is the first gate — no point running clippy/test if formatting is broken.
两阶段流水线:首先检查代码格式,然后并行执行其他检查。
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>
关键设计:
fmt
是第一关——如果代码格式不符合规范,执行clippy或测试毫无意义。

CI Jobs Summary

CI任务汇总

JobCommandGate
Format
cargo fmt --check
Must pass
Lint
cargo cranky --all-features
Must pass, zero warnings
Deny
cargo deny check
No banned deps, no license violations
Test
cargo test --all-features
Must pass
Build
cargo build --all-features --release
Must succeed
任务命令要求
格式检查
cargo fmt --check
必须通过
代码检查
cargo cranky --all-features
必须通过,零警告
依赖审计
cargo deny check
无禁用依赖,无许可证违规
测试
cargo test --all-features
必须通过
构建
cargo build --all-features --release
必须成功

justfile Tasks

justfile任务

just
undefined
just
undefined

Common 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
undefined
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
undefined

Conventions

规范汇总

AreaConvention
Crate naming
{project}-{module}
(e.g.,
gated-core
,
gated-common
)
ImportsModule granularity, grouped std/external/local
Error typesPer-crate
thiserror
enum,
anyhow
for boundaries
No unsafeEnforced by cranky deny rule
No unwrap/expectEnforced by cranky; use
#[allow]
only at fatal startup points
Shared state
Arc<Mutex<T>>
via
Services
DI container
Secrets
Secret<T>
wrapper with redacted Debug
Token auth
subtle::ConstantTimeEq
— never
==
for secrets
TLSrustls + aws-lc-rs only; openssl banned
HTTP serverAxum 0.8 with middleware + graceful shutdown
Concurrent cacheDashMap — no Mutex needed for shared caches
DatabaseDiesel 2 + r2d2 pool; feature-gated backend (sqlite default)
CLIclap 4 derive —
Parser
+
Subcommand
, doc comments for help
ConfigTOML +
serde(default)
+ auto-generate on first run
ShutdownTokio signal handler; SIGINT + SIGTERM graceful shutdown
Reproducible buildsPath remapping via rustflags
Release profile
lto = true
,
panic = "abort"
,
strip = "debuginfo"
领域规范
Crate命名
{project}-{module}
(例如:
gated-core
,
gated-common
导入模块粒度,按标准库/外部crate/本地crate分组
错误类型每个crate定义
thiserror
枚举,跨边界使用
anyhow
禁用unsafe通过cranky的禁止规则强制执行
禁用unwrap/expect通过cranky强制执行;仅在启动阶段(失败会导致程序终止)使用
#[allow]
共享状态通过
Services
依赖注入容器使用
Arc<Mutex<T>>
敏感信息使用
Secret<T>
包装,实现Debug脱敏
令牌认证使用
subtle::ConstantTimeEq
——绝不要用
==
比较敏感信息
TLS仅使用rustls + aws-lc-rs;禁用openssl
HTTP服务器Axum 0.8,支持中间件与优雅停机
并发缓存使用DashMap——共享缓存无需Mutex
数据库Diesel 2 + r2d2连接池;特性门控后端(默认sqlite)
命令行工具clap 4派生宏——
Parser
+
Subcommand
,使用文档注释编写帮助信息
配置TOML +
serde(default)
+ 首次运行自动生成
停机Tokio信号处理器;支持SIGINT + SIGTERM优雅停机
可复现构建通过rustflags实现路径重映射
发布配置
lto = true
,
panic = "abort"
,
strip = "debuginfo"