confik 0.7.0

A library for reading application configuration split across multiple sources
Documentation

confik

confik is a library for reading application configuration split across multiple sources.

Example

Assume that config.toml contains:

host=google.com
username=root

and the environment contains:

PASSWORD=hunter2

then:

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

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

Sources

A [Source] is any type that can create [ConfigurationBuilder]s. This crate implements the following sources:

  • [EnvSource]: Loads configuration from environment variables using the [envious] crate. Requires the env feature. (Enabled by default.)
  • [FileSource]: Loads configuration from a file, detecting json or toml files based on the file extension. Requires the json and toml feature respectively. (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.

Secrets

Fields annotated with #[confik(secret)] will only be read from secure sources. This serves as a runtime check that no secrets have been stored in insecure places such as world-readable files.

If a secret is found in an insecure source, an error will be returned. You can opt into loading secrets on a source-by-source basis.

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.

  • with-chrono - chrono 0.4
  • with-rust_decimal-1 - rust_decimal 1
  • with-url - url 1
  • with-uuid - uuid 1

Macro usage

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

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

Forwarding Attributes To Deserialize

The serde attributes used for customizing a Deserialize derive typically are achieved by adding #[confik(forward_serde(...)) attributes.

For example:

#[derive(confik::Configuration)]
struct Config {
#[confik(forward_serde(rename = "other_data"))]
data: usize,
}

Defaults

Defaults are specified on a per-field basis.

  • Defaults are used if the data cannot be fully read, even if it is partially read. E.g., even if data in the below example has one value read in, both will be overwritten by the default.
# #[cfg(feature = "toml")]
# {
use confik::{Configuration, TomlSource};

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

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

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

let config = Config::builder()
.override_with(TomlSource::new(toml))
.try_build()
.unwrap();
assert_eq!(config.data.a, 1);

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

Handling Foreign Types

If there's a foreign type used in your config, then you will not be able to implement [Configuration] for it. Instead any type that implements [Into] 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 Config {
#[confik(from = "MyForeignTypeCopy")]
foreign_data: ForeignType
}

Macro Limitations

Option Defaulting

Options cannot default to anything other than None. I.e., the below example ignores the provided default.

# use confik::Configuration;

const DEFAULT_DATA: Option<usize> = Some(5);

#[derive(Configuration)]
struct Config {
#[confik(default = "DEFAULT_DATA")]
data: Option<usize>
}

let config = Config::builder().try_build().unwrap();
assert_eq!(config.data, None);

This behaviour occurs due to Options needing to have a default value of None, as Optional configuration shouldn't be required. This defaulting occurs inside the [ConfigurationBuilder] implementation of [Option] and so happens before the macro can try to default the value.

This is in principle fixable by special casing any value with an Option<...> type, but this has not been implemented due to the fragility that trying to exact match on the string value of a type in a macro would bring. E.g., a custom type such as type MyOption = Option<usize> would then behave differently to using Option<usize> directly.

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 {
// ...
# 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].