Limitations & Not Planned
Several refusals below are stated against Go's Viper, because that is where the behaviour being refused is best known. What this project took from it is in CREDITS.md.
Limitations
- Every top-level key in a config file must be a table, with one exception:
$schema, so a JSON file can point at the schema that describes it. Every top-level key names a section, and a section is a table of settings, so a stray"_comment": "..."at the top level is an error — one that names the key and says why. - TOML datetimes are not modelled and deserialize as a table.
- The macro refers to the crate as
::dynamic_config, so renaming the dependency is not supported. - Error messages name the environment prefix —
APP_DB_*— when the exact variable cannot be confirmed. The variable is derived from the key path and used when it really exists; an aliased value, whose variable spells the old name, falls back to the prefix rather than naming a variable nobody set.
Not planned
Each of these is a real request with a real answer. They are refused rather than unbuilt, so that nobody spends an afternoon discovering the reason — and each says what would reopen it.
A ReloadExecutor / ReloadPolicy abstraction
Exists under another name: set_blocking_executor already steers where the
blocking half of a reload runs, and the tokio feature installs the blocking
pool. A second abstraction over the same choice would be a synonym. Reopened
by a steering need the executor hook cannot express.
on_reload_async
changes() is the async reload event — a Future on your executor, free
to await whatever the reaction needs. See
The Reload Lifecycle. Reopened by nothing; this is
what changes() is for.
Splitting the core into -core/-watch/-schema crates
Feature flags already give the isolation a split would: disable a feature and the dependency is gone. A crate split adds a version matrix to maintain without removing anything anyone is forced to take. Reopened by a feature whose dependency cannot be made optional.
Fewer official store backends
The eight exist, are tested against real servers, and are Beta as of 0.6.1. Withdrawing shipped crates punishes their users to save unshipped maintenance. Revisited per crate if one's client dependency becomes unmaintainable.
Nested profiles from a foreign provider
A section here is the subtree under its key, and the profile idea is
implemented on top with profile_env
and sibling files (config.production.toml). A figment provider handed to
Source::provider
files its values under figment profiles, and only three of them are read: the
default profile, the one named after the section being loaded, and global.
A provider whose own profile hierarchy is the point cannot carry it
through.
What would reopen it: a provider whose own profiles you need, where
Source::provider plus profile_env genuinely cannot express what you are
after.
A swappable loader backend
This one was reopened and then closed by building it. Through 0.8 the
answer here was that figment reached nineteen modules, that sections were
figment profiles, and that provenance was figment metadata — so a Loader
trait would either leak Metadata and Profile into its own signature or
drop provenance, which is most of what this crate sells. The stated
reopening condition was "a backend that resolves layered providers, profile
selection and loose environment typing and carries provenance".
The engine is now that. It parses the documents, walks the environment, reads the value strings, folds the layers and records the origin of each leaf as it wins — How resolution works is the whole of it — and figment is an optional interop adapter that is not in the default dependency graph.
What is still refused is the general trait: one Loader interface with
several interchangeable implementations behind it. Two resolvers that agree
on nearly everything are two sets of edge cases, and the compatibility
contract's §5 says features are additive — a resolver behind a cargo feature
would make the meaning of a configuration depend on which features its
dependents happened to turn on. There is one semantics here, and it is this
crate's.
What people actually want from a seam exists and is narrower:
- A format this crate does not read is
Source::provider: three methods, and it has carried the section and provenance contracts since 0.2. - Documents to combine before the loader sees them — a store crate
reading a prefix, a tool folding a fragment directory into one file — is
Value::parse/merge/overlapping_paths/render, described in Writing a Store. It hands over the parsing this crate already compiles, so nothing outside has to takeserde_json,tomlandserde_yamlas direct dependencies to re-do it.
What would reopen it: nothing about the backend. A source this crate
cannot express is a Source variant or a provider, not a second resolver.
A crate per backend
dynamic-config-engine-figment, dynamic-config-engine-config-rs — one
crate each, so a build takes only the adapter it wants. It already takes
only what it asks for: a feature is an optional dependency, and a build
that does not name figment compiles no figment at all.
tests/documents.rs asserts that in the manifest so it stays true.
What a crate split would cost, on top of buying nothing measurable:
- An adapter cannot be the default. It has to depend on this crate for
the
EngineandReadertraits, so this crate cannot depend on it. The default engine isconfig-rs— and since this crate has no fold of its own, out of tree there would be no default at all. - The agreement tests would lose their list.
engine::all()is walked by the property test that holds every engine to the same fold, leaf by leaf. It can only list what is in the crate — so an out-of-tree adapter is one nothing compares, and a test that quietly stops testing is worse than one that was never written. - figment cannot leave anyway. It is a permanent dev-dependency, because the ported value-string reader, deserializer, serializer and fold are each proved against it — and those comparisons reach private module internals that no other crate can see. Moving half of figment out would scatter it across a crate boundary, which is the opposite of why the backends were grouped into a directory each.
Source::providerwould have to go. It takes a&dyn figment::Providerand is part of this crate's public API.
What would reopen it: a backend heavy enough that carrying it as an optional dependency costs something a feature flag cannot avoid — a build script, a system library, a licence. None of the two is.
Case-insensitive keys
Viper lowercases everything. It hides typos — Prot and port become the same
key, so unknown-key detection can never
tell you about the first — and it cannot round-trip: a configuration read and
written back comes out in different case from the one a person wrote.
What would reopen it: nothing. This one is a principle rather than a cost.
HCL, and the next format after it
INI and .properties are here, behind their own features, because deployments
asked for them. HCL is a parser and a set of edge cases for a format nobody
here has asked for, and the same is true of whatever comes after it: a format
in the crate is a format the crate maintains forever.
The answer that is not a fork:
Source::provider
takes any figment provider, so a crate that parses one of these wires in
without this one growing a dependency.
Independent instances
Example Go Viper needs them because its default instance is a global. Here every configuration type already has its own storage, keyed by the type — the same isolation without the bookkeeping.
Inferring a type from a default value
serde already knows the type. Viper's SetTypeByDefaultValue exists because
Go's map[string]interface{} does not.
A service-account JSON key for Firestore
Signing one means an RS256 stack inside a configuration library, and Google's own guidance is that a downloaded key is the option of last resort. Workload identity covers GKE, Cloud Run, GCE and Cloud Functions; anything else can mint a token outside the process and pass it in.
Roadmap
ROADMAP.md is what might still be built, and why each item is not obvious. It is short on purpose.