confik 0.15.12

Compose configuration from multiple sources without giving up type safety
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
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
`confik` is a configuration library for Rust applications that need to compose settings from multiple sources without giving up type safety.

It is built for the common production path: read defaults from code, layer in config files, override with environment variables, keep secrets out of insecure sources, and build one strongly typed config value for the rest of your application.

## Built for Real App Config

- **Derive-first API** -- define your config once and get a builder that merges partial values from many sources.
- **Multi-source by design** -- combine files, environment variables, and inline formats in a predictable override order.
- **Secret-aware loading** -- mark sensitive fields and opt into reading them only from trusted sources.
- **Production-friendly features** -- support hot reloading and SIGHUP-triggered refreshes when your application needs them.
- **Serde ecosystem compatibility** -- reuse familiar `serde` attributes and common third-party config value types.

## Example

Assume your application ships with a `config.toml` file:

```toml
host = "duck.com"
username = "root"
```

and your deployment injects the secret through the environment:

```bash
PASSWORD=hunter2
```

then `confik` can merge both into one typed config object:

```no_run
# #[cfg(all(feature = "toml", feature = "env"))]
# {
use confik::{Configuration, EnvSource, FileSource};

#[derive(Debug, PartialEq, Configuration)]
struct Config {
    host: String,
    username: String,

    #[confik(secret)]
    password: String,
}

let config = Config::builder()
    .override_with(FileSource::new("config.toml"))
    .override_with(EnvSource::new().allow_secrets())
    .try_build()
    .unwrap();

assert_eq!(
    config,
    Config {
        host: "google.com".to_string(),
        username: "root".to_string(),
        password: "hunter2".to_string(),
    }
);
# }
```

## Hot Reloading

Configuration can be made hot-reloadable using the [`ReloadingConfig`] wrapper. This lets you atomically swap configuration at runtime without restarting your application. Requires the `reloading` feature.

```no_run
# #[cfg(all(feature = "toml", feature = "reloading"))]
# {
use confik::{Configuration, ReloadableConfig, FileSource};

#[derive(Debug, Configuration)]
struct AppConfig {
    host: String,
    port: u16,
}

impl ReloadableConfig for AppConfig {
    type Error = confik::Error;

    fn build() -> Result<Self, Self::Error> {
        Self::builder()
            .override_with(FileSource::new("config.toml"))
            .try_build()
    }
}

// Create a reloading config
let config = AppConfig::reloading().unwrap();

// Access the current config (cheap, non-blocking)
let current = config.load();
println!("Host: {}", current.host);

// Reload from sources
config.reload().unwrap();

// Add a callback for reload notifications
let config = config.with_on_update(|| {
    println!("Config reloaded!");
});
# }
```

### Signal Handling

When the `signal` feature is enabled (requires `reloading`), you can also set up automatic reloading on SIGHUP:

```no_run
# #[cfg(all(feature = "signal", feature = "toml"))]
# {
# use confik::{Configuration, ReloadableConfig, FileSource};
# #[derive(Debug, Configuration)]
# struct AppConfig { host: String, port: u16 }
# impl ReloadableConfig for AppConfig {
#     type Error = confik::Error;
#     fn build() -> Result<Self, Self::Error> {
#         Self::builder().override_with(FileSource::new("config.toml")).try_build()
#     }
# }
let config = AppConfig::reloading().unwrap();
let handle = config.spawn_signal_handler().unwrap();

// Config will now reload when receiving SIGHUP
// Send SIGHUP: kill -HUP <pid>
# }
```

When the `tracing` feature is enabled, reload errors in the signal handler are automatically logged with `tracing::error!`.

## Sources

A [`Source`] is anything that can produce a partial [`ConfigurationBuilder`]. `confik` ships with the following source types:

- [`EnvSource`]: Loads configuration from environment variables using the [`envious`] crate. Requires the `env` feature. (Enabled by default.)
- [`FileSource`]: Loads configuration from a file, detecting `.toml`, `.json`, `.corn`, `.ron`, `.yaml`, or `.yml` files based on the file extension. Requires the matching `toml`, `json`, `corn-0_10`, `ron-0_12`, or `yaml_serde-0_10` feature. (`toml` is enabled by default.)
- [`TomlSource`]: Loads configuration from a TOML string literal. Requires the `toml` feature. (Enabled by default.)
- [`JsonSource`]: Loads configuration from a JSON string literal. Requires the `json` feature.
- [`CornSource`]: Loads configuration from a Corn string literal. Requires the `corn-0_10` feature.
- [`RonSource`]: Loads configuration from a RON string literal. Requires the `ron-0_12` feature.
- [`YamlSource`]: Loads configuration from a YAML string literal. Requires the `yaml_serde-0_10` feature.
- [`OffsetSource`]: Loads configuration from an inner source that is provided to it, but applied to a particular offset of the root configuration builder.

## Secrets

Fields annotated with `#[confik(secret)]` are only read from sources that explicitly allow secrets. This gives you a runtime guard against accidentally loading sensitive data from insecure locations such as world-readable config files.

If secret data is found in an insecure source, `confik` returns an error. You opt into secret loading on a source-by-source basis, which keeps the trust boundary explicit in application code.

## Macro usage

The derive macro is called `Configuration` and is used as normal:

```rust
#[derive(confik::Configuration)]
struct Config {
    data: usize,
}
```

### Forwarding Attributes

This allows forwarding any kind of attribute on to the builder.

#### Serde

The serde attributes used for customizing a `Deserialize` derive are achieved by adding `#[confik(forward(serde(...)))]` attributes.

For example:

```rust
# use confik::Configuration;
#[derive(Configuration, Debug, PartialEq, Eq)]
struct Field {
    #[confik(forward(serde(rename = "other_name")))]
    field1: usize,
}
```

#### Derives

If you need additional derives for your type, these can be added via `#[confik(forward(derive...))]` attributes.

For example:

```rust
# use confik::Configuration;
#[derive(Debug, Configuration, Hash, Eq, PartialEq)]
#[confik(forward(derive(Hash, Eq, PartialEq)))]
struct Value {
    inner: String,
}
```

### Defaults

Defaults are specified on a per-field basis.

- Defaults only apply if no data has been read for that field. E.g., if `data` in the below example has one value read in, it will return an error.

  ```rust
  # #[cfg(feature = "toml")]
  # {
  use confik::{Configuration, TomlSource};

  #[derive(Debug, Configuration)]
  struct Data {
      a: usize,
      b: usize,
  }

  #[derive(Debug, Configuration)]
  struct Config {
      #[confik(default = Data  { a: 1, b: 2 })]
      data: Data
  }

  // Data is not specified, the default is used.
  let config = Config::builder()
      .try_build()
      .unwrap();
  assert_eq!(config.data.a, 1);

  let toml = r#"
      [data]
      a = 1234
  "#;

  // Data is partially specified, but is insufficient to create it. The default is not used
  // and an error is returned.
  let config = Config::builder()
      .override_with(TomlSource::new(toml))
      .try_build()
      .unwrap_err();

  let toml = r#"
      [data]
      a = 1234
      b = 4321
  "#;

  // Data is fully specified and the default is not used.
  let config = Config::builder()
      .override_with(TomlSource::new(toml))
      .try_build()
      .unwrap();
  assert_eq!(config.data.a, 1234);
  # }
  ```

- Defaults can be given by any rust expression, and have [`Into::into`] run over them. E.g.,

  ```rust
  const DEFAULT_VALUE: u8 = 4;

  #[derive(confik::Configuration)]
  struct Config {
      #[confik(default = DEFAULT_VALUE)]
      a: u32,
      #[confik(default = "hello world")]
      b: String,
      #[confik(default = 5f32)]
      c: f32,
  }
  ```

- Alternatively, a default without a given value called [`Default::default`]. E.g.,

  ```rust
  use confik::{Configuration};

  #[derive(Configuration)]
  struct Config {
      #[confik(default)]
      a: usize
  }

  let config = Config::builder().try_build().unwrap();
  assert_eq!(config.a, 0);
  ```

### `struct_default`

On **struct** configuration types only, you can mark individual fields with `#[confik(struct_default)]`. If no data was merged for that field, `try_build` uses the value of the same field from `<Self as Default>::default()` instead of requiring an explicit `#[confik(default)]` expression.

If any field uses `struct_default`, `try_build` always calls `<Self as Default>::default()` **once** at the **start** of the build—even when every such field ultimately receives merged data from sources. Values for missing fields are taken from that single result (see below), not by calling `default()` separately per field.

That is the **config struct's** [`Default`] implementation, not the field type's `Default` alone. For example, `port: u16` with `struct_default` uses whatever `Config::default().port` is (e.g. `8080`), not necessarily `u16::default()` (`0`).

This is useful when your config type already implements [`Default`] and you want missing keys to match those defaults field by field, without duplicating values in `#[confik(default = ...)]`.

- You cannot use `#[confik(struct_default)]` together with `#[confik(default)]` on the same field.
- It is not allowed on fields inside enum variants.
- With `#[confik(skip)]`, `struct_default` supplies the built value from `<Self as Default>::default()` for that field (the type need not implement `Configuration` or `Deserialize`).
- With `#[confik(from = ...)]` or `#[confik(try_from = ...)]`, a missing value still comes from the **target** field on `Default` (the same type as the struct field). When sources provide data, deserialization and conversion use the intermediate type as usual.
- With `#[confik(default = ...)]` and `from` / `try_from`, the missing-data branch uses the same rule: write the default as the **target** field type (e.g. `default = String::from("…")` for a `String` field). It is not the intermediate deserialized type.
- `struct_default` does **not** clone field values: it **moves** each missing field out of the `default()` value described above. That means it does not work when your **config struct** implements [`Drop`], because Rust does not allow moving out of individual fields of a type that implements `Drop` (you would see a move-out-of-struct-with-Drop error at compile time). Non-[`Copy`] field types do not need [`Clone`] for this path.

```rust
use confik::Configuration;

#[derive(Debug, PartialEq, Eq, Configuration)]
struct Config {
    #[confik(struct_default)]
    port: u16,
    #[confik(default = String::from("localhost"))]
    host: String,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            // Not `u16::default()` (0): missing `port` in sources uses this value.
            port: 8080,
            host: String::new(),
        }
    }
}

let config = Config::builder().try_build().unwrap();
assert_eq!(
    config,
    Config {
        port: 8080,
        host: String::from("localhost"),
    }
);
```

### Handling Foreign Types

This crate provides implementations of [`Configuration`] for a number of `std` types and the following third-party crates. Implementations for third-party crates are feature gated.

- `ahash`: v0.8
- `bigdecimal`: v0.4
- `bytesize`: v2
- `camino`: v1
- `chrono`: v0.4
- `ipnetwork`: v0.21
- `js_option`: v0.1
- `rust_decimal`: v1
- `secrecy`: v0.10 (Note that `#[config(secret)]` is not needed, although it is harmless, for these types as they are always treated as secrets.)
- `url`: v1
- `uuid`: v1

If there's another foreign type used in your config, then you will not be able to implement [`Configuration`] for it. Instead any type that implements [`Into`] or [`TryInto`] can be used.

```rust
struct ForeignType {
    data: usize,
}

#[derive(confik::Configuration)]
struct MyForeignTypeCopy {
    data: usize
}

impl From<MyForeignTypeCopy> for ForeignType {
    fn from(copy: MyForeignTypeCopy) -> Self {
        Self {
            data: copy.data,
        }
    }
}

#[derive(confik::Configuration)]
struct MyForeignTypeIsize {
    data: isize
}

impl TryFrom<MyForeignTypeIsize> for ForeignType {
    type Error = <usize as TryFrom<isize>>::Error;

    fn try_from(copy: MyForeignTypeIsize) -> Result<Self, Self::Error> {
        Ok(Self {
            data: copy.data.try_into()?,
        })
    }
}

#[derive(confik::Configuration)]
struct Config {
    #[confik(from = MyForeignTypeCopy)]
    foreign_data: ForeignType,

    #[confik(try_from = MyForeignTypeIsize)]
    foreign_data_isized: ForeignType,
}
```

### Named builders

If you want to directly access the builders, you can provide them with a name. This will also place the builder in the local module, to ensure there's a known path with which to reference them.

```rust
#[derive(confik::Configuration)]
#[confik(name = Builder)]
struct Config {
    data: usize,
}

let _ = Builder { data: Default::default() };
```

### Field and Builder visibility

Field and builder visibility are directly inherited from the underlying type. E.g.

```rust
use confik::helpers::BuilderOf;

mod config {
    #[derive(confik::Configuration)]
    pub struct Config {
        pub data: usize,
    }
}

let _ = BuilderOf::<config::Config> { data: Default::default() };
```

### Skipping fields

Fields can be skipped if necessary. This allows having types that cannot implement `Configuration` or be deserializable. The field must still get a value at `try_build` time: use `#[confik(default)]`, `#[confik(default = ...)]`, or `#[confik(skip, struct_default)]` on a struct whose [`Default`] implementation sets that field. E.g.

```rust
# use std::time::Instant;
#[derive(confik::Configuration)]
struct Config {
  #[confik(skip, default = Instant::now())]
  loaded_at: Instant,
}
```

### Specifying `confik` Base

Specify a path to the `confik` crate instance to use when referring to `confik` APIs from generated code. This is normally only applicable when invoking re-exported `confik` derives from a public macro in a different crate or when renaming `confik` in your Cargo manifest.

```rust,ignore
# use std::time::Instant;
#[derive(confik::Configuration)]
#[confik(crate = reexported_confik)]
struct Config {
  // ...
}
```

## Macro Limitations

### Custom `Deserialize` Implementations

If you're using a custom `Deserialize` implementation, then you cannot use the `Configuration` derive macro. Instead, define the necessary config implementation manually like so:

```rust
#[derive(Debug, serde_with::DeserializeFromStr)]
enum MyEnum {
    Foo,
    Bar,
};

impl std::str::FromStr for MyEnum {
    // ...
# type Err = String;
# fn from_str(_: &str) -> Result<Self, Self::Err> { unimplemented!() }
}

impl confik::Configuration for MyEnum {
    type Builder = Option<Self>;
}
```

Note that the `Option<Self>` builder type only works for simple types. For more info, see the docs on [`Configuration`] and [`ConfigurationBuilder`].

## Manual implementations

It is strongly recommended to use the `derive` macro where possible. However, there may be cases where this is not possible. For some cases there are additional attributes available in the `derive` macro to tweak the behaviour, see the section on Handling Foreign Types.

If you would like to manually implement `Configuration` for a type anyway, then this can mostly be broken down to three cases.

### Simple cases

If your type cannot be partial specified (e.g. `usize`, `String`), then a simple `Option<Self>` builder can be used.

```rust
#[derive(Debug, serde_with::DeserializeFromStr)]
enum MyEnum {
    Foo,
    Bar,
};

impl std::str::FromStr for MyEnum {
    // ...
# type Err = String;
# fn from_str(_: &str) -> Result<Self, Self::Err> { unimplemented!() }
}

impl confik::Configuration for MyEnum {
    type Builder = Option<Self>;
}
```

### Containers

Unless your container holds another container, which already implements `Configuration`, you'll likely need to implement `Configuration` yourself, instead of with a `derive`. There are two type of containers that may need to be handled here.

#### Keyed Containers

Keyed containers have their contents separate from their keys. Examples of these are [`HashMap`](std::collections::HashMap) and [`BTreeMap`](std::collections::BTreeMap). Whilst the implementations can be provided fully, there are helpers available. These are the [`KeyedContainerBuilder`][KeyedContainerBuilder] type and the [`KeyedContainer`][KeyedContainer] trait.

A type which implements all of [`KeyedContainer`][KeyedContainer], [`Deserialize`][serde::Deserialize], [`FromIterator`], [`Default`], and [`IntoIterator`] (for both the type and a reference to the type) can then use [`KeyedContainerBuilder`][KeyedContainerBuilder] as their builder. See [`KeyedContainerBuilder`][KeyedContainerBuilder] for an example.

Note that the key needs to implement `Display` so that an accurate error stack can be generated.

[KeyedContainerBuilder]: crate::helpers::KeyedContainerBuilder
[KeyedContainer]: crate::helpers::KeyedContainer

#### Unkeyed Containers

Unkeyed containers are types without a separate key. This includes [`Vec`], but also types like [`HashSet`](std::collections::HashSet). Whilst the implementations can be provided fully, there is a helper available. This is the [`UnkeyedContainerBuilder`][UnkeyedContainerBuilder].

A type which implements all of [`Deserialize`][serde::Deserialize], [`FromIterator`], [`Default`], and [`IntoIterator`] (for both the type and a reference to the type) can then use [`UnkeyedContainerBuilder`][UnkeyedContainerBuilder] as their builder. See [`UnkeyedContainerBuilder`][UnkeyedContainerBuilder] for an example.

[UnkeyedContainerBuilder]: crate::helpers::UnkeyedContainerBuilder

#### Other complex cases

For other complex cases, where `derive`s cannot work, the type is not simple enough to use an `Option<Self>` builder, and is not a container, there is currently no additional support. Please read through the [`Configuration`] and [`ConfigurationBuilder`] traits and implement them as appropriate.

If you believe your type is following a common pattern where we could provide more support, please raise an issue (or even better an MR).