|
1 | 1 | # rust-config-tree |
2 | 2 |
|
| 3 | +[English](#rust-config-tree) | [中文](#中文) |
| 4 | + |
3 | 5 | `rust-config-tree` provides configuration-tree loading and CLI helpers for Rust |
4 | 6 | applications that use layered config files. |
5 | 7 |
|
@@ -252,6 +254,16 @@ Flatten `ConfigCommand` into your existing clap command enum to add: |
252 | 254 | - `completions` |
253 | 255 | - `install-completions` |
254 | 256 |
|
| 257 | +The consuming application keeps its own `Parser` type and its own command enum. |
| 258 | +`rust-config-tree` only contributes reusable subcommands: |
| 259 | + |
| 260 | +1. Add `#[command(subcommand)] command: Command` to the application's parser. |
| 261 | +2. Add `#[command(flatten)] Config(ConfigCommand)` to the application's |
| 262 | + `Subcommand` enum. |
| 263 | +3. Clap expands the flattened variants into the same subcommand level as the |
| 264 | + application's own commands. |
| 265 | +4. Match that variant and call `handle_config_command::<Cli, AppConfig>`. |
| 266 | + |
255 | 267 | ```rust |
256 | 268 | use std::path::PathBuf; |
257 | 269 |
|
@@ -359,3 +371,300 @@ Licensed under either of: |
359 | 371 | - MIT license |
360 | 372 |
|
361 | 373 | at your option. |
| 374 | + |
| 375 | +## 中文 |
| 376 | + |
| 377 | +`rust-config-tree` 为使用分层配置文件的 Rust 应用提供配置树加载能力和 CLI |
| 378 | +辅助能力。 |
| 379 | + |
| 380 | +项目手册:<https://developerworks.github.io/rust-config-tree/>。英文手册和 |
| 381 | +中文手册作为独立的 mdBook 站点发布,并提供语言切换链接。 |
| 382 | + |
| 383 | +它提供: |
| 384 | + |
| 385 | +- 通过 Figment runtime provider 将 `confique` schema 加载成可直接使用的 |
| 386 | + config 对象 |
| 387 | +- `config-template`、`completions` 和 `install-completions` 命令处理 |
| 388 | +- YAML、TOML、JSON 和 JSON5 配置模板生成 |
| 389 | +- 递归 include 遍历 |
| 390 | +- 合并环境变量前加载 `.env` |
| 391 | +- 通过 Figment metadata 追踪配置来源 |
| 392 | +- 通过 `tracing` 输出 TRACE 级别来源追踪日志 |
| 393 | +- 相对 include 路径从声明它的文件解析 |
| 394 | +- 词法路径归一化 |
| 395 | +- include 循环检测 |
| 396 | +- 确定性遍历顺序 |
| 397 | +- 镜像模板目标收集 |
| 398 | +- 按嵌套 schema section 自动拆分 YAML 模板 |
| 399 | + |
| 400 | +应用通过派生 `confique::Config` 并实现 `ConfigSchema` 来提供自己的 schema。 |
| 401 | +`ConfigSchema` 用于暴露 schema 中的 include 字段。 |
| 402 | + |
| 403 | +### 安装 |
| 404 | + |
| 405 | +```toml |
| 406 | +[dependencies] |
| 407 | +rust-config-tree = "0.1" |
| 408 | +confique = { version = "0.4", features = ["yaml", "toml", "json5"] } |
| 409 | +figment = { version = "0.10", features = ["yaml", "env"] } |
| 410 | +serde = { version = "1", features = ["derive"] } |
| 411 | +clap = { version = "4", features = ["derive"] } |
| 412 | +``` |
| 413 | + |
| 414 | +### 配置结构 |
| 415 | + |
| 416 | +应用自己的 schema 持有 include 字段。`rust-config-tree` 只需要一个很小的 |
| 417 | +adapter,用来从中间 `confique` layer 提取 include。 |
| 418 | + |
| 419 | +```rust |
| 420 | +use std::path::PathBuf; |
| 421 | +
|
| 422 | +use confique::Config; |
| 423 | +use rust_config_tree::ConfigSchema; |
| 424 | +
|
| 425 | +#[derive(Debug, Config)] |
| 426 | +struct AppConfig { |
| 427 | + #[config(default = [])] |
| 428 | + include: Vec<PathBuf>, |
| 429 | +
|
| 430 | + #[config(default = "paper")] |
| 431 | + mode: String, |
| 432 | +
|
| 433 | + #[config(nested)] |
| 434 | + server: ServerConfig, |
| 435 | +} |
| 436 | +
|
| 437 | +#[derive(Debug, Config)] |
| 438 | +struct ServerConfig { |
| 439 | + #[config(default = 8080)] |
| 440 | + port: u16, |
| 441 | +} |
| 442 | +
|
| 443 | +impl ConfigSchema for AppConfig { |
| 444 | + fn include_paths(layer: &<Self as Config>::Layer) -> Vec<PathBuf> { |
| 445 | + layer.include.clone().unwrap_or_default() |
| 446 | + } |
| 447 | +} |
| 448 | +``` |
| 449 | + |
| 450 | +相对 include 路径从声明它的文件解析: |
| 451 | + |
| 452 | +```yaml |
| 453 | +# config.yaml |
| 454 | +include: |
| 455 | + - config/server.yaml |
| 456 | +
|
| 457 | +mode: shadow |
| 458 | +``` |
| 459 | + |
| 460 | +```yaml |
| 461 | +# config/server.yaml |
| 462 | +server: |
| 463 | + port: 7777 |
| 464 | +``` |
| 465 | + |
| 466 | +使用 `load_config` 加载最终 schema: |
| 467 | + |
| 468 | +```rust |
| 469 | +use rust_config_tree::load_config; |
| 470 | +
|
| 471 | +fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
| 472 | + let config = load_config::<AppConfig>("config.yaml")?; |
| 473 | + println!("{config:#?}"); |
| 474 | +
|
| 475 | + Ok(()) |
| 476 | +} |
| 477 | +``` |
| 478 | + |
| 479 | +`load_config` 会从 root config 所在目录开始向上查找第一个 `.env` 文件并 |
| 480 | +加载,然后让 Figment 读取 schema 中声明的环境变量。进程里已经存在的环境 |
| 481 | +变量会保留,并优先于 `.env` 中的值。 |
| 482 | + |
| 483 | +运行时配置加载由 Figment 完成。`confique` 仍负责 schema metadata、默认值、 |
| 484 | +校验和模板生成。环境变量名从 `#[config(env = "...")]` 读取;loader 不使用 |
| 485 | +`Env::split("_")` 或 `Env::split("__")`,因此 `APP_DATABASE_POOL_SIZE` 可以 |
| 486 | +映射到 `database.pool_size`,不会把单个 `_` 当成层级分隔符。 |
| 487 | + |
| 488 | +`load_config` 不会读取命令行参数,因为 CLI flag 是应用自己的语义。需要 CLI |
| 489 | +覆盖配置时,在 `build_config_figment` 之后合并 provider,再通过 |
| 490 | +`load_config_from_figment` 校验: |
| 491 | + |
| 492 | +```rust |
| 493 | +use figment::providers::Serialized; |
| 494 | +use serde::Serialize; |
| 495 | +use rust_config_tree::{build_config_figment, load_config_from_figment}; |
| 496 | +
|
| 497 | +#[derive(Debug, Serialize)] |
| 498 | +struct CliOverrides { |
| 499 | + #[serde(skip_serializing_if = "Option::is_none")] |
| 500 | + mode: Option<String>, |
| 501 | +} |
| 502 | +
|
| 503 | +fn load_with_cli_overrides() -> Result<AppConfig, Box<dyn std::error::Error + Send + Sync>> { |
| 504 | + let cli_overrides = CliOverrides { |
| 505 | + mode: Some("shadow".to_owned()), |
| 506 | + }; |
| 507 | +
|
| 508 | + let figment = build_config_figment::<AppConfig>("config.yaml")? |
| 509 | + .merge(Serialized::defaults(cli_overrides)); |
| 510 | +
|
| 511 | + let config = load_config_from_figment::<AppConfig>(&figment)?; |
| 512 | + Ok(config) |
| 513 | +} |
| 514 | +``` |
| 515 | + |
| 516 | +这样合并 CLI override 后,运行时优先级为: |
| 517 | + |
| 518 | +```text |
| 519 | +command-line overrides |
| 520 | + > environment variables |
| 521 | + > config files |
| 522 | + > confique code defaults |
| 523 | +``` |
| 524 | + |
| 525 | +### 模板生成 |
| 526 | + |
| 527 | +模板使用同一份 schema 和 include 遍历规则生成。输出格式由输出路径推断: |
| 528 | + |
| 529 | +- `.yaml` 和 `.yml` 生成 YAML |
| 530 | +- `.toml` 生成 TOML |
| 531 | +- `.json` 和 `.json5` 生成 JSON5-compatible 模板 |
| 532 | +- 未知或缺失扩展名生成 YAML |
| 533 | + |
| 534 | +使用 `write_config_templates` 创建 root 模板和 include tree 中的子模板: |
| 535 | + |
| 536 | +```rust |
| 537 | +use rust_config_tree::write_config_templates; |
| 538 | +
|
| 539 | +fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
| 540 | + write_config_templates::<AppConfig>("config.yaml", "config.example.yaml")?; |
| 541 | +
|
| 542 | + Ok(()) |
| 543 | +} |
| 544 | +``` |
| 545 | + |
| 546 | +模板生成按这个顺序选择 source tree: |
| 547 | + |
| 548 | +- 已存在的 config path |
| 549 | +- 已存在的 output template path |
| 550 | +- 将 output path 作为新的空 template tree |
| 551 | + |
| 552 | +### CLI 集成 |
| 553 | + |
| 554 | +依赖 `rust-config-tree` 的项目可以保留自己的 clap parser 和命令枚举。 |
| 555 | +`rust-config-tree` 只提供可复用的 `ConfigCommand` 子命令: |
| 556 | + |
| 557 | +- `config-template` |
| 558 | +- `completions` |
| 559 | +- `install-completions` |
| 560 | + |
| 561 | +合并方式如下: |
| 562 | + |
| 563 | +1. 在应用自己的 `Parser` 类型里保留 `#[command(subcommand)] command: Command`。 |
| 564 | +2. 在应用自己的 `Subcommand` enum 中添加 |
| 565 | + `#[command(flatten)] Config(ConfigCommand)`。 |
| 566 | +3. Clap 会把 flattened variants 展开到应用自己的同一层子命令里。 |
| 567 | +4. 在 `match` 中处理这个 variant,并调用 |
| 568 | + `handle_config_command::<Cli, AppConfig>`。 |
| 569 | + |
| 570 | +```rust |
| 571 | +use std::path::PathBuf; |
| 572 | +
|
| 573 | +use clap::{Parser, Subcommand}; |
| 574 | +use confique::Config; |
| 575 | +use rust_config_tree::{ConfigCommand, ConfigSchema, handle_config_command, load_config}; |
| 576 | +
|
| 577 | +#[derive(Debug, Config)] |
| 578 | +struct AppConfig { |
| 579 | + #[config(default = [])] |
| 580 | + include: Vec<PathBuf>, |
| 581 | + #[config(default = "paper")] |
| 582 | + mode: String, |
| 583 | +} |
| 584 | +
|
| 585 | +impl ConfigSchema for AppConfig { |
| 586 | + fn include_paths(layer: &<Self as Config>::Layer) -> Vec<PathBuf> { |
| 587 | + layer.include.clone().unwrap_or_default() |
| 588 | + } |
| 589 | +} |
| 590 | +
|
| 591 | +#[derive(Debug, Parser)] |
| 592 | +#[command(name = "demo")] |
| 593 | +struct Cli { |
| 594 | + #[arg(long, default_value = "config.yaml")] |
| 595 | + config: PathBuf, |
| 596 | + #[command(subcommand)] |
| 597 | + command: Command, |
| 598 | +} |
| 599 | +
|
| 600 | +#[derive(Debug, Subcommand)] |
| 601 | +enum Command { |
| 602 | + Run, |
| 603 | + #[command(flatten)] |
| 604 | + Config(ConfigCommand), |
| 605 | +} |
| 606 | +
|
| 607 | +fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
| 608 | + let cli = Cli::parse(); |
| 609 | + match cli.command { |
| 610 | + Command::Run => { |
| 611 | + let config = load_config::<AppConfig>(&cli.config)?; |
| 612 | + println!("{config:#?}"); |
| 613 | + } |
| 614 | + Command::Config(command) => { |
| 615 | + handle_config_command::<Cli, AppConfig>(command, &cli.config)?; |
| 616 | + } |
| 617 | + } |
| 618 | +
|
| 619 | + Ok(()) |
| 620 | +} |
| 621 | +``` |
| 622 | + |
| 623 | +`config-template --output <path>` 将模板写入指定路径。未提供 output path 时, |
| 624 | +写入当前目录下的 `config.example.yaml`。 |
| 625 | + |
| 626 | +`completions <shell>` 将 completions 输出到 stdout。 |
| 627 | + |
| 628 | +`install-completions <shell>` 将 completions 写入用户 home 目录,并在 shell |
| 629 | +需要时更新启动文件。支持 Bash、Elvish、Fish、PowerShell 和 Zsh。 |
| 630 | + |
| 631 | +### 低层 Tree API |
| 632 | + |
| 633 | +不使用 `confique`,或者需要直接访问遍历结果时,可以使用 `load_config_tree`: |
| 634 | + |
| 635 | +```rust |
| 636 | +use std::{fs, io, path::{Path, PathBuf}}; |
| 637 | +
|
| 638 | +use rust_config_tree::{ConfigSource, load_config_tree}; |
| 639 | +
|
| 640 | +fn load_source(path: &Path) -> io::Result<ConfigSource<String>> { |
| 641 | + let content = fs::read_to_string(path)?; |
| 642 | + let includes = content |
| 643 | + .lines() |
| 644 | + .filter_map(|line| line.strip_prefix("include: ")) |
| 645 | + .map(PathBuf::from) |
| 646 | + .collect(); |
| 647 | +
|
| 648 | + Ok(ConfigSource::new(content, includes)) |
| 649 | +} |
| 650 | +
|
| 651 | +fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
| 652 | + let tree = load_config_tree("config.yaml", load_source)?; |
| 653 | +
|
| 654 | + for node in tree.nodes() { |
| 655 | + println!("{}", node.path().display()); |
| 656 | + } |
| 657 | +
|
| 658 | + Ok(()) |
| 659 | +} |
| 660 | +``` |
| 661 | + |
| 662 | +Tree API 会进行词法路径归一化、拒绝空 include path、检测递归 include 循环, |
| 663 | +并跳过已经从其他 include 分支加载过的文件。 |
| 664 | + |
| 665 | +### 许可证 |
| 666 | + |
| 667 | +按你的选择使用以下任一许可证: |
| 668 | + |
| 669 | +- Apache License, Version 2.0 |
| 670 | +- MIT license |
0 commit comments