provcfg
A Rust config loader that tracks where each value came from.
provcfg (provenance config) layers configuration sources (compiled-in
defaults, files, environment variables, CLI flags) into one struct, and keeps
the provenance of every leaf field: which source set the active value, and
which earlier sources it overrode.
Choosing between provcfg and config
For most projects the config crate is the
better fit. It is mature, supports more formats, and is less ceremony if all
you need is the merged result.
Use provcfg when you need to know which source set each value, for example:
- a settings page that labels each field "from file" / "from env" / "default";
- diagnosing "why is this value set to that?" across layered sources;
- an admin UI that renders the effective config together with its origin.
If you never ask "where did this come from?", prefer config.
What it does
Derive Configurable on a plain config struct. Config::build returns a
companion *Prov struct whose every leaf is a ValueHistory: the value paired
with the Source it came from.
use ;
// `APP_HOST=db.internal` is set in the environment; `APP_PORT` is not.
let settings = new
.add_env
.
.unwrap;
assert_eq!;
assert_eq!;
// `port` was set by nobody, so it falls back to the compiled-in default.
assert_eq!;
assert_eq!;
Layering sources
Sources are applied in the order they are added. A later source overrides an earlier one for any leaf it sets. Unset leaves keep the earlier value, and the overridden values stay in each leaf's history.
// `from_cli` is a `SettingsPartial` (every field an `Option`) produced by a
// CLI parser; here it carries only `port = 9090`.
let settings = new
.add_toml_str
.add_env // environment has APP_HOST=db.internal
.add_cli // command line had --port 9090
.
.unwrap;
assert_eq!; // env overrode the file
assert_eq!;
assert_eq!; // cli overrode the file
assert_eq!;
Inspecting provenance
sources_map returns a flat dotted.path -> Category map for a settings UI:
let map = settings.sources_map;
assert_eq!;
assert_eq!;
walk_leaves visits every leaf with its value, category, and a secret flag, so
an effective-config view can redact sensitive fields:
settings.walk_leaves;
Field attributes
Key/value sources
KeyValueSource takes flat dotted.key -> string entries, such as the rows
of a settings table, and reports Category::Database. Values are converted to
the field type the same way env values are. It is always available and has no
dependencies; load the entries yourself and hand them over:
use KeyValueSource;
let rows = ;
let cfg = new
.add_source
.
.unwrap;
With the json feature, with_json_values() lets one entry hold a list, map,
struct or enum with data as JSON, such as hosts = [{"name": "a"}] for a
Vec<Host>. Scalars and strings stay plain text. EnvSource has the same
option, for APP_HOSTS='[{"name": "a"}]'.
Database sources
The provcfg-sea-orm crate reads settings from SQLite or PostgreSQL through
SeaORM. Settings live in a key/value table,
provcfg_settings (namespace, dotted key, text value), created by the
migrations in provcfg_sea_orm::migration::migrations(). load turns one
namespace into a KeyValueSource:
let cfg = new
.add_toml_file?
.add_source
.add_env
.?;
Enable the sqlite and/or postgres feature of provcfg-sea-orm. It selects
no async runtime or TLS backend; enable one (for example runtime-tokio-rustls)
on your own sea-orm dependency.
For a config that follows the database at runtime, use LiveSettings. It
places the database layer where you want it, writes values back after
validating them, and with the watch feature applies changes made elsewhere
(PostgreSQL LISTEN/NOTIFY, revision polling on SQLite):
use ;
let settings = new
.await?;
spawn;
let outcome = settings.set.await?; // validated, stored, applied
for in outcome.hidden
let level = settings.load.log_level.value.clone;
Writes reject unknown paths and values that do not convert to the field type,
and report when env or CLI still override the written value. Strings and
numbers are stored as plain text, lists, maps and structs as JSON
(set_value(path, &value) serializes them), secrets included.
LiveSettings::builder adds rules for the whole config and limits what the
database may set. A batch writes several keys in one transaction, optionally
only if nobody changed the namespace since it was read:
let settings = builder
.read_only // needed before the database, or at startup only
.validator // runs on every write, refresh and watch
.build
.await?;
let revision = settings.revision; // e.g. sent with a settings form
settings
.batch
.set_value
.unset
.expect_revision // Error::Conflict if the namespace changed meanwhile
.commit
.await?;
A write that fails any check stores nothing. Rows changed by other means that fail them are not applied; the previous config stays current.
Live reloading
With the live feature, LiveConfig rebuilds the config at runtime without a
restart. Wrap any source whose data can change in a DynamicSource, keep a
clone, and replace its data when it changes:
use ;
let db = new;
let live = new?;
live.on_change;
// Later, when the settings table changed:
db.replace;
live.reload?;
let port = *live.load.port.value;
load()returns a consistent snapshot; a reload swaps the whole config at once.- Files, environment variables and CLI arguments keep what they captured when
they were added, and keep their priority. A database change that env or CLI
overrides is recorded in
history()but not reported as a change. - A reload that fails to deserialize leaves the previous config in place.
LiveConfig::with_validatorchecks every build with a function on the whole config; a rejected reload also keeps the previous config.LiveConfig::preview(&dynamic, data)builds the config as a replacement ofdynamicwould make it, without changing anything, to check data before it is stored.
Custom sources
To support a format the built-ins don't cover (XML, a secret store), implement
the Source trait. Most sources build something that implements
serde::Deserializer (a serde_json::Value works fine) and erase it through
erased_serde:
use erased_serde;
use ;
let cfg = new
.add_source
.
.unwrap;
Two contract notes:
Source::deserializeis called synchronously fromConfig::build. I/O and async work belong in the source's constructor: fetch onnew, store the materialized data, hand it out on demand.- Convert your own error into
erased_serde::Errorvia<erased_serde::Error as serde::de::Error>::custom(my_error).Config::buildwraps that inError::Deserializewith the source'sname().
See the Source trait's rustdoc for the full doctested example.
Cargo features
Only env is enabled by default; opt into the file and CLI sources you need.
| Feature | Enables | Extra dependency |
|---|---|---|
env |
EnvSource / add_env (default) |
none |
json |
JsonStr / add_json_*, JSON values in key/value and env sources |
serde_json |
toml |
TomlStr / add_toml_* |
toml |
cli |
CliSource / add_cli |
serde_json |
live |
LiveConfig / DynamicSource |
arc-swap, serde_json |
clap-derive |
ClapArgs derive (provcfg-clap) |
clap |
[]
= { = "0.1", = ["toml", "cli"] }
clap integration
With the clap-derive feature, derive ClapArgs alongside Configurable to
generate a clap-compatible args struct that flows straight into a CLI source.
See the provcfg-clap crate.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.