← 中文资源

简体中文版本

分层合并 TOML 配置

递归合并 TOML 表,让环境覆盖层只修改声明的键,同时保留同级未覆盖配置,并输出合并后的文档。

阅读英文原文

功能说明

配置文件常按环境分层:基础文件提供默认值,开发、预发和生产文件只覆盖差异。整文件替换会丢失基础层中未被覆盖的键,深度合并则逐表递归,遇到两边都是表就继续向下,遇到标量、数组或其他类型就由覆盖层替换。这样生产环境只改端口和 TLS 开关时,主机名等设置仍来自基础配置。

本例以 toml::Value 为统一表示,解析两段内嵌 TOML 文本,合并后打印。使用 Value 而不是强类型结构体,便于在编译期未知键集合的配置中心、插件配置和多租户场景中动态合并。

关键代码

merge 对基准值取可变引用,对覆盖值取共享引用,避免复制整棵树。match 同时解构两侧:只有双方都是 Table 时才遍历覆盖层的键,目标中已有同键就递归,没有就克隆插入。其余组合落到兜底分支,直接把覆盖值克隆到基准槽位。main 的 parse 依赖左侧类型标注推断为 toml::Value,错误通过问号向上返回。

使用注意

合并会丢失 TOML 注释和原始排版,若需要保留注释应使用支持格式保留的编辑器。数组默认整体替换,不会按索引逐项合并;若需要追加语义,应自定义规则并写入文档。时间、整数和浮点的类型差异要保持一致,覆盖层写成字符串会直接替换掉数字。合并顺序决定优先级,应从最低优先级开始依次应用。配置中若含密钥,打印结果可能泄露,生产日志应脱敏。

验证方式

运行脚本,输出的 server.port 为 9090,server.host 仍为 127.0.0.1,server.tls.enabled 变为 true,说明嵌套表被递归合并而标量被覆盖。把覆盖层中的 tls 改成字符串再运行,会看到整个 tls 表被替换,提醒类型必须匹配。

代码示例

use toml::Value;

fn merge(base: &mut Value, overlay: &Value) {
    match (base, overlay) {
        (Value::Table(base_map), Value::Table(overlay_map)) => {
            for (key, value) in overlay_map {
                match base_map.get_mut(key) {
                    Some(existing) => merge(existing, value),
                    None => {
                        base_map.insert(key.clone(), value.clone());
                    }
                }
            }
        }
        (base_slot, _) => {
            *base_slot = overlay.clone();
        }
    }
}

fn main() -> Result<(), toml::de::Error> {
    let base: Value = r#"
[server]
port = 8080
host = "127.0.0.1"

[server.tls]
enabled = false
"#
    .parse()?;

    let overlay: Value = r#"
[server]
port = 9090

[server.tls]
enabled = true
"#
    .parse()?;

    let mut merged = base;
    merge(&mut merged, &overlay);
    let rendered = toml::to_string(&merged).expect("serialize merged config");
    println!("{rendered}");
    Ok(())
}