clapfig 0.9.2

Rich, layered configuration for Rust CLI apps
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# 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](https://docs.rs/clap) users, an optional adapter provides drop-in `config gen|list|get|set` subcommands with zero boilerplate.

Built on [confique](https://github.com/LukasKalbertodt/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
- **Multi-path file search** — platform config dir, home, cwd, ancestor walk, or any path, in precedence order
- **Search modes** — merge all found configs (layered overrides) or use the first match ("find my config")
- **Ancestor walk**`SearchPath::Ancestors` walks up from cwd to find project configs, with configurable boundary (`.git`, filesystem root)
- **Prefix-based env vars**`MYAPP__DATABASE__URL` maps to `database.url` automatically
- **Strict mode** — unknown keys in config files error with file path, key name, and line number (on by default)
- **Template generation** — emit a commented sample config derived from the struct's doc comments
- **Persistence with named scopes**`persist_scope("local", path)` / `persist_scope("global", path)` for global/local config patterns. Scope paths auto-added to search paths.

**Clap adapter** (`clap` feature, on by default):

- **Config subcommand** — drop-in `config gen|get|set|list` commands for clap
- **`--scope` flag** — target a specific scope for any config subcommand (e.g. `config set key val --scope global`)
- **Auto-matching overrides** — map clap args to config keys by name in one call

## Quick Start

Define your config with confique's `Config` derive:

```rust
use confique::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 in one line:

```rust
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.toml` in the platform config directory
- Merges env vars prefixed with `MYAPP__`
- Fills in `#[config(default)]` values for anything not provided

## Setup

### Defaults from `app_name`

| Derived setting | Value |
|-----------------|-------|
| File name | `{app_name}.toml` |
| Search paths | Platform config dir (via [`directories`]https://docs.rs/directories) |
| Env prefix | `{APP_NAME}` (uppercased) |

### Builder methods

```rust
use clapfig::{Clapfig, SearchPath};

let config: AppConfig = Clapfig::builder()
    // Required — sets defaults for file_name, search_paths, env_prefix
    .app_name("myapp")

    // Optional overrides
    .file_name("settings.toml")                                   // override config file name
    .search_paths(vec![SearchPath::Platform, SearchPath::Cwd])    // replace default search paths
    .add_search_path(SearchPath::Cwd)                             // append a path without replacing
    .env_prefix("MY_APP")                                         // override env var prefix
    .no_env()                                                     // disable env var loading entirely
    .strict(false)                                                // disable strict mode (allow unknown keys)
    .cli_override("host", some_value)                             // override a key from a CLI arg
    .load()?;
```

### Search Paths, Modes, and Persistence

Config file handling has three orthogonal axes on the builder:

- **Discovery** (`.search_paths()`) — where to look. Paths listed in priority-ascending order (last = highest).
- **Resolution** (`.search_mode()`) — `Merge` (default: deep-merge all found files) or `FirstMatch` (use the single highest-priority file).
- **Persistence** (`.persist_scope(name, path)`) — named targets for `config set` writes. Scope paths are auto-added to search paths.

```rust
use clapfig::{Clapfig, SearchPath, SearchMode, Boundary};

// Layered global + local with named scopes
let config: AppConfig = Clapfig::builder()
    .app_name("myapp")
    .search_paths(vec![SearchPath::Platform, SearchPath::Cwd])
    .persist_scope("local", SearchPath::Cwd)        // default for writes
    .persist_scope("global", SearchPath::Platform)
    .load()?;

// Find nearest project config (walk up to .git, use first match)
let config: AppConfig = Clapfig::builder()
    .app_name("mytool")
    .search_paths(vec![SearchPath::Ancestors(Boundary::Marker(".git"))])
    .search_mode(SearchMode::FirstMatch)
    .load()?;
```

With scopes configured, the `--scope` flag targets specific config files:

```sh
myapp config set port 3000                    # writes to "local" (default)
myapp config set port 3000 --scope global     # writes to "global"
myapp config list                             # merged resolved config
myapp config list --scope global              # only global scope's entries
```

Available `SearchPath` variants: `Platform`, `Home(".myapp")`, `Cwd`, `Path(PathBuf)`, `Ancestors(Boundary)`.

`Ancestors` walks up from cwd, expanding inline into multiple directories (shallowest first, cwd last = highest priority). `Boundary::Root` walks to the filesystem root; `Boundary::Marker(".git")` stops at the directory containing the marker (inclusive).

Missing files are silently skipped. See the [`types` module docs](https://docs.rs/clapfig/latest/clapfig/types/) for the full conceptual guide and use-case examples.

### Strict Mode

Strict mode is **on by default**. If a config file contains a key that doesn't match any field in your struct, loading fails with a clear error including the file path, key name, and line number:

```
Unknown key 'typo_key' in /home/user/.config/myapp/myapp.toml (line 5)
```

Disable it with `.strict(false)` if you want to allow extra keys.

## Normalizing Values

You can normalize config values during deserialization using confique's `#[config(deserialize_with = ...)]` attribute. The function has the standard serde deserializer signature and runs automatically when a value is loaded from any source — config files, environment variables, or programmatic overrides.

```rust
use confique::Config;
use serde::{Serialize, Deserialize, Deserializer};

/// Normalize a string to lowercase during deserialization.
fn normalize_lowercase<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
    let s = String::deserialize(d)?;
    Ok(s.to_lowercase())
}

#[derive(Config, Serialize, Deserialize, Debug)]
pub struct DisplayConfig {
    /// Terminal color name, always stored as lowercase.
    #[config(deserialize_with = normalize_lowercase, default = "yellow")]
    pub color: String,

    /// Output format (pretty or plain).
    #[config(default = "pretty")]
    pub format: String,
}
```

With this, `color = "BLUE"` in a TOML file, `MYAPP__COLOR=Blue` as an env var, or `.cli_override("color", "RED")` all resolve to their lowercase form. Note that `#[config(default)]` values are injected directly by confique and do **not** pass through the deserializer — if your default needs normalization, write it in normalized form.

## Environment Variables

With env prefix `MYAPP`, variables map via double-underscore nesting:

| Env var | Config key |
|---------|------------|
| `MYAPP__HOST` | `host` |
| `MYAPP__PORT` | `port` |
| `MYAPP__DATABASE__URL` | `database.url` |
| `MYAPP__DATABASE__POOL_SIZE` | `database.pool_size` |

`__` (double underscore) separates nesting levels. Single `_` within a segment is literal (part of the field name).

Disable env loading entirely with `.no_env()`.

## Programmatic Overrides

The `cli_override` and `cli_overrides_from` methods on the builder work with **any** value source — they are not clap-specific despite the name. Use them to inject overrides from CLI args, GUI inputs, HTTP requests, or anything else.

### Auto-matching

If your override source derives `Serialize`, `cli_overrides_from` auto-matches fields by name against config keys:

```rust
use clap::Parser;
use serde::Serialize;

#[derive(Parser, Serialize)]
struct Cli {
    #[command(subcommand)]
    #[serde(skip)]
    command: Commands,

    #[arg(long)]
    host: Option<String>,

    #[arg(long)]
    port: Option<i64>,

    #[arg(long)]
    db_url: Option<String>,
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    let config: AppConfig = Clapfig::builder()
        .app_name("myapp")
        .cli_overrides_from(&cli)                // auto-matches host, port
        .cli_override("database.url", cli.db_url) // manual: name doesn't match
        .load()?;

    Ok(())
}
```

`cli_overrides_from(source)` serializes the source, skips `None` values, and keeps only keys that match a config field. Non-matching fields (`command`, `db_url`) are silently ignored. Works with any `Serialize` type — structs, `HashMap`s, etc.

#### Manual overrides

For fields where the CLI name differs from the config key, use `cli_override`:

```rust
.cli_override("database.url", cli.db_url)
```

`cli_override(key, value)` takes `Option<V>` where `V: Into<toml::Value>` — `None` is silently skipped. Dot notation addresses nested keys.

Both methods compose freely and push to the same override list. Later calls take precedence.

Supported value types: `String`, `&str`, `i64`, `i32`, `i8`, `u8`, `u32`, `f64`, `f32`, `bool`.

## Clap Adapter (optional)

> Requires the `clap` Cargo feature (enabled by default). To use clapfig without clap:
> ```toml
> clapfig = { version = "...", default-features = false }
> ```

### Config Subcommand

Add config management to your CLI by nesting `clapfig::ConfigArgs`:

```rust
use clap::Subcommand;
use clapfig::{Clapfig, ConfigArgs, ConfigResult};

#[derive(Subcommand)]
enum Commands {
    /// Run the application
    Run,
    /// Manage configuration
    Config(ConfigArgs),
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Commands::Config(args) => {
            let action = args.into_action();
            let result = Clapfig::builder::<AppConfig>()
                .app_name("myapp")
                .handle(&action)?;
            match result {
                ConfigResult::Template(t) => print!("{t}"),
                ConfigResult::KeyValue { key, value, doc } => {
                    for line in &doc { println!("# {line}"); }
                    println!("{key} = {value}");
                }
                ConfigResult::ValueSet { key, value } => {
                    println!("Set {key} = {value}");
                }
            }
        }
        Commands::Run => {
            let config: AppConfig = Clapfig::builder()
                .app_name("myapp")
                .cli_override("host", cli.host)
                .cli_override("port", cli.port)
                .load()?;
            println!("Listening on {}:{}", config.host, config.port);
        }
    }

    Ok(())
}
```

This gives your users:

```sh
myapp config gen                    # print commented sample config to stdout
myapp config gen -o myapp.toml      # write to file
myapp config get database.url       # print the resolved value of a key
myapp config set port 3000          # persist a value to the user's config file
```

## Template Generation

`config gen` produces a commented TOML file derived from your struct's `///` doc comments:

```toml
# The host address to bind to.
# Default: "127.0.0.1"
#host = "127.0.0.1"

# The port number.
# Default: 8080
#port = 8080

[database]
# Connection string URL.
#url =

# Connection pool size.
# Default: 10
#pool_size = 10
```

The template stays in sync with code — it's generated from the same struct. Change a doc comment or a default, the template reflects it.

## 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
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.

## Persistence

`config set <key> <value>` writes to a named persist scope configured via `.persist_scope()` on the builder.

- **Named scopes** — each scope has a name (e.g. "local", "global") and a `SearchPath`. The first scope added is the default for writes.
- **Auto-discovery** — scope paths are automatically added to search paths, so persisted values are always discoverable in the merged view.
- **`--scope` flag** — target a specific scope: `config set key val --scope global`. Works with `list`, `get`, `set`, and `unset`.
- If the file exists, the key is patched in place using [`toml_edit`]https://docs.rs/toml_edit, **preserving existing comments and formatting**.
- If the file doesn't exist, a fresh config is created from the generated template with the target key set.
- If no scopes are configured, `config set` returns `ClapfigError::NoPersistPath`.

## Demo Application

The repo includes a runnable example that exercises every clapfig feature — nested config structs, file search paths, env vars, CLI overrides, and the `config` subcommand. It's a good starting point for integration and for ad-hoc testing.

```sh
# Print all resolved values (default color: yellow)
cargo run --example clapfig_demo -- echo

# Override via env var
CLAPFIG_DEMO__DISPLAY__COLOR=red cargo run --example clapfig_demo -- echo

# Override via CLI flag
cargo run --example clapfig_demo -- --color blue --port 8080 echo

# Config subcommands
cargo run --example clapfig_demo -- config gen
cargo run --example clapfig_demo -- config list
cargo run --example clapfig_demo -- config get server.port
```

See [`examples/clapfig_demo/`](examples/clapfig_demo/) for the full source.

## Full Example (with clap)

```rust
use clap::{Parser, Subcommand};
use confique::Config;
use serde::{Serialize, Deserialize};
use clapfig::{Clapfig, ConfigArgs, ConfigResult, SearchPath};

// -- Config struct --

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

// -- CLI --

#[derive(Parser, Serialize)]
#[command(name = "myapp")]
struct Cli {
    #[command(subcommand)]
    #[serde(skip)]
    command: Commands,

    #[arg(long, global = true)]
    host: Option<String>,

    #[arg(long, global = true)]
    port: Option<i64>,
}

#[derive(Subcommand)]
enum Commands {
    Run,
    Config(ConfigArgs),
}

// -- Main --

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Commands::Config(args) => {
            Clapfig::builder::<AppConfig>()
                .app_name("myapp")
                .add_search_path(SearchPath::Cwd)
                .persist_scope("local", SearchPath::Cwd)
                .handle_and_print(&args.into_action())?;
        }
        Commands::Run => {
            let config: AppConfig = Clapfig::builder()
                .app_name("myapp")
                .add_search_path(SearchPath::Cwd)
                .cli_overrides_from(&cli)
                .load()?;

            println!("Listening on {}:{}", config.host, config.port);
            if let Some(url) = &config.database.url {
                println!("Database: {}", url);
            }
        }
    }

    Ok(())
}
```