Skip to main content

Crate confik

Crate confik 

Source
Expand description

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:

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

and your deployment injects the secret through the environment:

PASSWORD=hunter2

then confik can merge both into one typed config object:

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.

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:

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, .ron, .yaml, or .yml files based on the file extension. Requires the matching toml, json, 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.
  • 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:

#[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:

#[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:

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

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

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

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

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.

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

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.

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

#[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:

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

impl std::str::FromStr for MyEnum {
    // ...
}

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.

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

impl std::str::FromStr for MyEnum {
    // ...
}

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 and BTreeMap. Whilst the implementations can be provided fully, there are helpers available. These are the KeyedContainerBuilder type and the KeyedContainer trait.

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

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

§Unkeyed Containers

Unkeyed containers are types without a separate key. This includes Vec, but also types like HashSet. Whilst the implementations can be provided fully, there is a helper available. This is the UnkeyedContainerBuilder.

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

§Other complex cases

For other complex cases, where derives 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).

Modules§

commoncommon
Useful configuration types that services will likely otherwise re-implement.
helpers
Utilities for manual implementations of Configuration.
humantimehumantime
jiffjiff-0_2

Structs§

ConfigBuilder
Used to accumulate ordered sources from which its Target is to be built.
EnvSourceenv
A Source referring to environment variables.
FailedTryInto
Captures the path and error of a failed conversion.
FileSource
A Source referring to a file path.
JsonSourcejson
A Source containing raw JSON data.
MissingValue
Captures the path of a missing value.
OffsetSource
A Source containing another source that can build the target at an offset determined by the provided path.
ReloadingConfigreloading
An instance of config that may reload itself.
RonSourceron-0_12
A Source containing raw RON data.
SecretBuilder
Wrapper type for carrying secrets, auto-applied to builders when using the #[config(secret)] attribute.
SecretOption
Builder for trivial types that always contain secrets, regardless of the presence of #[confik(secret)] annotations.
TomlSourcetoml
A Source containing raw TOML data.
UnexpectedSecret
Captures the path of a secret found in a non-secret source.
YamlSourceyaml_serde-0_10
A Source containing raw YAML data.

Enums§

Error
Possible error values.

Traits§

Configuration
The target to be deserialized from multiple sources.
ConfigurationBuilder
A builder for a multi-source config deserialization.
ReloadCallbackreloading
Trait for invoking reload callbacks.
ReloadableConfigreloading
Defines how to create a new instance of ReloadingConfig.
Source
A source of configuration data.