19 releases (breaking)
Uses new Rust 2024
| new 0.16.0 | Apr 15, 2026 |
|---|---|
| 0.14.0 | Apr 11, 2026 |
| 0.12.0 | Mar 6, 2026 |
#219 in Configuration
88 downloads per month
Used in 9 crates
300KB
6K
SLoC
clapfig
Rich, layered configuration for Rust applications. Define a struct, point at your files, and go.
clapfig discovers, merges, and manages configuration from multiple sources — config files, environment variables, and programmatic overrides — through a pure Rust builder API. The core library has no dependency on any CLI framework: you can use it in GUI apps, servers, or with any argument parser. For clap users, an optional adapter provides drop-in config gen|list|get|set|unset subcommands with zero boilerplate.
Built on confique for struct-driven defaults and commented template generation.
Features
Core (always available, no CLI framework needed):
- Struct as source of truth — define settings as a Rust struct with defaults and
///doc comments - Layered merge — defaults < config files < env vars < overrides, every layer sparse, customizable precedence order
- Multi-path file search — platform config dir, home, cwd, ancestor walk, or any path
- Search modes — merge all found configs or use the first match
- Ancestor walk — walk up from cwd to find project configs, with configurable boundary (
.git, filesystem root) - Tree-walk resolution — build a reusable
Resolver<C>once, call.resolve_at(&dir)for every leaf in a dynamic file tree (.htaccess/.editorconfigpattern). Per-callCwd/Ancestorsanchoring, instance-scoped file cache so repeated walks pay disk+parse once per unique file. - Prefix-based env vars —
MYAPP__DATABASE__URLmaps todatabase.urlautomatically - Strict mode — unknown keys error with file path, key name, and line number (on by default)
- Post-merge validation hook —
.post_validate(|c| ...)closes the gap between confique's structural validation and the semantic constraints every real app has: port ranges, cross-field invariants, enum combinations, filesystem preconditions - Structured errors + rendering —
ClapfigErrorcarries data (keys, paths, lines, source text); therendermodule turns it into plain text ormiette-style output with snippets and carets (rich mode behind therich-errorsfeature) - Template generation — emit a commented sample config from the struct's doc comments
- JSON Schema generation —
clapfig::schema::generate_schema::<C>()produces a Draft 2020-12 JSON Schema for UI editors, external validators, and IDE integrations; also exposed asapp config schema - Persistence with named scopes — global/local config file patterns with
--scopetargeting
Clap adapter (clap feature, on by default):
- Config subcommand — drop-in
config gen|get|set|unset|listfor clap --scopeflag — target a specific scope for any config subcommand- Auto-matching overrides — map clap args to config keys by name in one call
Quick Start
[dependencies]
clapfig = "0.10"
Define your config with the Config derive (re-exported from confique):
use clapfig::Config;
use serde::{Serialize, Deserialize};
#[derive(Config, Serialize, Deserialize, Debug)]
pub struct AppConfig {
/// The host address to bind to.
#[config(default = "127.0.0.1")]
pub host: String,
/// The port number.
#[config(default = 8080)]
pub port: u16,
/// Database settings.
#[config(nested)]
pub database: DbConfig,
}
#[derive(Config, Serialize, Deserialize, Debug)]
pub struct DbConfig {
/// Connection string URL.
pub url: Option<String>,
/// Connection pool size.
#[config(default = 10)]
pub pool_size: usize,
}
Load it:
use clapfig::Clapfig;
fn main() -> anyhow::Result<()> {
let config: AppConfig = Clapfig::builder()
.app_name("myapp")
.load()?;
println!("Listening on {}:{}", config.host, config.port);
Ok(())
}
That app_name("myapp") call sets sensible defaults:
- Searches for
myapp.tomlin the platform config directory - Merges env vars prefixed with
MYAPP__ - Fills in
#[config(default)]values for anything not provided
Without clap:
clapfig = { version = "0.10", default-features = false }
Layer Precedence
Compiled defaults #[config(default = ...)]
↑ overridden by
Config files search paths in order, later paths win
↑ overridden by
Environment vars MYAPP__KEY
↑ overridden by
URL query params .url_query() (requires "url" feature)
↑ overridden by
Overrides .cli_override()
Every layer is sparse. You only specify the keys you want to override. Unset keys fall through to the next layer down.
This is the default order. You can customize it with .layer_order():
use clapfig::{Clapfig, Layer};
let config: AppConfig = Clapfig::builder()
.app_name("myapp")
.layer_order(vec![Layer::Env, Layer::Files, Layer::Cli])
.load()?;
Layers listed later override earlier ones. Omitting a layer excludes it from merging entirely. See Layer for the available variants.
Demo
The repo includes a runnable example that exercises every feature:
cargo run --example clapfig_demo -- echo
cargo run --example clapfig_demo -- --color blue --port 8080 echo
cargo run --example clapfig_demo -- config gen
cargo run --example clapfig_demo -- config list
cargo run --example clapfig_demo -- config get server.port
# See the rich error renderer (miette) in action:
cargo run --example clapfig_demo --features rich-errors -- echo
# (drop a `clapfig-demo.toml` with an unknown key like `typo = 1` first)
See examples/clapfig_demo/ for the full source.
Tree-Walk Resolution with Resolver
For tools that walk a file tree where every directory can have its own config — the .editorconfig / .eslintrc / .htaccess pattern — Resolver provides cached, per-directory resolution:
use clapfig::{Clapfig, Config, SearchPath, Boundary};
let resolver = Clapfig::builder::<MyConfig>()
.app_name("mytool")
.file_name(".mytool.toml")
.search_paths(vec![SearchPath::Ancestors(Boundary::Marker(".git"))])
.build_resolver()?;
// Each call resolves config independently from that directory,
// walking ancestors up to the .git boundary.
for dir in directories_to_process {
let config = resolver.resolve_at(&dir)?;
process(&dir, &config);
}
Key properties:
- Per-call anchoring —
SearchPath::CwdandSearchPath::Ancestorsare relative to the directory passed toresolve_at(), not the process CWD. - File caching — files are cached by absolute path inside the resolver. A tree walk over 1000 directories sharing 5 ancestor configs pays disk+parse once per unique file.
post_validatecomposition — a validation hook registered on the builder fires on everyresolve_at()call.
See the Resolver docs for the full API.
Documentation
Guides (in docs/):
- Getting Started — installation, first config struct, basic usage
- Layered Configuration — layers, search paths, merge modes, env vars, overrides
- Resolver Guide — per-directory resolution for tree-walk tools
- Config Command Guide — the
config gen|list|get|set|unsetintegration
API reference: the full API with design rationale lives in the crate-level docs on docs.rs.
Dependencies
~2.7–6.5MB
~127K SLoC