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

rust-config-tree 手册

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

这是 rust-config-tree 的中文手册。手册介绍配置树加载、模板生成、 JSON Schema(JSON 结构定义)、CLI(命令行接口) 集成和来源追踪。

可以从简介快速开始或可运行的 示例开始阅读。

简介

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

rust-config-tree 为使用分层配置文件的 Rust(系统编程语言) 应用提供可复用的 配置树加载能力和 CLI(命令行接口) 辅助能力。

这个 crate(软件包) 按照清晰的职责边界组织各项功能:

  • confique 负责 schema(结构定义)、代码默认值、校验和配置模板生成。
  • figment 负责运行时加载和运行时来源元数据。
  • rust-config-tree 负责 include(包含文件) 的递归遍历、include(包含文件) 路径解析、.env 文件加载、模板目标发现,以及可复用的 clap(命令行解析库) 命令。

它适合这种自然的配置文件布局:

include:
  - config/server.yaml
  - config/database.yaml

log:
  level: info

每个被 include(包含) 的文件都可以使用相同的 schema(结构定义) 形状。加载器会从 声明 include(包含) 的文件所在目录解析相对路径。最终得到的配置仍然是一个普通的 confique schema(结构定义) 值。

主要能力

  • 它会递归遍历 include(包含文件),并检测循环包含。
  • 它会从声明 include(包含) 的文件解析相对路径。
  • 它会在环境变量 provider(值提供器) 求值之前加载 .env 文件。
  • 它会使用 schema(结构定义) 中声明的环境变量名,并且不会按分隔符拆分变量名。
  • 它会通过 Figment(配置合并库) metadata(元数据) 追踪运行时来源。
  • 它会通过 tracing 输出 TRACE(追踪级别) 的来源追踪事件。
  • 它会生成 Draft 7 JSON Schema(JSON 结构定义),供编辑器补全和基础 schema(结构定义) 检查使用。
  • 应用代码通过 #[config(validate = Self::validate)] 实现字段值合法性校验, load_configconfig-validate 会执行这个校验。
  • 它会生成 YAML、TOML、JSON 和 JSON5 配置模板。
  • 它会为生成的 TOML 模板写入 #:schema,为 YAML 模板写入 YAML Language Server(YAML 语言服务器) modeline(模式声明行),并为 JSON 和 JSON5 模板写入 $schema 字段。
  • 它会按 x-tree-split 标记拆分嵌套 section(配置段) 的 YAML 模板。
  • 它内置了用于 config template(配置模板)、JSON Schema(JSON 结构定义) 和 shell completion(命令补全) 的 clap(命令行解析库) 子命令。
  • 它为不使用 confique 的调用方提供低层 tree API(树形接口)。

主要入口

多数应用会使用这些 API(应用程序接口):

  • load_config::<S>(path) 会加载最终 schema(结构定义)。
  • load_config_with_figment::<S>(path) 会加载 schema(结构定义),并返回用于 来源追踪的 Figment(配置合并库) graph(配置图)。
  • write_config_templates::<S>(config_path, output_path) 会写入 root(根配置) 模板和递归发现的子模板。
  • write_config_schemas::<S>(output_path) 会写入 root(根配置) 和 section(配置段) 的 Draft 7 JSON Schema(JSON 结构定义)。
  • handle_config_command::<Cli, S>(command, config_path) 会处理内置的 clap(命令行解析库) 配置命令。

不使用 confique 时,可以直接使用 load_config_tree

快速开始

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

先添加 crate(软件包),再添加应用所需的 schema(结构定义) 库和运行时库:

[dependencies]
rust-config-tree = "0.1"
confique = { version = "0.4", features = ["yaml", "toml", "json5"] }
figment = { version = "0.10", features = ["yaml", "toml", "json", "env"] }
schemars = { version = "1", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] }

然后定义 confique schema(结构定义),并为 root(根配置) 类型实现 ConfigSchema

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

use confique::Config;
use rust_config_tree::ConfigSchema;

#[derive(Debug, Config)]
struct AppConfig {
    #[config(default = [])]
    include: Vec<PathBuf>,

    #[config(nested)]
    server: ServerConfig,
}

#[derive(Debug, Config)]
struct ServerConfig {
    #[config(default = "127.0.0.1")]
    #[config(env = "APP_SERVER_BIND")]
    bind: String,

    #[config(default = 8080)]
    #[config(env = "APP_SERVER_PORT")]
    port: u16,
}

impl ConfigSchema for AppConfig {
    fn include_paths(layer: &<Self as Config>::Layer) -> Vec<PathBuf> {
        layer.include.clone().unwrap_or_default()
    }
}
}

加载配置:

#![allow(unused)]
fn main() {
use rust_config_tree::load_config;

let config = load_config::<AppConfig>("config.yaml")?;
println!("{config:#?}");
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

root(根配置) 文件可以通过 include(包含) 递归加载子配置:

# config.yaml
include:
  - config/server.yaml
# config/server.yaml
server:
  bind: 0.0.0.0
  port: 3000

默认情况下,load_config 使用以下优先级:

环境变量
  > 配置文件,后合并的文件会覆盖先合并的文件
    > confique 代码默认值

通过高层 API(应用程序接口) 加载 include(包含文件) 时,root(根配置) 文件拥有 最高的文件优先级。被 include(包含) 的文件优先级更低,适合承载默认值,也适合 承载按 section(配置段) 拆分的配置。

命令行参数属于应用自己的 CLI(命令行接口) 语义,所以 load_config 不会自动读取。 当应用需要用命令行参数覆盖配置时,应用应在 build_config_figment 之后合并 CLI override(命令行覆盖值)。合并方式如下:

CLI flag(命令行参数) 名称由应用自己决定,加载器不会自动使用 a.b.c 这种 配置路径。推荐使用正常的 clap(命令行解析库) 参数名,比如 --server-port, 再把参数值映射成嵌套 override(覆盖值) 结构。序列化后的嵌套结构真正决定 哪个配置 key(键) 会被覆盖。

只有应用放进 CliOverrides provider(值提供器) 的值才会覆盖配置。这个机制 适合单次运行时频繁调整参数、但不想修改配置文件的场景。稳定值应继续保存在 配置文件中。

#![allow(unused)]
fn main() {
use figment::providers::Serialized;
use serde::Serialize;
use rust_config_tree::{build_config_figment, load_config_from_figment};

#[derive(Debug, Serialize)]
struct CliOverrides {
    #[serde(skip_serializing_if = "Option::is_none")]
    server: Option<CliServerOverrides>,
}

#[derive(Debug, Serialize)]
struct CliServerOverrides {
    #[serde(skip_serializing_if = "Option::is_none")]
    port: Option<u16>,
}

let cli_overrides = CliOverrides {
    server: Some(CliServerOverrides { port: Some(9000) }),
};

let figment = build_config_figment::<AppConfig>("config.yaml")?
    .merge(Serialized::defaults(cli_overrides));

let config = load_config_from_figment::<AppConfig>(&figment)?;
let _ = config;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

这样合并 CLI override(命令行覆盖值) 后,完整优先级如下:

命令行覆盖值
  > 环境变量
    > 配置文件
      > confique 代码默认值

配置结构

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

应用的 schema(结构定义) 是普通的 confique config(配置) 类型。 root schema(根结构定义) 必须实现 ConfigSchema,这样 rust-config-tree 才能从中间 confique layer(层) 中发现递归 include(包含文件)。

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

use confique::Config;
use schemars::JsonSchema;
use rust_config_tree::ConfigSchema;

#[derive(Debug, Config, JsonSchema)]
struct AppConfig {
    #[config(default = [])]
    include: Vec<PathBuf>,

    #[config(nested)]
    #[schemars(extend("x-tree-split" = true))]
    database: DatabaseConfig,
}

#[derive(Debug, Config, JsonSchema)]
struct DatabaseConfig {
    #[config(env = "APP_DATABASE_URL")]
    url: String,

    #[config(default = 16)]
    #[config(env = "APP_DATABASE_POOL_SIZE")]
    pool_size: u32,
}

impl ConfigSchema for AppConfig {
    fn include_paths(layer: &<Self as Config>::Layer) -> Vec<PathBuf> {
        layer.include.clone().unwrap_or_default()
    }
}
}

Include(包含) 字段

include(包含) 字段可以使用任意名称。rust-config-tree 只通过 ConfigSchema::include_paths 读取这个字段。

这个字段通常应有空默认值:

#![allow(unused)]
fn main() {
#[config(default = [])]
include: Vec<PathBuf>,
}

加载器会接收每个文件的部分 layer(层)。这样加载器可以在最终 schema(结构定义) 合并和校验之前发现子配置文件。

嵌套 Section(配置段)

使用 #[config(nested)] 表示结构化 section(配置段)。嵌套 section(配置段) 一定会影响运行时加载。如果某个 nested(嵌套) 字段还需要生成独立的 *.yaml 模板和 <section>.schema.json schema(结构定义),就给这个字段加上 #[schemars(extend("x-tree-split" = true))]

#![allow(unused)]
fn main() {
#[derive(Debug, Config, JsonSchema)]
struct AppConfig {
    #[config(nested)]
    #[schemars(extend("x-tree-split" = true))]
    server: ServerConfig,
}
}

对应的自然 YAML 形状为:

server:
  bind: 127.0.0.1
  port: 8080

环境变量专用字段

当 leaf(叶子) 字段只能由环境变量提供,并且不应该出现在生成的配置文件中时, 可以使用 #[schemars(extend("x-env-only" = true))]。生成的 YAML 模板和 JSON Schema(JSON 结构定义) 会省略 env-only(仅环境变量) 字段;如果父对象因此变空, 生成器也会删除这个父对象。

#![allow(unused)]
fn main() {
#[config(env = "APP_SECRET")]
#[schemars(extend("x-env-only" = true))]
secret: String,
}

字段值合法性校验

生成的 *.schema.json 文件只用于 IDE(集成开发环境) 补全和基础编辑期检查, 不负责判断具体字段值对应用是否合法。

字段值合法性应在代码中通过 #[config(validate = Self::validate)] 实现。 当 load_config 加载最终配置,或者 config-validate 检查最终配置时, 运行时会执行这个校验。

模板 Section(配置段) 路径覆盖

当模板 source(来源) 没有 include(包含文件) 时,crate(软件包) 可以从带 x-tree-split 标记的嵌套 schema section(结构定义配置段) 推导子模板文件。 默认顶层路径是相对 root template(根模板) 目录的 <section>.yaml

使用 template_path_for_section 覆盖路径:

#![allow(unused)]
fn main() {
impl ConfigSchema for AppConfig {
    fn include_paths(layer: &<Self as Config>::Layer) -> Vec<PathBuf> {
        layer.include.clone().unwrap_or_default()
    }

    fn template_path_for_section(section_path: &[&str]) -> Option<PathBuf> {
        match section_path {
            ["database"] => Some(PathBuf::from("examples/database.yaml")),
            _ => None,
        }
    }
}
}

运行时加载

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

运行时加载会把职责分别交给 Figment(配置合并库) 和 confique(配置结构定义库):

Figment(配置合并库):
  运行时文件加载
  运行时环境变量加载
  运行时来源元数据

confique(配置结构定义库):
  结构定义元数据
  默认值
  校验
  配置模板

主要 API(应用程序接口) 如下:

#![allow(unused)]
fn main() {
use rust_config_tree::load_config;

let config = load_config::<AppConfig>("config.yaml")?;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

当应用需要来源 metadata(元数据) 时,可以使用 load_config_with_figment

#![allow(unused)]
fn main() {
use rust_config_tree::load_config_with_figment;

let (config, figment) = load_config_with_figment::<AppConfig>("config.yaml")?;
let _ = (config, figment);
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

加载步骤

高层加载器会按以下步骤执行:

  1. 加载器会对 root config(根配置) 路径做词法解析。
  2. 加载器会从 root config(根配置) 所在目录开始向上查找,并加载第一个 .env 文件。
  3. 加载器会将每个配置文件加载成部分 layer(层),用于发现 include(包含文件)。
  4. 加载器会从发现的配置文件构建 Figment(配置合并库) graph(配置图)。
  5. 加载器会以高于文件的优先级合并 ConfiqueEnvProvider
  6. 应用可以选择合并自己的 CLI override(命令行覆盖值)。
  7. 加载器会从 Figment(配置合并库) 提取 confique layer(层)。
  8. 应用 confique 代码默认值。
  9. 加载器会校验并构造最终 schema(结构定义)。

load_configload_config_with_figment 执行第 1-5 步和第 7-9 步。 第 6 步属于应用语义,因为这个 crate(软件包) 无法推断某个 CLI flag(命令行参数) 应该映射到哪个 schema(结构定义) 字段。

文件格式

运行时文件 provider(值提供器) 会根据配置路径的扩展名选择文件格式:

  • .yaml.yml 使用 YAML。
  • .toml 使用 TOML。
  • .json.json5 使用 JSON。
  • 未知或缺失扩展名使用 YAML。

模板生成仍使用 confique(配置结构定义库) 的 YAML、TOML 和 JSON5-compatible(JSON5 兼容) 模板渲染器。

Include(包含) 优先级

高层加载器合并文件 provider(值提供器) 时,被 include(包含) 的文件优先级低于 include(包含) 它的文件。root config(根配置) 文件拥有最高的文件优先级。

环境变量优先级高于所有配置文件。confique 默认值只用于运行时 provider(值提供器) 没有提供的值。

当 CLI override(命令行覆盖值) 在 build_config_figment 之后合并时,完整优先级如下:

命令行覆盖值
  > 环境变量
    > 配置文件
      > confique 代码默认值

命令行语法不是由 rust-config-tree 定义的。只要应用把 --server-port 解析出的值映射进嵌套 serialized provider(序列化值提供器),这个值就可以覆盖 server.port--server.porta.b.c 这种点分路径语法只有在应用自己实现时才存在。

因此 CLI(命令行接口) 优先级只作用于应用 override provider(覆盖值提供器) 中存在的 key(键)。它适合临时、频繁调整的运行参数。长期稳定配置应留在 配置文件里。

#![allow(unused)]
fn main() {
use figment::providers::Serialized;
use serde::Serialize;
use rust_config_tree::{build_config_figment, load_config_from_figment};

#[derive(Debug, Serialize)]
struct CliOverrides {
    #[serde(skip_serializing_if = "Option::is_none")]
    server: Option<CliServerOverrides>,
}

#[derive(Debug, Serialize)]
struct CliServerOverrides {
    #[serde(skip_serializing_if = "Option::is_none")]
    port: Option<u16>,
}

let cli_overrides = CliOverrides {
    server: Some(CliServerOverrides { port: Some(9000) }),
};

let figment = build_config_figment::<AppConfig>("config.yaml")?
    .merge(Serialized::defaults(cli_overrides));

let config = load_config_from_figment::<AppConfig>(&figment)?;
let _ = config;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

环境变量

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

环境变量名通过 confique 的 schema(结构定义) 声明:

#![allow(unused)]
fn main() {
#[derive(Debug, Config)]
struct DatabaseConfig {
    #[config(env = "APP_DATABASE_URL")]
    url: String,

    #[config(default = 16)]
    #[config(env = "APP_DATABASE_POOL_SIZE")]
    pool_size: u32,
}
}

rust-config-tree 会从 confique::Config::META 读取这些名称,并构建 Figment(配置合并库) provider(值提供器),再把每个环境变量映射到精确字段路径。

不要在这个 crate(软件包) 的 schema(结构定义) 中使用基于分隔符的 Figment(配置合并库) 环境变量映射:

#![allow(unused)]
fn main() {
// 不要在 rust-config-tree 的结构定义中使用这种模式。
Env::prefixed("APP_").split("_")
Env::prefixed("APP_").split("__")
}

split("_") 会把下划线当成嵌套 key(键) 分隔符。这样 APP_DATABASE_POOL_SIZE 会变成类似 database.pool.size 的路径,与 pool_size 这种 Rust 字段名冲突。

使用 ConfiqueEnvProvider 时,映射是显式的:

APP_DATABASE_POOL_SIZE -> database.pool_size

单个下划线仍然只是环境变量名的一部分。Figment(配置合并库) 不会猜测嵌套规则。

Dotenv 加载

在运行时 provider(值提供器) 求值之前,加载器会从 root config(根配置) 文件所在目录开始向上查找 .env 文件。

已有的进程环境变量会被保留。.env 中的值只填充缺失的环境变量。

示例:

APP_SERVER_PORT=9000
APP_DATABASE_POOL_SIZE=64

当 schema(结构定义) 声明了匹配的 #[config(env = "...")] 属性时,这些变量会覆盖 配置文件中的值。

值解析

桥接 provider(值提供器) 会让 Figment(配置合并库) 解析环境变量值。它不会调用 confiqueparse_env hook(钩子函数)。复杂值应优先放在配置文件中; 只有当 Figment(配置合并库) 的环境变量值语法很适合目标类型时,才适合把复杂值 放进环境变量。

来源追踪

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

使用 load_config_with_figment 可以保留运行时加载使用的 Figment(配置合并库) graph(配置图):

#![allow(unused)]
fn main() {
use rust_config_tree::load_config_with_figment;

let (config, figment) = load_config_with_figment::<AppConfig>("config.yaml")?;
let _ = config;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

返回的 Figment(配置合并库) 值可以查询运行时值来源:

#![allow(unused)]
fn main() {
if let Some(metadata) = figment.find_metadata("database.pool_size") {
    let source = metadata.interpolate(
        &figment::Profile::Default,
        &["database", "pool_size"],
    );

    println!("database.pool_size came from {source}");
}
}

对于 ConfiqueEnvProvider 提供的值,interpolation(插值结果) 会返回 schema(结构定义) 中声明的原始环境变量名:

database.pool_size came from APP_DATABASE_POOL_SIZE

TRACE 事件

加载器使用 tracing::trace! 输出来源追踪事件。只有 TRACE(追踪级别) 启用时, 加载器才会输出这些事件:

#![allow(unused)]
fn main() {
use rust_config_tree::{load_config_with_figment, trace_config_sources};

let (config, figment) = load_config_with_figment::<AppConfig>("config.yaml")?;

// 如果配置加载完成后才初始化 tracing subscriber(追踪订阅器),
// 可以在安装订阅器后再次输出相同的来源事件。
trace_config_sources::<AppConfig>(&figment);
let _ = config;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

每个事件都会使用 rust_config_tree::config target(目标),并包含以下字段:

  • config_key 表示用点分隔的配置 key(键)。
  • source 表示渲染后的来源 metadata(元数据)。

如果某个字段只来自 confique 默认值,它就没有 Figment(配置合并库) 运行时 metadata(元数据),并会显示为 confique default or unset optional field

模板生成

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

模板由运行时使用的同一个 confique schema(结构定义) 生成。confique 负责 渲染实际模板内容,包括文档注释、默认值、必填字段和声明的环境变量名。

使用 write_config_templates

#![allow(unused)]
fn main() {
use rust_config_tree::write_config_templates;

write_config_templates::<AppConfig>("config.yaml", "config.example.yaml")?;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

下面的调用会为 root config(根配置) 和显式拆分的嵌套 section(配置段) 生成 Draft 7 JSON Schema(JSON 结构定义):

#![allow(unused)]
fn main() {
use rust_config_tree::write_config_schemas;

write_config_schemas::<AppConfig>("schemas/myapp.schema.json")?;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

如果 nested(嵌套) 字段需要独立生成 *.yaml 模板和 <section>.schema.json schema(结构定义),就使用 #[schemars(extend("x-tree-split" = true))] 标记这个字段。没有这个标记的 nested(嵌套) 字段会留在父模板和父 schema(结构定义) 中。

当某个 leaf(叶子) 字段只能从环境变量提供时,可以添加 #[schemars(extend("x-env-only" = true))]。生成的模板和 JSON Schema(JSON 结构定义) 会省略 env-only(仅环境变量) 字段;如果父对象因此变空, 生成器也会删除这个父对象。

生成的 schema(结构定义) 会移除 required 约束。IDE(集成开发环境) 仍然可以补全, 但是 log.yaml 这类局部文件不会因为缺少 root(根配置) 字段而报错。 root schema(根结构定义) 只补全 root(根配置) 文件里应该写的字段;被拆分的 section(配置段) 字段会从 root schema(根结构定义) 中省略,并只由各自的 section schema(配置段结构定义) 补全。 已经出现的字段仍可由 IDE(集成开发环境) 做基础编辑期检查,例如生成的 schema(结构定义) 支持的类型、枚举和未知属性检查。生成的 *.schema.json 不负责判断具体字段值对应用是否合法。字段值合法性应在代码中通过 #[config(validate = Self::validate)] 实现; load_configconfig-validate 会执行这类运行时校验。

生成 TOML、YAML、JSON 和 JSON5 模板时,可以绑定这些 schema(结构定义):

#![allow(unused)]
fn main() {
use rust_config_tree::write_config_templates_with_schema;

write_config_templates_with_schema::<AppConfig>(
    "config.toml",
    "config.example.toml",
    "schemas/myapp.schema.json",
)?;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

root(根配置) 模板会绑定 root schema(根结构定义),并且不会补全被拆分的 child section(子配置段) 字段。拆分出的 section(配置段) YAML 模板会绑定对应的 section schema(配置段结构定义)。JSON 和 JSON5 模板会写入顶层 $schema 字段。 VS Code(代码编辑器) json.schemas 等编辑器设置仍可作为替代绑定方式。

输出格式由输出路径推断:

  • .yaml.yml 生成 YAML。
  • .toml 生成 TOML。
  • .json.json5 会生成 JSON5-compatible(JSON5 兼容) 模板。
  • 未知或缺失扩展名生成 YAML。

模板 API 会严格写入调用方传入的 output_path。内置的 config-template CLI(命令行接口) 命令会把生成的模板归档到 config/<root_config_name>/; 未传 --output 时,AppConfig 会写入 config/app_config/app_config.example.yaml,对应的默认 schema(结构定义) 写入 config/app_config/app_config.schema.json

Schema(结构定义) 绑定

当 schema path(结构定义路径) 是 schemas/myapp.schema.json 时,生成的 root(根配置) 模板会使用以下内容:

#:schema ./schemas/myapp.schema.json
# yaml-language-server: $schema=./schemas/myapp.schema.json

生成的 section(配置段) 模板会绑定 section schema(配置段结构定义):

# log.yaml
# yaml-language-server: $schema=./schemas/log.schema.json

生成的 JSON 和 JSON5 模板会用顶层 $schema 字段绑定 schema(结构定义):

{
  "$schema": "./schemas/myapp.schema.json"
}

如果项目不想在文件内写绑定,也可以通过编辑器设置绑定:

{
  "json.schemas": [
    {
      "fileMatch": [
        "/config.json",
        "/config.*.json"
      ],
      "url": "./schemas/myapp.schema.json"
    }
  ]
}

模板 Source(来源) 选择

模板生成会按以下顺序选择 source tree(来源树):

  1. 它会先使用已存在的 config path(配置路径)。
  2. 它会再使用已存在的 output template path(输出模板路径)。
  3. 它最后会把 output path(输出路径) 当作新的空 template tree(模板树)。

这样项目可以从当前配置更新模板,也可以更新已有模板集,还可以只从 schema(结构定义) 创建新的模板集。

镜像 Include Tree(包含树)

如果 source(来源) 文件声明了 include(包含文件),生成的模板会在 output(输出) 目录下镜像这些 include path(包含路径)。

# config.yaml
include:
  - server.yaml

生成 config.example.yaml 会写入:

config.example.yaml
server.yaml

相对 include(包含) 目标会镜像到 output(输出) 文件父目录下。绝对 include(包含) 目标会保留绝对路径。

显式 Section(配置段) 拆分

当 source(来源) 文件没有 include(包含文件) 时,crate(软件包) 可以从带 x-tree-split 标记的嵌套 schema section(结构定义配置段) 推导 include(包含) 目标。对于包含已标记 server section(配置段) 的 schema(结构定义),空 root template source(根模板来源) 可以生成以下文件:

config.example.yaml
server.yaml

root template(根模板) 会得到 include block(包含块),server.yaml 只包含 server section(配置段)。没有标记的 nested section(嵌套配置段) 会内联 保留在父模板中;更深层 section(配置段) 只有同样带 x-tree-split 时才会继续拆分。

IDE(集成开发环境) 补全

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

生成的 JSON Schema(JSON 结构定义) 可以给 TOML、YAML、JSON 和 JSON5 配置文件使用。 这个 schema(结构定义) 从 confique 使用的同一个 Rust(系统编程语言) 类型生成:

#![allow(unused)]
fn main() {
use confique::Config;
use schemars::JsonSchema;

#[derive(Debug, Config, JsonSchema)]
struct AppConfig {
    #[config(nested)]
    #[schemars(extend("x-tree-split" = true))]
    server: ServerConfig,
}
}

生成方式:

#![allow(unused)]
fn main() {
use rust_config_tree::write_config_schemas;

write_config_schemas::<AppConfig>("schemas/myapp.schema.json")?;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

这会写入 root schema(根结构定义) 和 schemas/server.schema.json 这类 section schema(配置段结构定义)。生成的 schema(结构定义) 会移除 required 约束, 所以局部配置文件仍有补全,并且不会出现缺字段诊断。 root schema(根结构定义) 会省略被拆分的 nested section(嵌套配置段) 属性,所以 child section(子配置段) 的补全只会出现在绑定对应 section schema(配置段结构定义) 的文件里。没有标记的 nested section(嵌套配置段) 会保留在 root schema(根结构定义) 中。

x-env-only 标记的字段会从生成的 schema(结构定义) 中省略,因此 IDE(集成开发环境) 不会补全必须只来自环境变量的 secret(秘密值) 或其他值。

IDE schema(集成开发环境结构定义) 只用于补全和基础编辑期检查,例如生成的 schema(结构定义) 支持的类型、枚举和未知属性检查。它不负责判断具体字段值对应用 是否合法。字段值合法性应在代码中通过 #[config(validate = Self::validate)] 实现,并由 load_configconfig-validate 触发。必填字段和最终合并配置的校验也使用这些运行时路径。

TOML

TOML 文件应在顶部使用 #:schema directive(指令) 绑定 schema(结构定义):

#:schema ./schemas/myapp.schema.json

[server]
bind = "0.0.0.0"
port = 3000

不要使用根字段 $schema = "..."。它会成为真实配置数据,可能影响运行时 反序列化。write_config_templates_with_schema 会为 TOML 模板自动添加 #:schema directive(指令)。

YAML

YAML 文件使用 YAML Language Server(YAML 语言服务器) modeline(模式声明行):

# yaml-language-server: $schema=./schemas/myapp.schema.json

server:
  bind: 0.0.0.0
  port: 3000

write_config_templates_with_schema 会为 YAML 模板自动添加这个 modeline(模式声明行)。 拆分出的 YAML 模板会绑定对应 section schema(配置段结构定义),例如 log.yaml 绑定 ./schemas/log.schema.json

JSON

JSON 和 JSON5 文件可以用顶层 $schema 字段绑定 schema(结构定义)。 write_config_templates_with_schema 会为生成的 JSON 和 JSON5 模板自动加入这个字段:

{
  "$schema": "./schemas/myapp.schema.json"
}

如果项目不想在文件内写绑定,也可以通过编辑器设置绑定:

{
  "json.schemas": [
    {
      "fileMatch": [
        "/config.json",
        "/config.*.json",
        "/deploy/*.json"
      ],
      "url": "./schemas/myapp.schema.json"
    }
  ]
}

YAML 也可以通过 VS Code settings(代码编辑器设置) 绑定:

{
  "yaml.schemas": {
    "./schemas/myapp.schema.json": [
      "config.yaml",
      "config.*.yaml",
      "deploy/*.yaml"
    ]
  }
}

最终布局如下:

schemas/myapp.schema.json:
  只包含 root(根配置) 文件字段

schemas/server.schema.json:
  server(服务器) 配置段结构定义

config.toml:
  #:schema ./schemas/myapp.schema.json

config.yaml:
  # yaml-language-server: $schema=./schemas/myapp.schema.json

server.yaml:
  # yaml-language-server: $schema=./schemas/server.schema.json

config.json:
  "$schema": "./schemas/myapp.schema.json"

参考:

CLI(命令行接口) 集成

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

ConfigCommand 提供以下可复用的 clap(命令行解析库) 子命令:

  • config-template 会生成配置模板。
  • config-schema 会生成 JSON Schema(JSON 结构定义)。
  • config-validate 会校验最终配置。
  • completions 会输出 completion(补全脚本)。
  • install-completions 会安装 completion(补全脚本)。
  • uninstall-completions 会卸载 completion(补全脚本)。

这些内置子命令不同于应用自己的配置覆盖参数。配置覆盖参数应在运行时加载 路径里作为 Figment(配置合并库) provider(值提供器) 合并。

配置覆盖参数仍属于依赖方应用自己的 CLI(命令行接口)。参数名不需要匹配点分配置路径。 例如,应用可以解析 --server-port,再把它映射到嵌套配置 key(键) server.port。 只有应用映射进 CliOverrides 的 flag(命令行参数) 才会影响配置值。

应用可以把 ConfigCommand flatten(展开) 到自己的命令枚举中:

  1. 保留应用自己的 Parser 类型。
  2. 保留应用自己的 Subcommand enum。
  3. 在这个 enum 里添加 #[command(flatten)] Config(ConfigCommand)
  4. Clap(命令行解析库) 会把 flattened(已展开的) ConfigCommand variants(变体) 展开到应用自己的同一层命令。
  5. 应用在 match 里处理 Config(command) variant(变体),并交给 handle_config_command
use std::path::PathBuf;

use clap::{Parser, Subcommand};
use confique::Config;
use schemars::JsonSchema;
use rust_config_tree::{ConfigCommand, ConfigSchema, handle_config_command, load_config};

#[derive(Debug, Config, JsonSchema)]
struct AppConfig {
    #[config(default = [])]
    include: Vec<PathBuf>,
}

impl ConfigSchema for AppConfig {
    fn include_paths(layer: &<Self as Config>::Layer) -> Vec<PathBuf> {
        layer.include.clone().unwrap_or_default()
    }
}

#[derive(Debug, Parser)]
#[command(name = "demo")]
struct Cli {
    #[arg(long, default_value = "config.yaml")]
    config: PathBuf,

    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    Run,

    #[command(flatten)]
    Config(ConfigCommand),
}

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let cli = Cli::parse();

    match cli.command {
        Command::Run => {
            let config = load_config::<AppConfig>(&cli.config)?;
            println!("{config:#?}");
        }
        Command::Config(command) => {
            handle_config_command::<Cli, AppConfig>(command, &cli.config)?;
        }
    }

    Ok(())
}

配置模板

demo config-template

命令会在 config/<root_config_name>/ 下写入模板。如果 --output 接收到 路径,命令只使用其中的文件名。未提供 output file name(输出文件名) 时,命令写入 config/<root_config_name>/<root_config_name>.example.yaml。添加 --schema schemas/myapp.schema.json 后,生成的 TOML、YAML、JSON 和 JSON5 模板会绑定生成的 JSON Schema(JSON 结构定义)。拆分出的 YAML 模板会绑定对应的 section schema(配置段结构定义)。该命令也会把 root(根配置) 和 section schema(配置段结构定义) 写入指定的 schema path(结构定义路径)。

demo config-template --output app_config.example.toml --schema schemas/myapp.schema.json

下面的命令会生成 root(根配置) 和 section(配置段) 的 JSON Schema(JSON 结构定义):

demo config-schema

未提供 --output 时,config-schema 会把 root schema(根结构定义) 写入 config/<root_config_name>/<root_config_name>.schema.json

下面的命令会校验完整的 runtime config tree(运行时配置树):

demo config-validate

生成的 editor schema(编辑器结构定义) 会刻意避免在拆分文件里触发必填字段诊断。 config-validate 会加载 includes(包含文件)、应用默认值,并执行最终 confique 校验, 包括通过 #[config(validate = Self::validate)] 声明的校验。生成的 *.schema.json 仍然只用于 IDE(集成开发环境) 补全和基础编辑期检查,不负责字段值合法性判断。 校验成功时会输出 Configuration is ok

Shell Completions(命令行补全)

下面的命令会把 completions(补全脚本) 输出到 stdout(标准输出):

demo completions zsh

下面的命令会安装 completions(补全脚本):

demo install-completions zsh

下面的命令会卸载 completions(补全脚本):

demo uninstall-completions zsh

安装器支持 Bash、Elvish、Fish、PowerShell 和 Zsh。它会将 completion(补全脚本) 文件写入用户 home(主目录) 目录,并为需要显式配置的 shell(命令行外壳) 更新启动文件。

在修改已有 shell(命令行外壳) 启动文件之前,例如 ~/.zshrc~/.bashrc、 Elvish rc(运行控制) 文件或 PowerShell profile(配置文件),命令会先在原文件旁边写入备份:

<rc-file>.backup.by.<program-name>.<timestamp>

示例

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

仓库包含可运行示例。这些示例覆盖 config tree(配置树) 加载、 CLI(命令行接口) 覆盖参数、内置配置命令、模板生成和低层 tree API(树形接口)。

可以阅读仓库 examples(示例目录) 索引:

在仓库根目录运行示例:

cargo run --example basic_loading
cargo run --example cli_overrides -- --server-port 9000
cargo run --example config_commands -- config-template
cargo run --example config_commands -- config-schema
cargo run --example config_commands -- config-validate
cargo run --example generate_templates
cargo run --example tree_api

config_commands 的 template(模板) 和 schema(结构定义) 命令使用 CLI 默认路径, 因此 AppConfig 会把生成文件写到 config/app_config/ 下。

Tree API(树形接口)

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

当应用不使用 confique,或者需要直接访问遍历结果时,应用可以使用低层 tree API(树形接口)。

#![allow(unused)]
fn main() {
use std::{
    fs,
    io,
    path::{Path, PathBuf},
};

use rust_config_tree::{ConfigSource, load_config_tree};

fn load_source(path: &Path) -> io::Result<ConfigSource<String>> {
    let content = fs::read_to_string(path)?;
    let includes = content
        .lines()
        .filter_map(|line| line.strip_prefix("include: "))
        .map(PathBuf::from)
        .collect();

    Ok(ConfigSource::new(content, includes))
}

let tree = load_config_tree("config.yaml", load_source)?;

for node in tree.nodes() {
    println!("{}", node.path().display());
}
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

遍历规则

tree loader(树形加载器) 会执行以下操作:

  • 它会对 source path(来源路径) 做词法归一化。
  • 它会拒绝空 include path(包含路径)。
  • 它会从声明文件解析相对 include(包含)。
  • 它会保留绝对 include path(包含路径)。
  • 它会检测递归 include(包含) 循环。
  • 它会跳过已经从其他 include(包含) 分支加载过的文件。

ConfigTreeOptions 可以反转同级 include(包含) 的遍历顺序:

#![allow(unused)]
fn main() {
use rust_config_tree::{ConfigTreeOptions, IncludeOrder};

let options = ConfigTreeOptions::default().include_order(IncludeOrder::Reverse);
let _ = options;
}

路径辅助函数

路径辅助函数只做词法处理。它们不解析符号链接,也不要求路径存在:

  • absolutize_lexical(path) 会把路径转换成词法绝对路径。
  • normalize_lexical(path) 会对路径做词法归一化。
  • resolve_include_path(parent_path, include_path) 会根据父路径解析 include(包含) 路径。

GitHub Pages(静态站点托管)

English | 中文 | 日本語 | 한국어 | Français | Deutsch | Español | Português | Svenska | Suomi | Nederlands

本仓库使用 mdBook(文档构建工具) 和 GitHub Pages(静态站点托管) 发布手册。

每种语言的手册都是独立的 mdBook(文档构建工具) 项目。每种语言都有自己的 SUMMARY.md,因此左侧目录只显示当前语言的页面:

manual/
  en/
    book.toml
    SUMMARY.md
    introduction.md
    quick-start.md
    ...
  zh/
    book.toml
    SUMMARY.md
    introduction.md
    quick-start.md
    ...
  ja/
    book.toml
    SUMMARY.md
    introduction.md
    quick-start.md
    ...
  ko/
  fr/
  de/
  es/
  pt/
  sv/
  fi/
  nl/

本地构建:

scripts/publish-pages.sh

生成站点写入:

target/mdbook

发布 Workflow(工作流)

.github/workflows/pages.yml 中的 workflow(工作流) 会在 push(推送) 到 main 时运行,也支持手动触发。它会执行以下步骤:

  1. 它会 checkout(检出) 仓库。
  2. 它会安装 mdBook(文档构建工具)。
  3. 运行 scripts/publish-pages.sh
  4. 它会将 target/mdbook 上传为 Pages artifact(页面产物)。
  5. 它会将 artifact(产物) 部署到 GitHub Pages(静态站点托管)。

发布 URL:

https://developerworks.github.io/rust-config-tree/

Crate 发布

下面的命令会执行完整的提交、推送、Pages(静态页面) 部署和 crate(软件包) 发布流程:

scripts/release.sh --execute --message "Release 0.1.3"

在仓库根目录可以使用 crate(软件包) 发布辅助脚本:

scripts/publish-crate.sh

默认模式会运行检查和 cargo publish --dry-run。如果当前版本已经存在于 crates.io,脚本会自动 bump patch(递增补丁版本号)。检查通过后,可以发布到 crates.io:

scripts/publish-crate.sh --execute

脚本用法汇总在 scripts/README.md