Skip to main content

Crate config_kit

Crate config_kit 

Source
Expand description

Layered typed configuration for Rust.

config-kit centralizes the estate’s shared configuration pattern: serde-typed settings assembled from layered sources — a base file, then environment variables, then in-process overrides — with unknown-key denial at the top level, redacted secrets, and optional hot reload.

§Design

  • Typed and layered. You describe the target type; layers produce values that deep-merge key-by-key in the order added. Driven in the documented order, precedence is overrides > env > file: later layers win on colliding keys, and table contents merge so a late layer can override one nested field without clobbering siblings.
  • Strict by choice. ConfigBuilder::load ignores unknown keys; ConfigBuilder::load_strict denies unconsumed top-level keys with a typed error listing them — container-agnostically, for any DeserializeOwned type, without derive support (see load_strict for the exact contract).
  • Secrets are redacted by construction. Sensitive<T> renders "[REDACTED]" for Debug and Display (bound-free impls, so derived container Debugs cannot leak either) and requires an explicit, grep-friendly .expose() to read.
  • Formats. TOML is core. JSON (json feature) and YAML (yaml feature) are optional; format is picked by file extension, defaulting to TOML.
  • Hot reload, opt-in. The hot-reload feature adds [watch]: a 300 ms-debounced, notify-backed watcher that feeds freshly parsed values to your callback and records reload failures instead of panicking.
  • Tier-A hygiene. #![forbid(unsafe_code)], #![deny(missing_docs)], clippy unwrap_used/expect_used/panic/indexing_slicing denied — the public surface is fallible and typed, never panicking.

§Example

File layer omitted here so the example runs stand-alone; combine all three layers for the documented precedence:

use config_kit::{ConfigBuilder, ConfigLayer, Sensitive};
use serde::Deserialize;
use std::collections::BTreeMap;

#[derive(Deserialize)]
struct Service {
    port: u16,
    debug: bool,
    token: Sensitive<String>,
}

std::env::set_var("CKIT_EXAMPLE_SERVICE_TOKEN", "hunter2");
std::env::set_var("CKIT_EXAMPLE_SERVICE_DEBUG", "true");
let mut over: BTreeMap<String, toml::Value> = BTreeMap::new();
over.insert("port".into(), toml::Value::Integer(9090));

let service: Service = ConfigBuilder::new()
    .layer(ConfigLayer::env_prefix("CKIT_EXAMPLE_SERVICE_"))
    .layer(ConfigLayer::overrides(over))
    .load()?;

assert_eq!(service.port, 9090); // overrides beat env
assert!(service.debug);
assert_eq!(service.token.expose(), "hunter2"); // the only way in
assert_eq!(format!("{:?}", service.token), "[REDACTED]");

§Strict loading

ConfigBuilder::load_strict rejects typo’d keys at the top level:

use config_kit::{ConfigBuilder, ConfigLayer};
use serde::Deserialize;
use tempfile::TempDir;

#[derive(Deserialize, Debug)]
struct Db {
    url: String,
}
#[derive(Deserialize, Debug)]
struct Settings {
    db: Db,
}

let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("settings.toml");
std::fs::write(
    &path,
    "db = { url = \"postgres://localhost\" }\ntypo_key = 1\n",
)
.expect("write");

let error = ConfigBuilder::new()
    .layer(ConfigLayer::file(&path))
    .load_strict::<Settings>()
    .expect_err("typo_key is not a field");
assert!(error.to_string().contains("typo_key"), "{error}");

§Environment typing

Env values are inferred: true/false → bool, integer → i64, float → f64, else verbatim string; keys are prefix-stripped and lowercased (APP_PORT → port). See ConfigLayer::env_prefix.

§Hot reload

With the hot-reload feature, watch spawns a 300 ms-debounced, notify-backed watcher that parses the file on settle and hands the fresh value to your callback; reload failures are recorded (never panic) and retrievable via ConfigWatcher::last_error, and ConfigWatcher::stop joins the worker. See the watch function’s documentation for a runnable example.

Structs§

ConfigBuilder
Builder for layered, typed configuration.
Empty
Builder state: no layers added yet.
Ready
Builder state: at least one layer has been added, so the layer stack is meaningful and loading is available.
Sensitive
Wraps a secret so Debug and Display always render "[REDACTED]" while the value stays usable through Sensitive::expose.

Enums§

ConfigError
Loading, parsing, strict-validation, and watching failures.
ConfigLayer
One named source of configuration values.