`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
});
# }
```
### 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
}
let config = Config::builder()
.try_build()
.unwrap();
assert_eq!(config.data.a, 1);
let toml = r#"
[data]
a = 1234
"#;
let config = Config::builder()
.override_with(TomlSource::new(toml))
.try_build()
.unwrap_err();
let toml = r#"
[data]
a = 1234
b = 4321
"#;
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).