# `confik`
`confik` is a library for reading application configuration split across multiple sources.
## Example
Assume that `config.toml` contains:
```toml
host=google.com
username=root
```
and the environment contains:
```bash
PASSWORD=hunter2
```
then:
```no_run
# #[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.
## 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 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.
```
# #[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.,
```
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
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.
- `bigdecimal`: v0.4
- `bytesize`: v1
- `camino`: v1
- `chrono`: v0.4
- `ipnetwork`: v0.20
- `rust_decimal`: v1
- `secrecy`: v0.8 (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,
}
```
## 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`].