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 Manual

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

This is the English manual for rust-config-tree.

Start with Introduction, Quick Start, or the runnable Examples.

Introduction

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

rust-config-tree provides reusable configuration-tree loading and CLI helpers for Rust applications that use layered config files.

The crate is designed around a small division of responsibilities:

  • confique owns schema definitions, code defaults, validation, and config template generation.
  • figment owns runtime loading and runtime source metadata.
  • rust-config-tree owns recursive include traversal, include path resolution, .env loading, template target discovery, and reusable clap commands.

The crate is useful when an application wants a natural config file layout such as this:

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

log:
  level: info

Each included file can use the same schema shape, and relative include paths are resolved from the file that declared them. The final config is still a normal confique schema value.

Main Features

  • Recursive include traversal with cycle detection.
  • Relative include paths resolved from the declaring file.
  • .env loading before environment providers are evaluated.
  • Schema-declared environment variables without delimiter splitting.
  • Figment metadata for runtime source tracking.
  • TRACE-level source tracking events through tracing.
  • Draft 7 JSON Schema generation for editor completion and basic schema checks.
  • Field value validation in application code through #[config(validate = Self::validate)], executed by load_config or config-validate.
  • YAML, TOML, JSON, and JSON5 template generation.
  • TOML #:schema, YAML Language Server schema modelines, and JSON/JSON5 $schema fields for generated templates.
  • Opt-in YAML template splitting for nested sections marked with x-tree-split.
  • Built-in clap subcommands for config templates, JSON Schema, and shell completions.
  • A lower-level tree API for callers that do not use confique.

Public Entry Points

Use these APIs for most applications:

  • load_config::<S>(path) loads the final schema.
  • load_config_with_figment::<S>(path) loads the schema and returns the Figment graph used for source tracking.
  • write_config_templates::<S>(config_path, output_path) writes the root template and recursively discovered child templates.
  • write_config_schemas::<S>(output_path) writes root and section Draft 7 JSON Schemas.
  • handle_config_command::<Cli, S>(command, config_path) handles built-in clap config commands.

Use load_config_tree when you need the traversal primitive without confique.

Quick Start

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

Add the crate and the schema/runtime libraries used by your application:

[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"] }

Define a confique schema and implement ConfigSchema for the root type:

#![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()
    }
}
}

Load the config:

#![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>>(())
}

Use a root file with recursive includes:

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

The default load_config precedence is:

environment variables
  > config files, with later merged files overriding earlier files
    > confique code defaults

When includes are loaded by the high-level API, the root file has the highest file priority. Included files provide lower-priority values and can be used for defaults or section-specific files.

Command-line arguments are application-specific, so load_config does not read them automatically. Merge CLI overrides after build_config_figment when the application has config override flags:

CLI flag names are chosen by the application. They are not automatically a.b.c config paths. Prefer normal clap flags such as --server-port, then map them into a nested override structure. The nested serialized shape controls the config key that is overridden.

Only values represented in the application’s CliOverrides provider override configuration. This is useful for parameters that are changed frequently for one run without editing the config file. Stable values should stay in config files.

#![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>>(())
}

With CLI overrides merged this way, the full precedence is:

command-line overrides
  > environment variables
    > config files
      > confique code defaults

Configuration Schema

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

Application schemas are normal confique config types. The root schema must implement ConfigSchema so rust-config-tree can discover recursive includes from the intermediate confique layer.

#![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 Field

The include field can have any name. rust-config-tree only knows about it through ConfigSchema::include_paths.

The field should normally have an empty default:

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

The loader receives a partially loaded layer for each file. That lets it discover child config files before the final schema is merged and validated.

Nested Sections

Use #[config(nested)] for structured sections. Nested sections are always used for runtime loading. Add #[schemars(extend("x-tree-split" = true))] when a nested field should also be generated as an independent *.yaml template and <section>.schema.json schema:

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

The natural YAML shape is:

server:
  bind: 127.0.0.1
  port: 8080

Environment-Only Fields

Mark a leaf field with #[schemars(extend("x-env-only" = true))] when the value must be supplied only by an environment variable and should not appear in generated config files. Generated YAML templates and JSON Schemas omit env-only fields, and empty parent objects left behind by those omissions are pruned.

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

Field Value Validation

Generated *.schema.json files are for IDE completion and basic editor checks only. They do not decide whether a concrete field value is legal for the application.

Implement field value validation in code with #[config(validate = Self::validate)]. The validator runs when the final config is loaded through load_config or checked through config-validate.

Template Section Overrides

When a template source has no includes, the crate can derive child template files from nested schema sections marked with x-tree-split. The default top-level path is <section>.yaml relative to the root template directory.

Override that path with 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,
        }
    }
}
}

Runtime Loading

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

Runtime loading is intentionally split between Figment and confique:

figment:
  runtime file loading
  runtime environment loading
  runtime source metadata

confique:
  schema metadata
  defaults
  validation
  config templates

The main API is:

#![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>>(())
}

Use load_config_with_figment when the application needs source metadata:

#![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>>(())
}

Loading Steps

The high-level loader performs these steps:

  1. Resolve the root config path lexically.
  2. Load the first .env file found by walking upward from the root config directory.
  3. Load each config file as a partial layer to discover includes.
  4. Build a Figment graph from the discovered config files.
  5. Merge the ConfiqueEnvProvider with higher priority than files.
  6. Optionally merge application-specific CLI overrides.
  7. Extract a confique layer from Figment.
  8. Apply confique code defaults.
  9. Validate and construct the final schema.

load_config and load_config_with_figment perform steps 1-5 and 7-9. Step 6 is application-specific because this crate cannot infer how a CLI flag maps to a schema field.

File Formats

The runtime file provider is selected from the config path extension:

  • .yaml and .yml use YAML.
  • .toml uses TOML.
  • .json and .json5 use JSON.
  • unknown or missing extensions use YAML.

Template generation still uses confique’s template renderers for YAML, TOML, and JSON5-compatible output.

Include Priority

The high-level loader merges file providers so included files are lower priority than the file that included them. The root config file has the highest file priority.

Environment variables have higher priority than all config files. confique defaults are only used for values that are not supplied by runtime providers.

When CLI overrides are merged after build_config_figment, the full precedence is:

command-line overrides
  > environment variables
    > config files
      > confique code defaults

The command-line syntax is not defined by rust-config-tree. A flag like --server-port can override server.port if the application maps that parsed value into a nested serialized provider. A dotted --server.port or a.b.c syntax only exists if the application implements it.

This means CLI precedence applies only to keys present in the application’s override provider. Use it for operational values that are frequently changed for a single run. Leave durable configuration in files.

#![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>>(())
}

Environment Variables

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

Environment variable names are declared in the schema with confique:

#![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 reads those names from confique::Config::META and builds a Figment provider that maps each environment variable to its exact field path.

Do not use delimiter-based Figment environment mapping for this crate:

#![allow(unused)]
fn main() {
// Do not use this pattern for rust-config-tree schemas.
Env::prefixed("APP_").split("_")
Env::prefixed("APP_").split("__")
}

split("_") treats underscores as nested key separators. That makes APP_DATABASE_POOL_SIZE become a path like database.pool.size, which conflicts with Rust field names such as pool_size.

With ConfiqueEnvProvider, this mapping is explicit:

APP_DATABASE_POOL_SIZE -> database.pool_size

Single underscores remain part of the environment variable name. Figment does not guess the nesting rule.

Dotenv Loading

Before runtime providers are evaluated, the loader searches for a .env file by walking upward from the root config file’s directory.

Existing process environment variables are preserved. Values from .env only fill missing environment variables.

Example:

APP_SERVER_PORT=9000
APP_DATABASE_POOL_SIZE=64

These variables override config file values when the schema declares matching #[config(env = "...")] attributes.

Parsing Values

The bridge provider lets Figment parse environment values. It does not call confique’s parse_env hooks. Keep complex values in config files unless the Figment environment value syntax is a good fit for the type.

Source Tracking

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

Use load_config_with_figment to keep the Figment graph used by runtime loading:

#![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>>(())
}

The returned Figment value can answer source questions for runtime values:

#![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}");
}
}

For values supplied by ConfiqueEnvProvider, interpolation returns the native environment variable name declared in the schema:

database.pool_size came from APP_DATABASE_POOL_SIZE

TRACE Events

The loader emits source tracking events with tracing::trace!. It does this only when TRACE is enabled:

#![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")?;

// If the tracing subscriber is initialized after config loading, emit the
// same source events again after installing the subscriber.
trace_config_sources::<AppConfig>(&figment);
let _ = config;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}

Each event uses the rust_config_tree::config target and includes:

  • config_key: the dotted config key.
  • source: the rendered source metadata.

Values that came only from confique defaults do not have Figment runtime metadata. They are reported as confique default or unset optional field.

Template Generation

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

Templates are generated from the same confique schema used at runtime. confique renders the actual template content, including doc comments, defaults, required fields, and declared environment variable names.

Use 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>>(())
}

Generate Draft 7 JSON Schemas for the root config and split nested sections:

#![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>>(())
}

Mark a nested field with #[schemars(extend("x-tree-split" = true))] when it should be generated as its own *.yaml template and <section>.schema.json schema. Unmarked nested fields stay in the parent template and parent schema.

Mark a leaf field with #[schemars(extend("x-env-only" = true))] when the value must come only from environment variables. Generated templates and JSON Schemas omit env-only fields, and empty parent objects left behind by those omissions are pruned.

Generated schemas omit required constraints. IDEs can still offer completion, but partial files such as log.yaml do not report missing root fields. The root schema only completes fields that belong in the root file; split section fields are omitted there and completed by their own section schemas. Present fields can still receive basic editor checks, such as type, enum, and unknown property checks supported by the generated schema. Generated *.schema.json files do not decide whether a concrete field value is legal for the application. Implement field value validation in code with #[config(validate = Self::validate)]; load_config and config-validate execute that runtime validation.

Bind those schemas from generated TOML, YAML, JSON, and JSON5 templates:

#![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 templates bind the root schema and do not complete split child section fields. Split section YAML templates bind their section schema. JSON and JSON5 templates receive a top-level $schema field. Editor settings such as VS Code json.schemas can still be used as an alternative binding path.

The output format is inferred from the output path:

  • .yaml and .yml generate YAML.
  • .toml generates TOML.
  • .json and .json5 generate JSON5-compatible templates.
  • unknown or missing extensions generate YAML.

The template APIs write exactly under the output_path you pass. The built-in config-template CLI command normalizes generated templates under config/<root_config_name>/; without --output, AppConfig writes config/app_config/app_config.example.yaml and the matching default schema config/app_config/app_config.schema.json.

Schema Bindings

With a schema path of schemas/myapp.schema.json, generated root templates use:

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

Generated section templates bind section schemas:

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

Generated JSON and JSON5 templates bind the schema with a top-level $schema field:

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

Editor settings are still useful when a project does not want an in-file binding:

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

Template Source Selection

Template generation chooses its source tree in this order:

  1. Existing config path.
  2. Existing output template path.
  3. Output path treated as a new empty template tree.

This lets a project update templates from current config files, update an existing template set, or create a new template set from only the schema.

Mirrored Include Trees

If the source file declares includes, generated templates mirror those include paths under the output directory.

# config.yaml
include:
  - server.yaml

Generating config.example.yaml writes:

config.example.yaml
server.yaml

Relative include targets are mirrored under the output file’s parent directory. Absolute include targets remain absolute.

Opt-in Section Splitting

When a source file has no includes, the crate can derive include targets from nested schema sections marked with x-tree-split. For a schema with a marked server section, an empty root template source can produce:

config.example.yaml
server.yaml

The root template receives an include block, and server.yaml contains only the server section. Unmarked nested sections stay inline in their parent template. Nested sections are split recursively only when those fields also carry x-tree-split.

IDE Completions

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

Generated JSON Schemas can be used by TOML, YAML, JSON, and JSON5 config files. They are generated from the same Rust type used by confique:

#![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,
}
}

Generate them with:

#![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>>(())
}

This writes the root schema and section schemas such as schemas/server.schema.json. Generated schemas omit required constraints so completion works for partial config files without missing-field diagnostics. The root schema omits split nested section properties, so split child section completion is available only in files that bind the matching section schema. Unmarked nested sections remain in the root schema.

Fields marked with x-env-only are omitted from generated schemas, so IDEs do not suggest secrets or other values that must come only from environment variables.

IDE schemas are for completion and basic editor checks, such as type, enum, and unknown property checks supported by the generated schema. They do not decide whether a concrete field value is legal for the application. Implement field value validation in code with #[config(validate = Self::validate)], then run it through load_config or config-validate. Required fields and final merged config validation also use those runtime paths.

TOML

TOML files should bind the schema with a top-of-file #:schema directive:

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

[server]
bind = "0.0.0.0"
port = 3000

Do not use a root $schema = "..." field in TOML. It becomes real config data and can affect runtime deserialization. write_config_templates_with_schema adds the #:schema directive automatically for TOML templates.

YAML

YAML files should use the YAML Language Server modeline:

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

server:
  bind: 0.0.0.0
  port: 3000

write_config_templates_with_schema adds this modeline automatically for YAML templates. Split YAML templates bind their section schema, for example log.yaml binds ./schemas/log.schema.json.

JSON

JSON and JSON5 files can bind a schema with a top-level $schema property. write_config_templates_with_schema adds it automatically for generated JSON and JSON5 templates:

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

Editor settings are still useful when a project does not want an in-file binding:

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

YAML can also be bound through VS Code settings:

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

The final layout is:

schemas/myapp.schema.json:
  Root file fields only

schemas/server.schema.json:
  Server section schema

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"

References:

CLI Integration

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

ConfigCommand provides reusable clap subcommands:

  • config-template
  • config-schema
  • config-validate
  • completions
  • install-completions
  • uninstall-completions

These built-in subcommands are separate from application-specific config override flags. Merge config override flags as Figment providers in the runtime loading path.

Config override flags remain part of the consuming application’s CLI. Their names do not need to match dotted config paths. For example, the application can parse --server-port and map it to the nested server.port config key. Only flags that the application maps into CliOverrides affect config values.

Flatten it into an application command enum:

  1. Keep the application’s own Parser type.
  2. Keep the application’s own Subcommand enum.
  3. Add #[command(flatten)] Config(ConfigCommand) to that enum.
  4. Clap expands the flattened ConfigCommand variants into the same command level as the application’s own variants.
  5. Match the Config(command) variant and pass it to 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(())
}

Config Templates

demo config-template

The command writes templates under config/<root_config_name>/. If --output receives a path, only the file name is used. If no output file name is provided, the command writes config/<root_config_name>/<root_config_name>.example.yaml. Add --schema schemas/myapp.schema.json to bind generated TOML, YAML, JSON, and JSON5 templates to generated JSON Schemas. Split YAML templates bind the matching section schema. The command also writes the root and section schemas to the selected schema path.

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

Generate root and section JSON Schemas:

demo config-schema

Without --output, config-schema writes the root schema to config/<root_config_name>/<root_config_name>.schema.json.

Validate the complete runtime config tree:

demo config-validate

Generated editor schemas intentionally avoid required-field diagnostics for split files. config-validate loads includes, applies defaults, and runs final confique validation, including validators declared with #[config(validate = Self::validate)]. Generated *.schema.json files remain for IDE completion and basic editor checks, not field value legality. It prints Configuration is ok when validation succeeds.

Shell Completions

Print completions to stdout:

demo completions zsh

Install completions:

demo install-completions zsh

Uninstall completions:

demo uninstall-completions zsh

The installer supports Bash, Elvish, Fish, PowerShell, and Zsh. It writes the completion file under the user’s home directory and updates the shell startup file for shells that require it.

Before changing an existing shell startup file such as ~/.zshrc, ~/.bashrc, an Elvish rc file, or a PowerShell profile, the command writes a backup next to the original file:

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

Examples

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

The repository includes runnable examples for loading config trees, CLI overrides, built-in config commands, template generation, and the lower-level tree API.

Read the repository examples index:

Run examples from the repository root:

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

The config_commands template and schema commands use the CLI defaults, so AppConfig writes generated files under config/app_config/.

Tree API

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

Use the lower-level tree API when the application does not use confique, or when it needs direct access to traversal results.

#![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>>(())
}

Traversal Rules

The tree loader:

  • normalizes source paths lexically;
  • rejects empty include paths;
  • resolves relative includes from the file that declared them;
  • preserves absolute include paths;
  • detects recursive include cycles;
  • skips files already loaded through another include branch.

ConfigTreeOptions can reverse sibling include traversal:

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

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

Path Helpers

The path helpers are lexical only. They do not resolve symbolic links and do not require paths to exist:

  • absolutize_lexical(path)
  • normalize_lexical(path)
  • resolve_include_path(parent_path, include_path)

GitHub Pages

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

This repository publishes the manual with mdBook and GitHub Pages.

Each language manual is an independent mdBook project. Each language has its own SUMMARY.md, so the left sidebar only contains pages for the current language:

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/

Build locally with:

scripts/publish-pages.sh

The generated site is written to:

target/mdbook

Publishing Workflow

The workflow in .github/workflows/pages.yml runs on pushes to main and on manual dispatch. It:

  1. Checks out the repository.
  2. Installs mdBook.
  3. Runs scripts/publish-pages.sh.
  4. Uploads target/mdbook as the Pages artifact.
  5. Deploys the artifact to GitHub Pages.

The published URL is:

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

Crate Release

For the complete commit, push, Pages deploy, and crate publish flow:

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

Use the crate release helper from the repository root:

scripts/publish-crate.sh

The default mode runs checks and cargo publish --dry-run. To publish to crates.io after the checks pass. If the current version already exists on crates.io, the script bumps the patch version automatically:

scripts/publish-crate.sh --execute

Script usage is summarized in scripts/README.md.