dynamic_config/engine.rs
1//! The resolution engine: the thing that folds the collected layers.
2//!
3//! A load walks its sources itself — discovery, decryption, sections, the
4//! environment and the `.env` files are this crate's, and no engine sees a
5//! file. What an engine does is the step after that: take one tree per
6//! layer, in precedence order, and answer with the merged tree and which
7//! layer won each leaf.
8//!
9//! ```text
10//! defaults {host: localhost, port: 5432} tag 0
11//! file {host: db.internal} tag 1
12//! env {port: 6543} tag 2
13//! ──────────── fold ────────────
14//! values {host: db.internal, port: 6543}
15//! tags host → 1, port → 2
16//! ```
17//!
18//! **Every engine here implements the same rule** — tables descend,
19//! everything else replaces, arrays included — so which one is installed is
20//! not a question about what a configuration means. That is not a hope: the
21//! whole composition corpus, a generated corpus of layer stacks, and the
22//! corner cases where a backend's own habits could show through all run
23//! through every engine, and both the tree and the winner of every leaf are
24//! compared.
25//!
26//! Where a backend disagrees, the adapter is what gives way. One of them
27//! reads a top-level key as a *path expression*, which would turn
28//! `{"my.module": "debug"}` into a nested table; its adapter hands over
29//! stand-in names and puts the document's own back afterwards. Another
30//! records provenance per provider and cannot answer for a key with a dot
31//! in it; the fold fills that in from the layers it was given.
32//!
33//! Two ship:
34//!
35//! | engine | feature | what it is |
36//! |---|---|---|
37//! | [`config_rs()`] | — | the fold of the `config` crate; the default |
38//! | [`figment()`] | `figment` | the fold of the `figment` crate |
39//!
40//! A third is anything implementing [`Engine`]: the trait deals in this
41//! crate's [`Value`] and in opaque tags, so nothing about a backend reaches
42//! it. **This crate keeps no fold of its own** — it wrote one, proved the
43//! others against it, and then deleted it rather than maintain a second
44//! implementation of somebody else's rule.
45
46use std::collections::BTreeMap;
47use std::fmt;
48
49use crate::error::Error;
50use crate::value::Value;
51
52/// One layer, on its way into an engine.
53///
54/// The tag is opaque and belongs to the caller: hand it back for every leaf
55/// this layer wins, and the caller turns it into the file or variable a
56/// person reads. An engine that invents a tag it was not given is an engine
57/// reporting a source that does not exist.
58#[derive(Debug, Clone, Copy)]
59pub struct Layer<'a> {
60 /// Which layer this is, as the caller counts them.
61 pub tag: usize,
62 /// What it has to say: always a [`Value::Table`], because a section is
63 /// keys and a layer with nothing to say supplies an empty one.
64 pub values: &'a Value,
65}
66
67/// What an engine hands back.
68#[derive(Debug, Clone, PartialEq)]
69pub struct Folded {
70 /// The merged tree, always a [`Value::Table`].
71 pub values: Value,
72 /// The winning layer's tag per dotted leaf path.
73 ///
74 /// A leaf with no entry reports [`Origin::Unknown`](crate::Origin) —
75 /// honest, and better than a guess.
76 pub tags: BTreeMap<String, usize>,
77}
78
79/// A fold, and where each leaf came from.
80///
81/// Implement this to resolve with something else entirely — a backend this
82/// crate does not ship, or a rule of your own. Two things are asked of an
83/// implementation, and the rest is its business:
84///
85/// - **Precedence is the argument's order.** `layers[0]` is the lowest.
86/// - **A tag is reported only for a leaf that layer actually supplied.**
87///
88/// # Errors
89///
90/// A fold can fail — a backend may refuse a key shape of its own — and the
91/// error reaches the caller as an ordinary load failure. It must not carry
92/// a configuration value: an engine that puts one in a message breaks the
93/// contract every other part of this crate keeps.
94pub trait Engine: fmt::Debug + Send + Sync {
95 /// What to call this engine in a diagnostic.
96 fn name(&self) -> &str;
97
98 /// Folds `layers`, lowest precedence first.
99 ///
100 /// # Errors
101 ///
102 /// If the backend refuses the shape it was handed.
103 fn fold(&self, layers: &[Layer<'_>]) -> Result<Folded, Error>;
104}
105
106/// The [`config`](https://docs.rs/config) crate's fold.
107///
108/// The default. `config` carries an origin on every value, so a leaf's
109/// winner comes back from the backend rather than from a second walk.
110#[must_use]
111pub fn config_rs() -> &'static dyn Engine {
112 &ConfigRs
113}
114
115/// The [`figment`](https://docs.rs/figment) crate's fold.
116///
117/// figment records metadata per *provider*, so each layer is merged as its
118/// own provider and the winner of a leaf is read back from the metadata
119/// that survived the merge.
120#[cfg(feature = "figment")]
121#[cfg_attr(docsrs, doc(cfg(feature = "figment")))]
122#[must_use]
123pub fn figment() -> &'static dyn Engine {
124 &Figment
125}
126
127/// Every engine this build ships, lowest-level first.
128///
129/// **The one list.** The agreement tests walk it rather than naming
130/// engines, so an engine added here is compared against the others on every
131/// corpus the crate has without a test being edited — which is the point:
132/// an engine that nothing compares is an engine nobody has checked.
133///
134/// The default comes first, and is the one a disagreement is reported
135/// against.
136#[must_use]
137pub fn all() -> Vec<&'static dyn Engine> {
138 vec![
139 config_rs(),
140 #[cfg(feature = "figment")]
141 figment(),
142 ]
143}
144
145/// The engine a load uses when nothing chose one.
146///
147/// **This crate has no fold of its own.** It had one, as the reference the
148/// others were compared against — and carrying a second implementation of
149/// a rule somebody else already implements is maintenance with no reader.
150/// The comparison it existed for is between the backends; the rule itself
151/// is written down in the book and held by the tests either way.
152pub(crate) fn default() -> &'static dyn Engine {
153 config_rs()
154}
155
156/// The installed engine, or the default.
157pub(crate) fn installed() -> &'static dyn Engine {
158 INSTALLED.get().copied().unwrap_or_else(default)
159}
160
161static INSTALLED: std::sync::OnceLock<&'static dyn Engine> = std::sync::OnceLock::new();
162
163/// Installs `engine` for every load in this process that does not name one.
164///
165/// Call it before the first `init()`. A load that names its own engine —
166/// `builder(..).engine(..)` — uses that one whatever is installed here.
167///
168/// # Errors
169///
170/// If one is already installed. The rejected engine is returned, so a
171/// caller can tell "already set" from "failed".
172pub fn set_engine(engine: &'static dyn Engine) -> Result<(), &'static dyn Engine> {
173 INSTALLED.set(engine)
174}
175
176/// Whether an engine has been installed.
177#[must_use]
178pub fn has_engine() -> bool {
179 INSTALLED.get().is_some()
180}
181
182// ---------------------------------------------------------------------------
183// config-rs
184// ---------------------------------------------------------------------------
185
186use crate::backend::config_rs::Engine as ConfigRs;
187
188// ---------------------------------------------------------------------------
189// figment
190// ---------------------------------------------------------------------------
191
192#[cfg(feature = "figment")]
193use crate::backend::figment::Engine as Figment;