Skip to main content

dynamic_config/
layer.rs

1//! Values set from code rather than read from a file.
2//!
3//! Two of these bracket the file and environment layers:
4//!
5//! ```text
6//! defaults  <  files  <  environment  <  overrides
7//! ```
8//!
9//! **Defaults** are for values a program can compute but a file need not state —
10//! a pool size derived from the core count, say. `#[serde(default)]` covers the
11//! constant case; this covers the case where the fallback is only known at run
12//! time, and it is *observable*: the value came from somewhere, rather than from
13//! a `Deserialize` impl nobody can see.
14//!
15//! **Overrides** win over everything, which is what makes them useful in tests
16//! and behind a `--set key=value` flag. They are also the one layer that can be
17//! changed while the process runs, so they take effect on the next `load()`.
18
19use std::collections::BTreeMap;
20use std::sync::Mutex;
21
22use figment::value::{Dict, Map, Value};
23use figment::{Metadata, Profile, Provider};
24use serde::Serialize;
25
26use crate::error::{Error, ErrorKind};
27
28/// Metadata name for the defaults layer. Matched when attributing an error.
29pub(crate) const DEFAULTS_NAME: &str = "values set as defaults";
30
31/// Metadata name for the overrides layer.
32pub(crate) const OVERRIDES_NAME: &str = "values set as overrides";
33
34/// Metadata name for the command-line layer.
35pub(crate) const FLAGS_NAME: &str = "values set from the command line";
36
37/// A set of values addressed by dotted path.
38///
39/// `Layer::new()` is `const`, so this lives in a `static` — which is how
40/// `#[dynamic_config]` emits it. Every method takes `&self`, so a layer can be
41/// written to from anywhere without threading a handle around.
42///
43/// # Example
44///
45/// ```
46/// use dynamic_config::Layer;
47///
48/// static OVERRIDES: Layer = Layer::new();
49///
50/// assert!(OVERRIDES.is_empty());
51///
52/// OVERRIDES.set("pool.max_size", 32u16).unwrap();
53/// assert!(!OVERRIDES.is_empty());
54///
55/// OVERRIDES.clear();
56/// ```
57#[derive(Default)]
58pub struct Layer {
59    entries: Mutex<BTreeMap<String, Value>>,
60}
61
62impl Layer {
63    /// An empty layer.
64    #[must_use]
65    pub const fn new() -> Self {
66        Self {
67            entries: Mutex::new(BTreeMap::new()),
68        }
69    }
70
71    /// Sets `path` — dotted for nested fields, as in `"pool.max_size"`.
72    ///
73    /// Setting the same path again replaces it. The value is converted once,
74    /// here, so a type that cannot be represented fails at the call site rather
75    /// than at the next reload.
76    ///
77    /// # Errors
78    ///
79    /// If `path` is empty or has a blank segment, or if `value` cannot be
80    /// serialized.
81    ///
82    pub fn set<T: Serialize>(&self, path: &str, value: T) -> Result<(), Error> {
83        check_path(path)?;
84
85        let value = Value::serialize(value)
86            .map_err(|error| Error::new(ErrorKind::Type, error.to_string()).prepend_key(path))?;
87
88        self.lock().insert(path.to_owned(), value);
89
90        Ok(())
91    }
92
93    /// Sets every top-level field of a serializable value at once.
94    ///
95    /// The typed way to seed the defaults layer: instead of one
96    /// `set_default(path, value)` per field, hand over a whole struct —
97    /// usually `&Config::default()` — and every field lands in the layer in
98    /// one step, in the same shape the configuration itself has.
99    ///
100    /// # Errors
101    ///
102    /// If `value` does not serialize, or serializes to something other than
103    /// a map — defaults have named fields by definition.
104    pub fn set_struct<T: serde::Serialize>(&self, value: &T) -> Result<(), Error> {
105        let serialized = Value::serialize(value).map_err(|error| {
106            Error::new(
107                crate::ErrorKind::Type,
108                format!("the defaults struct did not serialize: {error}"),
109            )
110        })?;
111
112        let Value::Dict(_, entries) = serialized else {
113            return Err(Error::new(
114                crate::ErrorKind::Type,
115                "defaults must be a struct or a map; a bare value has no field name to live under",
116            ));
117        };
118
119        let mut layer = self.lock();
120
121        for (path, value) in entries {
122            layer.insert(path, value);
123        }
124
125        Ok(())
126    }
127
128    /// Sets `path` from text, read the way an environment variable is.
129    ///
130    /// `"8080"` becomes a number, `"true"` a boolean, `"[a, b]"` a list — the
131    /// same loose reading the environment layer gets, so a value means the same
132    /// thing whether it arrives as `APP_DB_PORT=8080` or `--set db.port=8080`.
133    ///
134    /// # Errors
135    ///
136    /// If `path` is empty or has a blank segment.
137    ///
138    pub fn set_text(&self, path: &str, text: &str) -> Result<(), Error> {
139        check_path(path)?;
140
141        // Parsed the way the environment layer parses, fallback included:
142        // `figment`'s parser does not fail today, but "cannot fail" is its
143        // implementation detail, not this crate's to promise on.
144        let value = text
145            .parse::<Value>()
146            .unwrap_or_else(|_| Value::from(text.to_owned()));
147
148        self.lock().insert(path.to_owned(), value);
149
150        Ok(())
151    }
152
153    /// Sets every `key=value` in `assignments`.
154    ///
155    /// The shape behind a `--set key=value` flag. A pair with no `=` is an
156    /// error naming the offending argument rather than a silently ignored one.
157    ///
158    /// # Errors
159    ///
160    /// If an assignment has no `=`, or its key is not a usable path.
161    ///
162    pub fn set_assignments<I, S>(&self, assignments: I) -> Result<(), Error>
163    where
164        I: IntoIterator<Item = S>,
165        S: AsRef<str>,
166    {
167        for assignment in assignments {
168            let assignment = assignment.as_ref();
169
170            let Some((path, value)) = assignment.split_once('=') else {
171                return Err(Error::new(
172                    ErrorKind::Type,
173                    format!("`{assignment}` is not a `key=value` assignment"),
174                ));
175            };
176
177            self.set_text(path.trim(), value)?;
178        }
179
180        Ok(())
181    }
182
183    /// Copies clap arguments into this layer, by `(argument id, key path)`.
184    ///
185    /// Only arguments that came from the **command line** are taken. clap's own
186    /// `default_value` looks identical to a typed flag in `ArgMatches`, and
187    /// letting a clap default outrank a configuration file would invert the
188    /// whole precedence order — the file would be ignored in favour of a
189    /// fallback nobody asked for.
190    ///
191    /// Values are read the way environment variables are, so `--port 8080` and
192    /// `APP_DB_PORT=8080` mean the same thing whatever type the argument was
193    /// declared with.
194    ///
195    /// ```no_run
196    /// # use dynamic_config::Layer;
197    /// # fn example(matches: &clap::ArgMatches, flags: &Layer) -> Result<(), dynamic_config::Error> {
198    /// flags.bind_clap(matches, &[("db-host", "host"), ("db-port", "port")])?;
199    /// # Ok(())
200    /// # }
201    /// ```
202    ///
203    /// # Errors
204    ///
205    /// If a key path is unusable, or an argument's value is not valid UTF-8.
206    ///
207    #[cfg(feature = "clap")]
208    #[cfg_attr(docsrs, doc(cfg(feature = "clap")))]
209    pub fn bind_clap(
210        &self,
211        matches: &clap::ArgMatches,
212        bindings: &[(&str, &str)],
213    ) -> Result<(), Error> {
214        for (argument, path) in bindings {
215            if matches.value_source(argument) != Some(clap::parser::ValueSource::CommandLine) {
216                continue;
217            }
218
219            let Some(mut values) = matches.get_raw(argument) else {
220                continue;
221            };
222
223            // A repeated argument is a list; a single one is a scalar, so that
224            // `--port 8080` fills a `u16` rather than a one-element `Vec`.
225            let raw: Vec<&std::ffi::OsStr> = values.by_ref().collect();
226
227            let text = match raw.as_slice() {
228                [] => continue,
229                [single] => utf8(single, argument)?.to_owned(),
230                many => {
231                    let mut rendered = String::from("[");
232
233                    for (index, value) in many.iter().enumerate() {
234                        if index > 0 {
235                            rendered.push(',');
236                        }
237
238                        rendered.push_str(utf8(value, argument)?);
239                    }
240
241                    rendered.push(']');
242                    rendered
243                }
244            };
245
246            self.set_text(path, &text)?;
247        }
248
249        Ok(())
250    }
251
252    /// Removes `path`, reporting whether anything was there.
253    #[must_use = "the return says whether anything was removed; ignore it \
254                  deliberately with `let _ =` if you do not care"]
255    pub fn unset(&self, path: &str) -> bool {
256        self.lock().remove(path).is_some()
257    }
258
259    /// Removes everything.
260    ///
261    pub fn clear(&self) {
262        self.lock().clear();
263    }
264
265    /// Whether the layer would contribute anything.
266    ///
267    #[must_use]
268    pub fn is_empty(&self) -> bool {
269        self.lock().is_empty()
270    }
271
272    /// Recovers from poisoning rather than propagating it.
273    ///
274    /// The map behind this lock has no invariant a panic could break — it is a
275    /// `BTreeMap` of paths to values, written one entry at a time. Propagating
276    /// the poison would mean one panicked caller turns every later `load()`
277    /// into a panic, which is a poor trade for a configuration library. The
278    /// same choice is made everywhere else in the crate.
279    fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, Value>> {
280        self.entries
281            .lock()
282            .unwrap_or_else(std::sync::PoisonError::into_inner)
283    }
284
285    /// Expands the dotted paths into the nested shape the loader wants.
286    fn dict(&self) -> Dict {
287        let mut root = Dict::new();
288
289        for (path, value) in self.lock().iter() {
290            insert_path(&mut root, path, value.clone());
291        }
292
293        root
294    }
295
296    /// A figment provider emitting this layer under `profile`.
297    pub(crate) fn provider<'a>(&'a self, profile: &str, name: &'static str) -> LayerProvider<'a> {
298        LayerProvider {
299            layer: self,
300            profile: Profile::from(profile),
301            name,
302        }
303    }
304}
305
306/// Configuration is text, and an argument that is not valid UTF-8 cannot be a
307/// configuration value whatever its bytes mean elsewhere.
308#[cfg(feature = "clap")]
309fn utf8<'a>(value: &'a std::ffi::OsStr, argument: &str) -> Result<&'a str, Error> {
310    value.to_str().ok_or_else(|| {
311        Error::new(
312            ErrorKind::Type,
313            format!("`--{argument}` is not valid UTF-8"),
314        )
315    })
316}
317
318/// Writes `value` at a dotted path, creating intermediate tables.
319///
320/// A non-table sitting where a table is needed is replaced: the caller asked
321/// for a nested key, so the scalar that was there cannot also be honoured.
322/// Rejects a path that names nothing: empty, or with an empty segment.
323///
324/// Shared with the environment bindings, which have exactly the same rule for
325/// exactly the same reason.
326pub(crate) fn check_path(path: &str) -> Result<(), Error> {
327    if path.is_empty() || path.split('.').any(str::is_empty) {
328        return Err(Error::new(
329            ErrorKind::Type,
330            format!("`{path}` is not a usable key path"),
331        ));
332    }
333
334    Ok(())
335}
336
337pub(crate) fn insert_path(root: &mut Dict, path: &str, value: Value) {
338    let mut segments = path.split('.').peekable();
339    let mut current = root;
340
341    while let Some(segment) = segments.next() {
342        if segments.peek().is_none() {
343            current.insert(segment.to_owned(), value);
344            return;
345        }
346
347        let entry = current
348            .entry(segment.to_owned())
349            .or_insert_with(|| Value::from(Dict::new()));
350
351        if !matches!(entry, Value::Dict(..)) {
352            *entry = Value::from(Dict::new());
353        }
354
355        let Value::Dict(_, nested) = entry else {
356            unreachable!("just replaced with a dict")
357        };
358
359        current = nested;
360    }
361}
362
363pub(crate) struct LayerProvider<'a> {
364    layer: &'a Layer,
365    profile: Profile,
366    name: &'static str,
367}
368
369// Key names and a count, never the values: an override layer is exactly
370// where a secret set at runtime lives, and `{:?}` reaching a log is an
371// ordinary accident.
372impl std::fmt::Debug for Layer {
373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374        let entries = self.lock();
375
376        f.debug_struct("Layer")
377            .field("keys", &entries.keys().collect::<Vec<_>>())
378            .field("len", &entries.len())
379            .finish_non_exhaustive()
380    }
381}
382
383impl Provider for LayerProvider<'_> {
384    fn metadata(&self) -> Metadata {
385        Metadata::named(self.name)
386    }
387
388    fn data(&self) -> figment::Result<Map<Profile, Dict>> {
389        let mut map = Map::new();
390        map.insert(self.profile.clone(), self.layer.dict());
391
392        Ok(map)
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    #[test]
401    fn a_fresh_layer_contributes_nothing() {
402        assert!(Layer::new().is_empty());
403    }
404
405    #[test]
406    fn setting_the_same_path_replaces_it() {
407        let layer = Layer::new();
408        layer.set("port", 1u16).unwrap();
409        layer.set("port", 2u16).unwrap();
410
411        let dict = layer.dict();
412        assert_eq!(dict.get("port"), Some(&Value::from(2u16)));
413    }
414
415    #[test]
416    fn a_dotted_path_becomes_a_nested_table() {
417        let layer = Layer::new();
418        layer.set("pool.max_size", 32u16).unwrap();
419
420        let dict = layer.dict();
421        let Some(Value::Dict(_, pool)) = dict.get("pool") else {
422            panic!("expected a nested dict, got {dict:?}");
423        };
424
425        assert_eq!(pool.get("max_size"), Some(&Value::from(32u16)));
426    }
427
428    #[test]
429    fn siblings_under_one_parent_do_not_clobber_each_other() {
430        let layer = Layer::new();
431        layer.set("pool.max_size", 32u16).unwrap();
432        layer.set("pool.min_size", 4u16).unwrap();
433
434        let dict = layer.dict();
435        let Some(Value::Dict(_, pool)) = dict.get("pool") else {
436            panic!("expected a nested dict");
437        };
438
439        assert_eq!(pool.len(), 2);
440    }
441
442    #[test]
443    fn a_scalar_standing_where_a_table_is_needed_is_replaced() {
444        let layer = Layer::new();
445        layer.set("pool", 1u16).unwrap();
446        layer.set("pool.max_size", 32u16).unwrap();
447
448        let dict = layer.dict();
449        assert!(matches!(dict.get("pool"), Some(Value::Dict(..))));
450    }
451
452    #[test]
453    fn unset_and_clear_both_report_honestly() {
454        let layer = Layer::new();
455        layer.set("a", 1u16).unwrap();
456
457        assert!(layer.unset("a"));
458        assert!(!layer.unset("a"));
459        assert!(layer.is_empty());
460
461        layer.set("b", 1u16).unwrap();
462        layer.clear();
463        assert!(layer.is_empty());
464    }
465
466    #[test]
467    fn text_is_read_the_way_an_environment_variable_is() {
468        let layer = Layer::new();
469        layer.set_text("port", "8080").unwrap();
470        layer.set_text("enabled", "true").unwrap();
471        layer.set_text("host", "localhost").unwrap();
472
473        let dict = layer.dict();
474
475        assert_eq!(dict.get("port"), Some(&Value::from(8080u64)));
476        assert_eq!(dict.get("enabled"), Some(&Value::from(true)));
477        assert_eq!(dict.get("host"), Some(&Value::from("localhost")));
478    }
479
480    #[test]
481    fn assignments_are_split_on_the_first_equals() {
482        let layer = Layer::new();
483        layer
484            .set_assignments(["db.host=post=gres", "db.port=5432"])
485            .unwrap();
486
487        let dict = layer.dict();
488        let Some(Value::Dict(_, db)) = dict.get("db") else {
489            panic!("expected a nested dict");
490        };
491
492        assert_eq!(db.get("host"), Some(&Value::from("post=gres")));
493        assert_eq!(db.get("port"), Some(&Value::from(5432u64)));
494    }
495
496    #[test]
497    fn an_assignment_without_an_equals_names_itself() {
498        let error = Layer::new().set_assignments(["nonsense"]).unwrap_err();
499
500        assert!(error.to_string().contains("`nonsense`"), "{error}");
501    }
502
503    #[test]
504    fn an_unusable_path_is_rejected_at_the_call_site() {
505        let layer = Layer::new();
506
507        assert!(layer.set("", 1u16).is_err());
508        assert!(layer.set("a..b", 1u16).is_err());
509        assert!(layer.set(".a", 1u16).is_err());
510    }
511
512    #[test]
513    fn structured_values_survive_the_round_trip() {
514        #[derive(serde::Serialize)]
515        struct Pool {
516            max_size: u16,
517        }
518
519        let layer = Layer::new();
520        layer.set("pool", Pool { max_size: 7 }).unwrap();
521
522        let dict = layer.dict();
523        let Some(Value::Dict(_, pool)) = dict.get("pool") else {
524            panic!("expected a nested dict");
525        };
526
527        assert_eq!(pool.get("max_size"), Some(&Value::from(7u16)));
528    }
529}