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, Transparent Array Sections, 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:
confiqueowns schema definitions, code defaults, validation, and config template generation.figmentowns runtime loading and runtime source metadata.rust-config-treeowns recursive include traversal, include path resolution,.envloading, 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.
.envloading 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 byload_configorvalidate-config. - YAML, TOML, JSON, and JSON5 template generation.
- TOML
#:schema, YAML Language Server schema modelines, and JSON/JSON5$schemafields for generated templates. - Opt-in YAML template splitting for nested sections marked with
x-tree-split. - Transparent array sections marked with
x-tree-transparent-arrayfor body-only split YAML arrays, runtime shape adaptation, and array-typed section schemas. See Transparent Array Sections. - 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.2"
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. Use the ConfigOverrides derive macro to build an override
provider from parsed CLI flags:
#![allow(unused)]
fn main() {
use clap::Parser;
use rust_config_tree::{
ConfigSchema,
cli::ConfigOverrides,
config::{build_config_figment, load_config_from_figment},
};
#[derive(Debug, Parser, ConfigOverrides)]
struct Cli {
/// Config file path
#[arg(long)]
config: Option<std::path::PathBuf>,
/// Override server port
#[arg(long)]
#[config_override(path = "server.port")]
server_port: Option<u16>,
/// Override log level
#[arg(long)]
#[config_override(path = "log.level")]
log_level: Option<String>,
}
let cli = Cli::parse();
let figment = build_config_figment::<AppConfig>("config.yaml")?
.merge(cli.config_overrides()?);
let config = load_config_from_figment::<AppConfig>(&figment)?;
let _ = config;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}
The #[config_override(path = "...")] attribute maps each CLI flag to a dotted
config path. Only provided flags produce override values; omitted flags
disappear. The override provider is merged last, so provided flags override file
and environment values:
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.
The simplest way is to derive ConfigSchema:
#![allow(unused)]
fn main() {
use confique::Config;
use schemars::JsonSchema;
use rust_config_tree::ConfigSchema;
#[derive(Debug, Config, JsonSchema, ConfigSchema)]
struct AppConfig {
#[config(default = [])]
include: Vec<std::path::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,
}
}
#[derive(ConfigSchema)] expects a field named include of type
Vec<PathBuf> with a #[config(default = [])] attribute. For schemas that
use a different field name or custom include logic, implement the trait
manually:
#![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,
}
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
Transparent Array Sections
When a split section should appear as section: [...] in a single file and as
a body-only [...] array in its split file, mark the field with both
x-tree-split and x-tree-transparent-array, and pair it with
transparent_array_section! or ArraySection<T>:
#![allow(unused)]
fn main() {
use rust_config_tree::transparent_array_section;
transparent_array_section! {
pub struct ChildrenSection {
#[config(default = [{ "name": "worker" }])]
pub items: Vec<ChildDeclaration>,
}
}
#[derive(Debug, Config, JsonSchema)]
struct AppConfig {
#[config(nested)]
#[schemars(extend(
"x-tree-split" = true,
"x-tree-transparent-array" = true
))]
children: ChildrenSection,
}
}
See Transparent Array Sections for the full workflow.
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 validate-config.
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,
}
}
}
}
Transparent Array Sections
Overview
A transparent array section lets a split config section appear as a YAML
array while confique stores the data in an inner items field. The loader,
template generator, and section schema all treat the section as an array rather
than an { items: [...] } object.
In short: write children: [...] in a single file, or write only [...] in a
split file without an items: wrapper.
Use this when list-shaped configuration (worker declarations, route tables, plugin lists) should live in its own split file while keeping that file as a plain array body.
Schema Markers
Transparent array sections require both x-tree-split and
x-tree-transparent-array:
#![allow(unused)]
fn main() {
use rust_config_tree::transparent_array_section;
transparent_array_section! {
/// Child declarations stored as a transparent array section.
pub struct ChildrenSection {
#[config(default = [{ "name": "worker" }])]
pub items: Vec<ChildDeclaration>,
}
}
#[derive(Debug, Config, JsonSchema)]
struct AppConfig {
#[config(nested)]
#[schemars(extend(
"x-tree-split" = true,
"x-tree-transparent-array" = true
))]
children: ChildrenSection,
}
}
| Extension | Purpose |
|---|---|
x-tree-split | Emit a separate children.yaml template and children.schema.json |
x-tree-transparent-array | Treat the section as a YAML array at runtime and in templates |
x-tree-inner-field (optional) | Override the inner confique field name; default is "items" |
Rust Type Options
transparent_array_section! macro
The macro generates len, is_empty, as_slice, and From<SectionName> for Vec<T>. Each section struct can declare its own
#[config(default = ...)] template sample.
Generic ArraySection<T>
Skip the macro and use ArraySection<T> directly:
#![allow(unused)]
fn main() {
use rust_config_tree::ArraySection;
#[derive(Debug, Config, JsonSchema)]
struct ChildrenSection {
#[config(default = [{ "name": "worker" }])]
items: Vec<ChildDeclaration>,
}
}
ArraySection<T> supports only one #[config(default)]. When multiple sections
need different template defaults, define a separate struct per section or use
the macro.
YAML Shapes
Single file
Write the array directly in the root config:
children:
- name: api
- name: worker
Split file
Reference the split file from the root config:
include:
- children.yaml
mode: demo
children.yaml contains only the array body. Do not add a section root key
or an items: wrapper:
- name: api
- name: worker
Shapes accepted by the loader
load_config accepts all three forms:
- Transparent array:
children: [...] - Explicit inner field:
children:\n items: [...] - Body-only split file:
children.yamlcontains[...]
The loader merges split files into children: { items: [...] } before
confique deserializes the final config.
Runtime defaults vs template defaults
Template generation uses #[config(default = ...)] to write sample entries
(for example a worker row).
When the transparent section is omitted entirely at runtime, the library
injects { items: [] } through TransparentSectionTracker so template defaults
do not leak into runtime as phantom entries.
Therefore:
- Template defaults guide authors and appear in
generate-templateoutput. - Runtime defaults yield an empty array when the section is omitted, not the template sample worker.
Templates and schemas
Generated children.schema.json uses a top-level array type, so IDEs complete
array items directly while editing children.yaml.
Template generation emits block YAML array bodies:
- No
children:root key - No
items:wrapper - No flow-style
[{ ... }]
The root template config.example.yaml includes include: [children.yaml].
Accessing data
#![allow(unused)]
fn main() {
config.children.len();
config.children.is_empty();
config.children.as_slice();
let vec: Vec<ChildDeclaration> = config.children.into();
}
Both the macro and ArraySection expose this API surface.
Complete example
The repository ships a runnable demo:
cargo run --example transparent_array_section
The example:
- Writes split config files into a temporary directory
- Generates the section schema
- Calls
load_configand verifies transparent array loading
For a downstream project, see
rust-supervisor split-config
for real groups and children usage.
Related pages
- Configuration Schema — nested sections and split markers
- Template Generation — split section template output rules
- Runtime Loading — loading steps and merge precedence
- IDE Completions — section schema bindings
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:
- Resolve the root config path lexically.
- Load the first
.envfile found by walking upward from the root config directory. - Load each config file as a partial layer to discover includes.
- Build a Figment graph from the discovered config files.
- Merge the
ConfiqueEnvProviderwith higher priority than files. - Optionally merge application-specific CLI overrides.
- Extract a
confiquelayer from Figment. - Apply
confiquecode defaults. - 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.
Transparent Array Section Adaptation
When the schema marks a section with x-tree-transparent-array, the loader
adapts YAML shapes after the Figment merge and before confique
deserialization:
- Body-only split files (for example
children.yamlcontaining[...]) merge intochildren: { items: [...] }. - Single-file
children: [...]normalizes to the same inner shape. - When the section is omitted entirely at runtime, the library injects
{ items: [] }so template defaults do not leak in as phantom entries.
Applications call load_config directly and do not need post-normalize
logic. See Transparent Array Sections for details.
File Formats
The runtime file provider is selected from the config path extension:
.yamland.ymluse YAML..tomluses TOML..jsonand.json5use 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.
Use the ConfigOverrides derive macro to build an override provider from
parsed CLI flags:
#![allow(unused)]
fn main() {
use clap::Parser;
use rust_config_tree::{
ConfigSchema,
cli::ConfigOverrides,
config::{build_config_figment, load_config_from_figment},
};
#[derive(Debug, Parser, ConfigOverrides)]
struct Cli {
/// Config file path
#[arg(long)]
config: Option<std::path::PathBuf>,
/// Override server port
#[arg(long)]
#[config_override(path = "server.port")]
server_port: Option<u16>,
/// Override log level
#[arg(long)]
#[config_override(path = "log.level")]
log_level: Option<String>,
}
let cli = Cli::parse();
let figment = build_config_figment::<AppConfig>("config.yaml")?
.merge(cli.config_overrides()?);
let config = load_config_from_figment::<AppConfig>(&figment)?;
let _ = config;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}
The #[config_override(path = "...")] attribute maps each CLI flag to a dotted
config path. Only provided flags produce override values; omitted flags
disappear.
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 validate-config
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:
.yamland.ymlgenerate YAML..tomlgenerates TOML..jsonand.json5generate JSON5-compatible templates.- unknown or missing extensions generate YAML.
The template APIs write exactly under the output_path you pass. The built-in
generate-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:
- Existing config path.
- Existing output template path.
- 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.
Transparent Array Section Templates
When a nested section also carries x-tree-transparent-array, the section
template emits only a block YAML array body. It does not write a section root
key (such as children:), an items: wrapper, or flow-style [{ ... }].
The matching section schema uses a top-level array type rather than an
{ items: [...] } object, so IDEs complete array items directly while editing
split files such as children.yaml.
See Transparent Array Sections for the full workflow.
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 validate-config. Required fields and final merged
config validation also use those runtime paths.
Transparent Array Sections
Use x-tree-transparent-array when a split section should serialize as a bare
YAML array in both the root config and the split file body. Pair it with
transparent_array_section! or ArraySection<T>. See
Transparent Array Sections for the full workflow.
#![allow(unused)]
fn main() {
use rust_config_tree::transparent_array_section;
transparent_array_section! {
pub struct ChildrenSection {
#[config(default = [{ "name": "worker" }])]
pub items: Vec<ChildDeclaration>,
}
}
#[derive(Debug, Config, JsonSchema)]
struct AppConfig {
#[config(nested)]
#[schemars(extend(
"x-tree-split" = true,
"x-tree-transparent-array" = true
))]
children: ChildrenSection,
}
}
Behavior:
load_configacceptschildren: [...],children:\n items: [...], and body-onlychildren.yaml.- Generated
children.schema.jsonis a top-levelarray, not an{ items: [...] }object. - Template generation emits block YAML array bodies without
children:or flow-style[{ ... }].
Optional extension x-tree-inner-field = "items" overrides the confique inner
field name. The default is "items".
Run cargo run --example transparent_array_section for a complete demo.
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:
generate-templategenerate-schemavalidate-configcompletionsinstall-completionsuninstall-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:
- Keep the application’s own
Parsertype. - Keep the application’s own
Subcommandenum. - Add
#[command(flatten)] Config(ConfigCommand)to that enum. - Clap expands the flattened
ConfigCommandvariants into the same command level as the application’s own variants. - Match the
Config(command)variant and pass it tohandle_config_command.
The AppConfig type argument in handle_config_command::<Cli, AppConfig>
is the schema that supplies fields, defaults, validation, and template output.
generate-template does not discover arbitrary Rust structs at runtime. If an
application exposes multiple config schemas, add an application level selector
and dispatch to different handle_config_command::<Cli, S> calls.
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 generate-template
The command writes templates to the output path specified by --output. If no
--output 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 generate-template --output app_config.example.toml --schema schemas/myapp.schema.json
Generate root and section JSON Schemas:
demo generate-schema
Without --output, generate-schema writes the root schema to
config/<root_config_name>/<root_config_name>.schema.json.
Validate the complete runtime config tree:
demo validate-config
demo validate-config --config config.yaml
When --config is omitted, validate-config uses the consumer default path
passed to handle_config_command.
Generated editor schemas intentionally avoid required-field diagnostics for
split files. validate-config 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 -- generate-template
cargo run --example config_commands -- generate-schema
cargo run --example config_commands -- validate-config
cargo run --example generate_templates
cargo run --example tree_api
cargo run --example transparent_array_section
The config_commands template and schema commands use the CLI defaults, so
AppConfig writes generated files under config/app_config/.
See transparent-sections.md for transparent array section documentation.
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:
rust-config-tree/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:
- Checks out the repository.
- Installs mdBook.
- Runs
scripts/publish-pages.sh. - Uploads
target/mdbookas the Pages artifact. - 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.