Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

15. Crate 架构和 API 设计 🟡

你将学到:

  • 模块布局约定和重新导出策略
  • 精制 crate 的公共 API 设计清单
  • 人体工学的参数模式:impl Into、AsRef、Cow
  • 使用 TryFrom 和已验证类型的“解析,不要验证“
  • 特性标志、条件编译和工作区组织

模块布局约定

my_crate/
├── Cargo.toml
├── src/
│   ├── lib.rs          # Crate 根 —— 重新导出和公共 API
│   ├── config.rs       # 特性模块
│   ├── parser/         # 带有子模块的复杂模块
│   │   ├── mod.rs      # 或父级的 parser.rs(Rust 2018+)
│   │   ├── lexer.rs
│   │   └── ast.rs
│   ├── error.rs        # 错误类型
│   └── utils.rs        # 内部辅助(pub(crate))
├── tests/
│   └── integration.rs  # 集成测试
├── benches/
│   └── perf.rs         # 基准测试
└── examples/
    └── basic.rs        # cargo run --example basic
#![allow(unused)]
fn main() {
// lib.rs —— 使用重新导出策划你的公共 API:
mod config;
mod error;
mod parser;
mod utils;

// 重新导出用户需要的内容:
pub use config::Config;
pub use error::Error;
pub use parser::Parser;

// 公共类型在 crate 根 —— 用户编写:
// use my_crate::Config;
// 而不是:use my_crate::config::Config;
}

可见性修饰符:

修饰符对谁可见
pub所有人
pub(crate)仅这个 crate
pub(super)父模块
pub(in path)特定的祖先模块
(无)当前模块及其子模块

公共 API 设计清单

  1. 接受引用,返回 owned —— fn process(input: &str) -> String
  2. 对参数使用 impl Trait —— fn read(r: impl Read) 而不是 fn read<R: Read>(r: R),签名更清晰
  3. 返回 Result,而不是 panic! —— 让调用者决定如何处理错误
  4. 实现标准 trait —— Debug、Display、Clone、Default、From/Into
  5. 使无效状态不可表示 —— 使用类型状态和新类型
  6. 对复杂配置使用 builder 模式 —— 如果字段是必需的则使用类型状态
  7. 密封你不想让用户实现的 trait —— pub trait Sealed: private::Sealed {}
  8. 标记类型和函数为 #[must_use] —— 防止静默丢弃重要的 Result、守卫或值。适用于任何忽略返回值几乎肯定是 bug 的类型:
    #![allow(unused)]
    fn main() {
    #[must_use = "丢弃守卫会立即释放锁"]
    pub struct LockGuard<'a, T> { /* ... */ }
    
    #[must_use]
    pub fn validate(input: &str) -> Result<ValidInput, ValidationError> { /* ... */ }
    }
#![allow(unused)]
fn main() {
// 密封 trait 模式 —— 用户可以使用但不能实现:
mod private {
    pub trait Sealed {}
}

pub trait DatabaseDriver: private::Sealed {
    fn connect(&self, url: &str) -> Connection;
}

// 只有这个 crate 中的类型可以实现 Sealed → 只有我们可以实现 DatabaseDriver
pub struct PostgresDriver;
impl private::Sealed for PostgresDriver {}
impl DatabaseDriver for PostgresDriver {
    fn connect(&self, url: &str) -> Connection { /* ... */ }
}
}

#[non_exhaustive] —— 标记公共枚举和结构体,这样添加变体或字段不是破坏性变更。下游 crate 必须在 match 语句中使用通配符臂(_ =>),并且不能使用结构体字面量语法构造类型:

#![allow(unused)]
fn main() {
#[non_exhaustive]
pub enum DiagError {
    Timeout,
    HardwareFault,
    // 在未来的版本中添加新变体不是语义化版本破坏。
}
}

人体工学参数模式 —— impl Into、AsRef、Cow

最有影响力的 Rust API 模式之一是在函数参数中接受最通用的类型,这样调用者不需要在每个调用点重复 .to_string()、&*s 或 .as_ref()。这是 Rust 特定版本的“接受时宽容“。

impl Into<T> —— 接受任何可转换的内容

#![allow(unused)]
fn main() {
// ❌ 摩擦:调用者必须手动转换
fn connect(host: String, port: u16) -> Connection {
    // ...
}
connect("localhost".to_string(), 5432);  // 讨厌的 .to_string()
connect(hostname.clone(), 5432);          // 如果已经有 String 则不必要的 clone

// ✅ 人体工学:接受任何可以转换为 String 的内容
fn connect(host: impl Into<String>, port: u16) -> Connection {
    let host = host.into();  // 在函数内部转换一次
    // ...
}
connect("localhost", 5432);     // &str —— 零摩擦
connect(hostname, 5432);        // String —— move,无 clone
}

这有效是因为 Rust 的 From/Into trait 对提供通用转换。 当你接受 impl Into<T> 时,你是在说:“给我任何知道如何变成 T 的东西。”

AsRef<T> —— 借用为引用

AsRef<T> 是 Into<T> 的借用对应物。当你只需要读取数据而不需要所有权时使用它:

#![allow(unused)]
fn main() {
use std::path::Path;

// ❌ 强制调用者转换为 &Path
fn file_exists(path: &Path) -> bool {
    path.exists()
}
file_exists(Path::new("/tmp/test.txt"));  // 笨拙

// ✅ 接受任何可以表现为 &Path 的内容
fn file_exists(path: impl AsRef<Path>) -> bool {
    path.as_ref().exists()
}
file_exists("/tmp/test.txt");                    // &str ✅
file_exists(String::from("/tmp/test.txt"));      // String ✅
file_exists(Path::new("/tmp/test.txt"));         // &Path ✅
file_exists(PathBuf::from("/tmp/test.txt"));     // PathBuf ✅

// 字符串参数的相同模式:
fn log_message(msg: impl AsRef<str>) {
    println!("[LOG] {}", msg.as_ref());
}
log_message("hello");                    // &str ✅
log_message(String::from("hello"));      // String ✅
}

Cow<T> —— 写入时克隆

Cow<'a, T>(Clone on Write)延迟分配直到需要修改。 它持有借用的 &T 或 owned 的 T::Owned。当大多数调用不需要修改数据时这很完美:

#![allow(unused)]
fn main() {
use std::borrow::Cow;

/// 规范化诊断消息 —— 仅在需要更改时分配。
fn normalize_message(msg: &str) -> Cow<'_, str> {
    if msg.contains('\t') || msg.contains('\r') {
        // 必须分配 —— 我们需要修改内容
        Cow::Owned(msg.replace('\t', "    ").replace('\r', ""))
    } else {
        // 无分配 —— 只是借用原始内容
        Cow::Borrowed(msg)
    }
}

// 大多数消息无分配地通过:
let clean = normalize_message("All tests passed");          // 借用 —— 免费
let fixed = normalize_message("Error:\tfailed\r\n");        // Owned —— 分配

// Cow<str> 实现 Deref<Target=str>,所以它像 &str 一样工作:
println!("{}", clean);
println!("{}", fixed.to_uppercase());
}

快速参考:使用哪个

你需要在函数内部拥有数据的所有权吗?
├── 是 → impl Into<T>
│         "给我任何可以变成 T 的东西"
└── 否  → 你只需要读取它吗?
     ├── 是 → impl AsRef<T> 或 &T
     │         "给我任何我可以借用为 &T 的东西"
     └── 可能(有时可能需要修改?)
          └── Cow<'_, T>
              "可能时借用,必须时才克隆"
模式所有权分配何时使用
&str借用从不简单字符串参数
impl AsRef<str>借用从不接受 String、&str 等 —— 只读
impl Into<String>Owned转换时接受 &str、String —— 将存储/拥有
Cow<'_, str>任一仅在修改时通常不修改的处理
&[u8] / impl AsRef<[u8]>借用从不字节导向的 API

Borrow<T> vs AsRef<T>:两者都提供 &T,但 Borrow<T> 额外保证 Eq、Ord 和 Hash 在原始形式和借用形式之间一致。 这就是为什么 HashMap<String, V>::get() 接受 &Q where String: Borrow<Q> —— 而不是 AsRef。 当借用形式用作查找键时使用 Borrow;对一般“给我引用“参数使用 AsRef。

在 API 中组合转换

#![allow(unused)]
fn main() {
/// 使用人体工学参数的精心设计诊断 API:
pub struct DiagRunner {
    name: String,
    config_path: PathBuf,
    results: HashMap<String, TestResult>,
}

impl DiagRunner {
    /// 对 name 接受任何字符串类型,对 config 接受任何路径类型。
    pub fn new(
        name: impl Into<String>,
        config_path: impl Into<PathBuf>,
    ) -> Self {
        DiagRunner {
            name: name.into(),
            config_path: config_path.into(),
        }
    }

    /// 对只读查找接受任何 AsRef<str>。
    pub fn get_result(&self, test_name: impl AsRef<str>) -> Option<&TestResult> {
        self.results.get(test_name.as_ref())
    }
}

// 所有这些都零摩擦地工作:
let runner = DiagRunner::new("GPU Diag", "/etc/diag_tool/config.json");
let runner = DiagRunner::new(format!("Diag-{}", node_id), config_path);
let runner = DiagRunner::new(name_string, path_buf);
}

案例研究:设计公共 Crate API —— 之前和之后

一个将字符串类型的内部 API 演化为人体工学、类型安全的公共 API 的真实示例。考虑一个配置解析 crate:

之前(字符串类型,容易误用):

#![allow(unused)]
fn main() {
// ❌ 所有参数都是字符串 —— 无编译时验证
pub fn parse_config(path: &str, format: &str, strict: bool) -> Result<Config, String> {
    // 哪些格式是有效的?"json"?"JSON"?"Json"?
    // path 是文件路径还是 URL?
    // "strict" 到底是什么意思?
    todo!()
}
}

之后(类型安全、自文档化):

#![allow(unused)]
fn main() {
use std::path::Path;

/// 支持的文件配置格式。
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]  // 添加格式不会破坏下游
pub enum Format {
    Json,
    Toml,
    Yaml,
}

/// 控制解析严格性。
#[derive(Debug, Clone, Copy, Default)]
pub enum Strictness {
    /// 拒绝未知字段(库的默认值)
    #[default]
    Strict,
    /// 忽略未知字段(对前向兼容配置有用)
    Lenient,
}

pub fn parse_config(
    path: &Path,          // 类型强制:必须是文件系统路径
    format: Format,       // 枚举:不可能传递无效格式
    strictness: Strictness,  // 命名替代,而不是裸 bool
) -> Result<Config, ConfigError> {
    todo!()
}
}

改进了什么:

方面之前之后
格式验证运行时字符串比较编译时枚举
路径类型裸 &str(可以是任何东西)&Path(特定于文件系统)
严格性神秘 bool自文档化枚举
错误类型String(不透明)ConfigError(结构化)
可扩展性破坏性变更#[non_exhaustive]

经验法则:如果你发现自己编写对字符串值的 match,考虑用枚举替换参数。 如果参数是上下文中不明显的布尔值,使用双变体枚举代替。


解析不要验证 —— TryFrom 和已验证类型

“解析,不要验证“是一个原则,它说:不要检查数据然后传递未经检查的原始形式 —— 而是解析为只有在数据有效时才能存在的类型。 Rust 的 TryFrom trait 是此的标准工具。

问题:验证无强制

#![allow(unused)]
fn main() {
// ❌ 验证后使用:没有阻止在检查后使用无效值
fn process_port(port: u16) {
    if port == 0 || port > 65535 {
        panic!("Invalid port");           // 我们检查了,但是...
    }
    start_server(port);                    // 如果有人直接调用 start_server(0) 怎么办?
}

// ❌ 字符串类型:email 只是 String —— 任何垃圾都能通过
fn send_email(to: String, body: String) {
    // `to` 实际上是有效的 email 吗?我们不知道。
    // 有人可以传递 "not-an-email",我们只在 SMTP 服务器发现。
}
}

解决方案:使用 TryFrom 解析为已验证的新类型

use std::convert::TryFrom;
use std::fmt;

/// 已验证的 TCP 端口号(1–65535)。
/// 如果你有 `Port`,它保证有效。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Port(u16);

impl TryFrom<u16> for Port {
    type Error = PortError;

    fn try_from(value: u16) -> Result<Self, Self::Error> {
        if value == 0 {
            Err(PortError::Zero)
        } else {
            Ok(Port(value))
        }
    }
}

impl Port {
    pub fn get(&self) -> u16 { self.0 }
}

#[derive(Debug)]
pub enum PortError {
    Zero,
    InvalidFormat,
}

impl fmt::Display for PortError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PortError::Zero => write!(f, "port must be non-zero"),
            PortError::InvalidFormat => write!(f, "invalid port format"),
        }
    }
}

impl std::error::Error for PortError {}

// 现在类型系统强制执行有效性:
fn start_server(port: Port) {
    // 不需要验证 —— Port 只能通过 TryFrom 构造,
    // 它已经验证它是有效的。
    println!("Listening on port {}", port.get());
}

// 用法:
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let port = Port::try_from(8080)?;   // ✅ 在边界处验证一次
    start_server(port);                  // 下游无需重新验证

    let bad = Port::try_from(0);         // ❌ Err(PortError::Zero)
    Ok(())
}

真实示例:已验证的 IPMI 地址

#![allow(unused)]
fn main() {
/// 已验证的 IPMI 从地址(0x20–0xFE,仅偶数)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IpmiAddr(u8);

#[derive(Debug)]
pub enum IpmiAddrError {
    Odd(u8),
    OutOfRange(u8),
}

impl fmt::Display for IpmiAddrError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            IpmiAddrError::Odd(v) => write!(f, "IPMI address 0x{v:02X} must be even"),
            IpmiAddrError::OutOfRange(v) => {
                write!(f, "IPMI address 0x{v:02X} out of range (0x20..=0xFE)")
            }
        }
    }
}

impl TryFrom<u8> for IpmiAddr {
    type Error = IpmiAddrError;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        if value % 2 != 0 {
            Err(IpmiAddrError::Odd(value))
        } else if value < 0x20 || value > 0xFE {
            Err(IpmiAddrError::OutOfRange(value))
        } else {
            Ok(IpmiAddr(value))
        }
    }
}

impl IpmiAddr {
    pub fn get(&self) -> u8 { self.0 }
}

// 下游代码从不需要重新检查:
fn send_ipmi_command(addr: IpmiAddr, cmd: u8, data: &[u8]) -> Result<Vec<u8>, IpmiError> {
    // addr.get() 保证是有效的、偶数的 IPMI 地址
    raw_ipmi_send(addr.get(), cmd, data)
}
}

使用 FromStr 解析字符串

对于通常从文本解析的类型(CLI 参数、配置文件),实现 FromStr:

#![allow(unused)]
fn main() {
use std::str::FromStr;

impl FromStr for Port {
    type Err = PortError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let n: u16 = s.parse().map_err(|_| PortError::InvalidFormat)?;
        Port::try_from(n)
    }
}

// 现在可以使用 .parse():
let port: Port = "8080".parse()?;   // 一步验证

// 与 clap CLI 解析一起使用:
// #[derive(Parser)]
// struct Args {
//     #[arg(short, long)]
//     port: Port,   // clap 自动调用 FromStr
// }
}

复杂验证的 TryFrom 链

#![allow(unused)]
fn main() {
/// 已验证的配置,只有当所有字段有效时才能存在。
pub struct ValidConfig {
    pub host: Hostname,
    pub port: Port,
    pub timeout_ms: Timeout,
}

impl TryFrom<RawConfig> for ValidConfig {
    type Error = ConfigError;

    fn try_from(raw: RawConfig) -> Result<Self, Self::Error> {
        Ok(ValidConfig {
            host: Hostname::try_from(raw.host)
                .map_err(ConfigError::InvalidHost)?,
            port: Port::try_from(raw.port)
                .map_err(ConfigError::InvalidPort)?,
            timeout_ms: Timeout::try_from(raw.timeout_ms)
                .map_err(ConfigError::InvalidTimeout)?,
        })
    }
}

// 在边界处解析一次,在任何地方使用已验证的类型:
fn load_config(path: &str) -> Result<ValidConfig, ConfigError> {
    let raw: RawConfig = serde_json::from_str(&std::fs::read_to_string(path)?)?;
    ValidConfig::try_from(raw)  // 所有验证发生在这里
}
}

总结:验证 vs 解析

方法数据检查?编译器强制执行有效性?需要重新验证?
运行时检查(if/assert)✅❌每个函数边界
已验证新类型 + TryFrom✅✅从不 —— 类型是证明

规则:在边界处解析,在内部任何地方使用已验证类型。 原始字符串、整数和字节切片进入你的系统,通过 TryFrom/FromStr 解析为已验证类型,从那时起类型系统保证它们是有效的。

特性标志和条件编译

# Cargo.toml
[features]
default = ["json"]          # 默认启用
json = ["dep:serde_json"]   # 启用 JSON 支持
xml = ["dep:quick-xml"]     # 启用 XML 支持
full = ["json", "xml"]      # 元特性:启用所有

[dependencies]
serde = "1"
serde_json = { version = "1", optional = true }
quick-xml = { version = "0.31", optional = true }
#![allow(unused)]
fn main() {
// 基于特性的条件编译:
#[cfg(feature = "json")]
pub fn to_json<T: serde::Serialize>(value: &T) -> String {
    serde_json::to_string(value).unwrap()
}

#[cfg(feature = "xml")]
pub fn to_xml<T: serde::Serialize>(value: &T) -> String {
    quick_xml::se::to_string(value).unwrap()
}

// 如果未启用必需特性则编译错误:
#[cfg(not(any(feature = "json", feature = "xml")))]
compile_error!("必须启用至少一个格式特性(json、xml)");
}

最佳实践:

  • 保持 default 特性最小 —— 用户可以选择加入
  • 使用 dep: 语法(Rust 1.60+)用于可选依赖以避免创建隐式特性
  • 在 README 和 crate 级文档中记录特性

工作区组织

对于大型项目,使用 Cargo 工作区共享依赖和构建产物:

# Root Cargo.toml
[workspace]
members = [
    "core",         # 共享类型和 trait
    "parser",       # 解析库
    "server",       # 二进制 —— 主应用程序
    "client",       # 客户端库
    "cli",          # CLI 二进制
]

# 共享依赖版本:
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
tracing = "0.1"

# 在每个成员的 Cargo.toml 中:
# [dependencies]
# serde = { workspace = true }

好处:

  • 单个 Cargo.lock —— 所有 crate 使用相同的依赖版本
  • cargo test --workspace 运行所有测试
  • 共享构建缓存 —— 编译一个 crate 有利于所有
  • 组件之间清晰的依赖边界

.cargo/config.toml:项目级配置

.cargo/config.toml 文件(在工作区根目录或 $HOME/.cargo/ 中) 自定义 Cargo 行为而无需修改 Cargo.toml:

# .cargo/config.toml

# 这个工作区的默认 target
[build]
target = "x86_64-unknown-linux-gnu"

# 自定义运行器 —— 例如通过 QEMU 运行交叉编译的二进制文件
[target.aarch64-unknown-linux-gnu]
runner = "qemu-aarch64-static"
linker = "aarch64-linux-gnu-gcc"

# Cargo 别名 —— 自定义快捷命令
[alias]
xt = "test --workspace --release"        # cargo xt = release 模式下运行所有测试
ci = "clippy --workspace -- -D warnings" # cargo ci = lint,警告视为错误
cov = "llvm-cov --workspace"             # cargo cov = 覆盖率(需要 cargo-llvm-cov)

# 构建脚本的环境变量
[env]
IPMI_LIB_PATH = "/usr/lib/bmc"

# 使用自定义注册表(用于内部包)
# [registries.internal]
# index = "https://gitlab.internal/crates/index"

常见配置模式:

设置目的示例
[build] target默认编译 targetx86_64-unknown-linux-musl 用于静态构建
[target.X] runner如何运行二进制文件"qemu-aarch64-static" 用于交叉编译
[target.X] linker使用哪个链接器"aarch64-linux-gnu-gcc"
[alias]自定义 cargo 子命令xt = "test --workspace"
[env]构建时环境变量库路径、特性切换
[net] offline防止网络访问true 用于隔离构建

编译时环境变量:env!() 和 option_env!()

Rust 可以在编译时将环境变量嵌入二进制文件 —— 对版本字符串、构建元数据和配置很有用:

#![allow(unused)]
fn main() {
// env!() —— 如果变量缺失则在编译时 panic
const VERSION: &str = env!("CARGO_PKG_VERSION"); // Cargo.toml 中的 "0.1.0"
const PKG_NAME: &str = env!("CARGO_PKG_NAME");   // Cargo.toml 中的 crate 名称

// option_env!() —— 返回 Option<&str>,如果缺失则不 panic
const BUILD_SHA: Option<&str> = option_env!("GIT_SHA");
const BUILD_TIME: Option<&str> = option_env!("BUILD_TIMESTAMP");

fn print_version() {
    println!("{PKG_NAME} v{VERSION}");
    if let Some(sha) = BUILD_SHA {
        println!("  commit: {sha}");
    }
    if let Some(time) = BUILD_TIME {
        println!("  built:  {time}");
    }
}
}

Cargo 自动设置许多有用的环境变量:

变量值使用场景
CARGO_PKG_VERSION"1.2.3"版本报告
CARGO_PKG_NAME"diag_tool"二进制标识
CARGO_PKG_AUTHORS来自 Cargo.toml关于/帮助文本
CARGO_MANIFEST_DIRCargo.toml 的绝对路径定位测试数据文件
OUT_DIR构建输出目录build.rs 代码生成目标
TARGETTarget triplebuild.rs 中特定于平台的逻辑

你可以从 build.rs 设置自定义环境变量:

// build.rs
fn main() {
    println!("cargo::rustc-env=GIT_SHA={}", git_sha());
    println!("cargo::rustc-env=BUILD_TIMESTAMP={}", timestamp());
}

cfg_attr:条件属性

cfg_attr 仅当条件为真时应用属性。这比 #[cfg()] 更有针对性,后者包含/排除整个项:

#![allow(unused)]
fn main() {
// 仅当启用 "serde" 特性时 derive Serialize:
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct DiagResult {
    pub fc: u32,
    pub passed: bool,
    pub message: String,
}
// 无 "serde" 特性:根本不需要 serde 依赖
// 有 "serde" 特性:DiagResult 可序列化

// 测试的条件属性:
#[cfg_attr(test, derive(PartialEq))]  // 仅在测试构建中 derive PartialEq
pub struct LargeStruct { /* ... */ }

// 平台特定函数属性:
#[cfg_attr(target_os = "linux", link_name = "ioctl")]
#[cfg_attr(target_os = "freebsd", link_name = "__ioctl")]
extern "C" fn platform_ioctl(fd: i32, request: u64) -> i32;
}
模式做什么
#[cfg(feature = "x")]包含/排除整个项
#[cfg_attr(feature = "x", derive(Foo))]仅当启用特性 “x” 时添加 derive(Foo)
#[cfg_attr(test, allow(unused))]仅在测试构建中抑制警告
#[cfg_attr(doc, doc = "...")]仅在 cargo doc 中可见的文档

cargo deny 和 cargo audit:供应链安全

# 安装安全审计工具
cargo install cargo-deny
cargo install cargo-audit

# 检查依赖中的已知漏洞
cargo audit

# 综合检查:许可证、禁止、咨询、来源
cargo deny check

使用工作区根目录的 deny.toml 配置 cargo deny:

# deny.toml
[advisories]
vulnerability = "deny"      # 拒绝已知漏洞
unmaintained = "warn"        # 警告不再维护的 crate

[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause"]
deny = ["GPL-3.0"]          # 拒绝 copyleft 许可证

[bans]
multiple-versions = "warn"  # 如果同一 crate 有多个版本则警告
deny = [
    { name = "openssl" },   # 强制使用 rustls
]

[sources]
allow-git = []              # 生产环境中无 git 依赖
工具目的何时运行
cargo audit检查依赖中的已知 CVECI 流水线、发布前
cargo deny check许可证、禁止、咨询、来源CI 流水线
cargo deny check licenses仅许可证合规开源前
cargo deny check bans防止特定 crate强制执行架构决策

文档测试:文档中的测试

Rust 文档注释(///)可以包含作为测试编译和运行的代码块:

#![allow(unused)]
fn main() {
/// 从字符串解析诊断错误码。
///
/// # Examples
///
/// ```
/// use my_crate::parse_fc;
///
/// let fc = parse_fc("FC:12345").unwrap();
/// assert_eq!(fc, 12345);
/// ```
///
/// 无效输入返回错误:
///
/// ```
/// use my_crate::parse_fc;
///
/// assert!(parse_fc("not-a-fc").is_err());
/// ```
pub fn parse_fc(input: &str) -> Result<u32, ParseError> {
    input.strip_prefix("FC:")
        .ok_or(ParseError::MissingPrefix)?
        .parse()
        .map_err(ParseError::InvalidNumber)
}
}
cargo test --doc  # 仅运行文档测试
cargo test        # 运行单元 + 集成 + 文档测试

模块级文档在文件顶部使用 //!:

#![allow(unused)]
fn main() {
//! # 诊断框架
//!
//! 这个 crate 提供核心诊断执行引擎。
//! 它支持运行诊断测试、收集结果、
//! 并通过 IPMI 报告给 BMC。
//!
//! ## 快速入门
//!
//! ```no_run
//! use diag_framework::Framework;
//!
//! let mut fw = Framework::new("config.json")?;
//! fw.run_all_tests()?;
//! ```
}

使用 Criterion 进行基准测试

完整覆盖:参见 使用 criterion 进行基准测试 第 13 章(测试和基准测试模式)中的完整 criterion 设置、API 示例和与 cargo bench 的比较表。 下面是特定于架构的用法的快速参考。

在对 crate 的公共 API 进行基准测试时,将基准测试放在 benches/ 中, 并专注于热点路径 —— 通常是解析器、序列化器或验证边界:

cargo bench                  # 运行所有基准测试
cargo bench -- parse_config  # 运行特定基准测试
# 结果在 target/criterion/ 中,带有 HTML 报告

关键要点 —— 架构和 API 设计

  • 接受最通用的类型(impl Into、impl AsRef、Cow);返回最具体的
  • 解析不要验证:使用 TryFrom 创建通过构造有效的类型
  • 公共枚举上的 #[non_exhaustive] 防止添加变体时的破坏性变更
  • #[must_use] 捕获重要值的静默丢弃

另见: 第 9 章 — 错误处理 了解公共 API 中的错误类型设计。第 13 章 — 测试 了解测试 crate 的公共 API。


练习:Crate API 重构 ★★(约 30 分钟)

将以下“字符串类型“的 API 重构为使用 TryFrom、新类型和 builder 模式的 API:

// 之前:容易误用
fn create_server(host: &str, port: &str, max_conn: &str) -> Server { ... }

设计一个 ServerConfig,带有已验证类型 Host、Port(1–65535)和 MaxConnections(1–10000),在解析时拒绝无效值。

🔑 解答
#[derive(Debug, Clone)]
struct Host(String);

impl TryFrom<&str> for Host {
    type Error = String;
    fn try_from(s: &str) -> Result<Self, String> {
        if s.is_empty() { return Err("host cannot be empty".into()); }
        if s.contains(' ') { return Err("host cannot contain spaces".into()); }
        Ok(Host(s.to_string()))
    }
}

#[derive(Debug, Clone, Copy)]
struct Port(u16);

impl TryFrom<u16> for Port {
    type Error = String;
    fn try_from(p: u16) -> Result<Self, String> {
        if p == 0 { return Err("port must be >= 1".into()); }
        Ok(Port(p))
    }
}

#[derive(Debug, Clone, Copy)]
struct MaxConnections(u32);

impl TryFrom<u32> for MaxConnections {
    type Error = String;
    fn try_from(n: u32) -> Result<Self, String> {
        if n == 0 || n > 10_000 {
            return Err(format!("max_connections must be 1–10000, got {n}"));
        }
        Ok(MaxConnections(n))
    }
}

#[derive(Debug)]
struct ServerConfig {
    host: Host,
    port: Port,
    max_connections: MaxConnections,
}

impl ServerConfig {
    fn new(host: Host, port: Port, max_connections: MaxConnections) -> Self {
        ServerConfig { host, port, max_connections }
    }
}

fn main() {
    let config = ServerConfig::new(
        Host::try_from("localhost").unwrap(),
        Port::try_from(8080).unwrap(),
        MaxConnections::try_from(100).unwrap(),
    );
    println!("{config:?}");

    // 在解析时捕获无效值:
    assert!(Host::try_from("").is_err());
    assert!(Port::try_from(0).is_err());
    assert!(MaxConnections::try_from(99999).is_err());
}