confroid 0.0.4

The n+1-st config reader for your environment-based configs.
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
# confroid

> Your config on 'roids

confroid is the n+1-st config reader for your environment-based configs.

confroid derives an environment-variable reader from a plain Rust struct.
It is type-driven: nesting, collections, and optionality all come from the field's type,
so there is very little to annotate.

## Installation

```sh
cargo add confroid
# for documentation generation:
cargo add confroid --features docs
```

## Quick start

```rust
const DEFAULT_NAME: &str = "World";

#[derive(confroid::Config)]
struct HttpConfig {
    #[confroid(default = 8080)]
    /// The port to listen on.
    port: u16,
    #[confroid(example = "localhost")]
    /// The host to listen on.
    host: String,
}

#[derive(confroid::Config)]
struct Config {
    /// The HTTP configuration.
    http: HttpConfig,
    #[confroid(default = "Hello")]
    /// The greeting to use.
    greeting: String,
    #[confroid(default = DEFAULT_NAME, example = "John")]
    /// The name of the user.
    name: String,
}

fn main() {
    let config: Config = confroid::from_env().unwrap();
    println!("{}, {}!", config.greeting, config.name);
}
```

With only `HTTP__HOST=localhost` set, this reads `http.port` as `8080`, `greeting` as `Hello`, and `name` as `World`.

The entry points are:

- `confroid::from_env()` — read from the process environment.
- `confroid::from_pairs(pairs)` — read from an explicit set of key/value pairs (handy if you have a custom config
  source).

## Error reporting

Confroid reads every independent field before returning an error.
When multiple variables are missing or invalid,
`ConfroidError::Multiple` contains all of the failures in declaration order;
nested structs and collections are flattened into the same report.
Its `Display` output lists every problem, so a deployment can be fixed in one pass rather than one restart per variable.

Invalid-value errors retain the concrete `FromStr` error as their `std::error::Error::source`,
allowing reporters such as `anyhow` and `color-eyre` to show the parser's cause chain.
Use `ConfroidError::errors()` to iterate over either a single leaf error or all errors in an aggregate uniformly.

## Naming

Every field maps to an environment variable named after the field in `SCREAMING_SNAKE_CASE`.
Nesting is detected from the field's type and nested fields prepend their own name as a prefix, joined with `__`,
so `http.port` becomes `HTTP__PORT`.

Add a prefix to every variable represented by a struct with a container-level `prefix` attribute.
This is useful for namespacing an application's environment:

```rust
#[derive(confroid::Config)]
#[confroid(prefix = "GUPPY")]
struct Config {
    // Read from `GUPPY__PORT`.
    port: u16,
}
```

Override an individual field's derived name with the `name` attribute.
Renaming a nested field renames the whole prefix, so its children follow along:

```rust
#[derive(confroid::Config)]
struct Config {
    #[confroid(name = "LISTEN_PORT")]
    /// Read from `LISTEN_PORT` instead of `PORT`.
    port: u16,
    #[confroid(name = "SERVER")]
    /// Nested fields are read as `SERVER__*` instead of `HTTP__*`
    /// (e.g. `http.port` -> `SERVER__PORT`).
    http: HttpConfig,
}
```

## Defaults

Use `default = <expr>` to supply an explicit fallback, or the bare `default` attribute
(no value) to fall back to [`Default::default()`] for the field's type:

```rust
#[derive(confroid::Config)]
struct Config {
    // Unset -> 8080
    #[confroid(default = 8080)]
    port: u16,
    // Unset -> 0 (u16::default())
    #[confroid(default)]
    retries: u16,
}
```

A default only applies when the variable is **unset** — a value that is present but fails to parse is always an error.

## Optional fields

An `Option<T>` field is read from presence:

```rust
#[derive(confroid::Config)]
struct Config {
    name: Option<String>,
}
```

- if you don't set `NAME`, `name` is `None`
- if `NAME` is present but empty, `name` is also `None`
- if you set `NAME` to a non-empty string, `name` is `Some(value)`

Treating an empty optional leaf as absent matches `.env` examples and deployment tools such as Compose, Helm,
and CI templating, which commonly materialize unset variables as empty strings.
Required `String` fields still preserve an explicit empty value;
required parsed fields receive it and report a parse error when it is invalid.

### Combining `Option` with `default`

An `Option` field with a `default` can never be `None` when the variable is unset, since the default takes over
(unless when the default itself resolves to `None`):

```rust
#[derive(confroid::Config)]
struct Config {
    // Unset -> Some("anonymous")
    #[confroid(default = "anonymous")]
    name: Option<String>,
    // Unset -> None, because Option::default() is None
    #[confroid(default)]
    nickname: Option<String>,
}
```

### Optional structs

For an `Option` around a nested struct there is no single variable to test, so presence is decided by its children:

```rust
#[derive(confroid::Config)]
struct Config {
    tls: Option<TlsConfig>,
}

#[derive(confroid::Config)]
struct TlsConfig {
    cert: String,
    key: String,
}
```

- if none of `TLS__*` are set, `tls` is `None`
- if any of `TLS__*` are set, the struct is parsed normally — so a
  partially-set struct (e.g. `TLS__CERT` without `TLS__KEY`) is an
  `EnvVarNotFound` error rather than silently becoming `None`

## Collections

### HashMap

Confroid supports `HashMap` fields out of the box.
Map keys are taken verbatim from the variable name (case preserved), while field names are still uppercased:

```rust
#[derive(confroid::Config)]
struct Config {
    people: HashMap<String, Person>,
}

#[derive(confroid::Config)]
struct Person {
    name: String,
    age: u8,
}
```

```bash
PEOPLE__alice__NAME=Alice
PEOPLE__alice__AGE=30
PEOPLE__bob__NAME=Bob
PEOPLE__bob__AGE=25
```

becomes

```rust
assert_eq!(config.people.len(), 2);
assert_eq!(config.people.get("alice").unwrap().name, "Alice");
assert_eq!(config.people.get("bob").unwrap().name, "Bob");
```

### Vectors

By default a `Vec` is read from contiguous indexed variables, no configuration required:

```rust
#[derive(confroid::Config)]
struct Config {
    names: Vec<String>,
}
```

```bash
NAMES__0=Alice
NAMES__1=Bob
```

becomes

```rust
assert_eq!(config.names.len(), 2);
assert_eq!(config.names[0], "Alice");
assert_eq!(config.names[1], "Bob");
```

Indices must be contiguous starting at `0`.
A gap (e.g. `NAMES__0` and `NAMES__2` with no `NAMES__1`) is an error rather than a silently compacted list.

A `Vec` field is required: if no matching variables are set and the field has no `default`,
it is treated as missing and returns the same `EnvVarNotFound` error as any other required variable.
Use `#[confroid(default)]` to fall back to an empty vector instead:

```rust
#[derive(confroid::Config)]
struct Config {
    // No NAMES__* set -> Vec::new()
    #[confroid(default)]
    names: Vec<String>,
}
```

#### Delimited values

For scalar elements you can opt into a single delimited variable with `auto_vec` (default delimiter `,`):

```diff
#[derive(confroid::Config)]
struct Config {
+   #[confroid(auto_vec)]
    names: Vec<String>,
}
```

so `NAMES=Alice,Bob` becomes `names: vec!["Alice", "Bob"]`.

Choose a different delimiter with `auto_vec_delimiter`:

```rust
#[derive(confroid::Config)]
struct Config {
    #[confroid(auto_vec, auto_vec_delimiter = ";")]
    names: Vec<String>,
}
```

`auto_vec` only applies to scalar element types, since a delimited value cannot express a struct's fields.

#### Vectors of structs

The indexed form composes with nested structs — each index is its own prefix:

```rust
#[derive(confroid::Config)]
struct Config {
    servers: Vec<Server>,
}

#[derive(confroid::Config)]
struct Server {
    host: String,
    port: u16,
}
```

```bash
SERVERS__0__HOST=a.example.com
SERVERS__0__PORT=80
SERVERS__1__HOST=b.example.com
SERVERS__1__PORT=443
```

becomes

```rust
assert_eq!(config.servers.len(), 2);
assert_eq!(config.servers[0].host, "a.example.com");
assert_eq!(config.servers[1].port, 443);
```

## Supported types

Out of the box, confroid reads:

- integers (`u8``u128`, `usize`, `i8``i128`, `isize`), floats (`f32`, `f64`),
  `bool`, `char`, `String`, `Box<str>`, and `Arc<str>`
- `NonZeroU16`, useful for ports that must not be zero
- `std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}`
  and `std::path::PathBuf`
- `Option<T>`, `Vec<T>`, and `HashMap<String, T>` for any supported `T`
- any struct that derives `confroid::Config`

Two optional features add external scalar types:

- `humantime` enables `#[confroid(humantime)]` on `std::time::Duration` and
  `Option<std::time::Duration>` fields, accepting values such as `30s`, `5m`,
  or `1h 15m` via the `humantime` crate
- `url` parses `url::Url` values using the `url` crate

```toml
[dependencies]
confroid = { version = "0.0.2", features = ["humantime", "url"] }
```

Duration parsing is explicitly enabled per field:

```rust
#[derive(confroid::Config)]
struct Config {
    #[confroid(humantime)]
    timeout: std::time::Duration,
}
```

Scalars are parsed via [`FromStr`].
To read a custom scalar type — an enum, a newtype, anything with a `FromStr` —
implement `FromStr` for it and derive `ConfigValue`:

```rust
use std::str::FromStr;

/// How chatty the logs are.
#[derive(confroid::ConfigValue, Debug, Clone, Copy)]
enum LogLevel {
    Error,
    Warn,
    Info,
    Debug,
    Trace,
}

impl FromStr for LogLevel {
    type Err = std::io::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_ascii_lowercase().as_str() {
            "error" => Ok(Self::Error),
            "warn" => Ok(Self::Warn),
            "info" => Ok(Self::Info),
            "debug" => Ok(Self::Debug),
            "trace" => Ok(Self::Trace),
            other => Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!("unknown log level `{other}`"),
            )),
        }
    }
}

#[derive(confroid::Config)]
struct Config {
    #[confroid(default = LogLevel::Info)]
    log_level: LogLevel,
}
```

so `LOG_LEVEL=debug` parses into `LogLevel::Debug`.
The `FromStr` error must implement `std::error::Error + Send + Sync + 'static`;
confroid preserves it as the source of an invalid-value error.
With the `docs` feature, a `ConfigValue` must also implement `Display` so defaults and examples can be rendered.

The existing `confroid::from_str_scalar!(Type)` macro remains available
when adding a derive at the type definition is not convenient.

A full runnable version is in [`examples/enum_config.rs`](crates/confroid/examples/enum_config.rs).
(If you enable the `docs` feature, also implement `Display` for the type so its default and example can be rendered.)

## Generating documentation

Enable the `docs` feature:

```toml
[dependencies]
confroid = { version = "0.0.2", features = ["docs"] }
```

and call one of the generators (using the `Config` struct from the quick-start example):

```rust
// A `.env.example` style file.
println!("{}", confroid::env_example::<Config>());
// A Markdown table.
println!("{}", confroid::markdown_table::<Config>());
```

`env_example::<Config>()` produces:

```bash
# Config configuration

# The HTTP configuration.

# The port to listen on. (default: 8080)
# HTTP__PORT=8080
# The host to listen on. (example: localhost)
HTTP__HOST=

# The greeting to use. (default: Hello)
# GREETING=Hello

# The name of the user. (default: World, example: John)
# NAME=World
```

Optional fields and fields with defaults are emitted as commented-out assignments.
Required fields remain active.

`markdown_table::<Config>()` produces:

| Variable     | Description            | Default | Example     |
| ------------ | ---------------------- | ------- | ----------- |
| `HTTP__PORT` | The port to listen on. | `8080`  |             |
| `HTTP__HOST` | The host to listen on. |         | `localhost` |
| `GREETING`   | The greeting to use.   | `Hello` |             |
| `NAME`       | The name of the user.  | `World` | `John`      |

Default and example annotations are rendered only for scalar fields.
`Vec` fields appear as a single variable line without them.
`HashMap` fields use `<key>` to show where the map key belongs;
maps of structs also include the value struct's nested fields.

## Errors

`from_env` returns `Result<T, ConfroidError>`.
Every error names both the environment variable and the dotted field path:

```rust
// A required variable is not set.
ConfroidError::EnvVarNotFound { var_name: "HTTP__PORT", field: "http.port" }

// A variable is set but its value cannot be parsed.
ConfroidError::EnvVarInvalid { var_name: "HTTP__PORT", field: "http.port", value: "abc", parser_error: /* ... */ }

// A vector has a gap in its indices.
ConfroidError::VecIndexGap { var_name: "NAMES", field: "names", missing: 1 }
```

## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option.

[`Default::default()`]: https://doc.rust-lang.org/std/default/trait.Default.html
[`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html