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    // `db::timeout` means "in another section", and exactly one place can
335    // honour that: the old path of an alias, which splits the qualifier off
336    // before it gets here. Everywhere else a path is relative to the section
337    // being loaded, so accepting one would silently create a key with a colon
338    // in its name that looks, in every report, like it worked.
339    if path.contains(crate::aliases::SECTION) {
340        return Err(Error::new(
341            ErrorKind::Type,
342            format!(
343                "`{path}` names another section, and this path is relative to \
344                 the section being loaded; `{}` is only meaningful in the old \
345                 path of an alias",
346                crate::aliases::SECTION
347            ),
348        ));
349    }
350
351    Ok(())
352}
353
354pub(crate) fn insert_path(root: &mut Dict, path: &str, value: Value) {
355    let mut segments = path.split('.').peekable();
356    let mut current = root;
357
358    while let Some(segment) = segments.next() {
359        if segments.peek().is_none() {
360            current.insert(segment.to_owned(), value);
361            return;
362        }
363
364        let entry = current
365            .entry(segment.to_owned())
366            .or_insert_with(|| Value::from(Dict::new()));
367
368        if !matches!(entry, Value::Dict(..)) {
369            *entry = Value::from(Dict::new());
370        }
371
372        let Value::Dict(_, nested) = entry else {
373            unreachable!("just replaced with a dict")
374        };
375
376        current = nested;
377    }
378}
379
380pub(crate) struct LayerProvider<'a> {
381    layer: &'a Layer,
382    profile: Profile,
383    name: &'static str,
384}
385
386// Key names and a count, never the values: an override layer is exactly
387// where a secret set at runtime lives, and `{:?}` reaching a log is an
388// ordinary accident.
389impl std::fmt::Debug for Layer {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        let entries = self.lock();
392
393        f.debug_struct("Layer")
394            .field("keys", &entries.keys().collect::<Vec<_>>())
395            .field("len", &entries.len())
396            .finish_non_exhaustive()
397    }
398}
399
400impl Provider for LayerProvider<'_> {
401    fn metadata(&self) -> Metadata {
402        Metadata::named(self.name)
403    }
404
405    fn data(&self) -> figment::Result<Map<Profile, Dict>> {
406        let mut map = Map::new();
407        map.insert(self.profile.clone(), self.layer.dict());
408
409        Ok(map)
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    #[test]
418    fn a_fresh_layer_contributes_nothing() {
419        assert!(Layer::new().is_empty());
420    }
421
422    #[test]
423    fn setting_the_same_path_replaces_it() {
424        let layer = Layer::new();
425        layer.set("port", 1u16).unwrap();
426        layer.set("port", 2u16).unwrap();
427
428        let dict = layer.dict();
429        assert_eq!(dict.get("port"), Some(&Value::from(2u16)));
430    }
431
432    #[test]
433    fn a_dotted_path_becomes_a_nested_table() {
434        let layer = Layer::new();
435        layer.set("pool.max_size", 32u16).unwrap();
436
437        let dict = layer.dict();
438        let Some(Value::Dict(_, pool)) = dict.get("pool") else {
439            panic!("expected a nested dict, got {dict:?}");
440        };
441
442        assert_eq!(pool.get("max_size"), Some(&Value::from(32u16)));
443    }
444
445    #[test]
446    fn siblings_under_one_parent_do_not_clobber_each_other() {
447        let layer = Layer::new();
448        layer.set("pool.max_size", 32u16).unwrap();
449        layer.set("pool.min_size", 4u16).unwrap();
450
451        let dict = layer.dict();
452        let Some(Value::Dict(_, pool)) = dict.get("pool") else {
453            panic!("expected a nested dict");
454        };
455
456        assert_eq!(pool.len(), 2);
457    }
458
459    #[test]
460    fn a_scalar_standing_where_a_table_is_needed_is_replaced() {
461        let layer = Layer::new();
462        layer.set("pool", 1u16).unwrap();
463        layer.set("pool.max_size", 32u16).unwrap();
464
465        let dict = layer.dict();
466        assert!(matches!(dict.get("pool"), Some(Value::Dict(..))));
467    }
468
469    #[test]
470    fn unset_and_clear_both_report_honestly() {
471        let layer = Layer::new();
472        layer.set("a", 1u16).unwrap();
473
474        assert!(layer.unset("a"));
475        assert!(!layer.unset("a"));
476        assert!(layer.is_empty());
477
478        layer.set("b", 1u16).unwrap();
479        layer.clear();
480        assert!(layer.is_empty());
481    }
482
483    #[test]
484    fn text_is_read_the_way_an_environment_variable_is() {
485        let layer = Layer::new();
486        layer.set_text("port", "8080").unwrap();
487        layer.set_text("enabled", "true").unwrap();
488        layer.set_text("host", "localhost").unwrap();
489
490        let dict = layer.dict();
491
492        assert_eq!(dict.get("port"), Some(&Value::from(8080u64)));
493        assert_eq!(dict.get("enabled"), Some(&Value::from(true)));
494        assert_eq!(dict.get("host"), Some(&Value::from("localhost")));
495    }
496
497    #[test]
498    fn assignments_are_split_on_the_first_equals() {
499        let layer = Layer::new();
500        layer
501            .set_assignments(["db.host=post=gres", "db.port=5432"])
502            .unwrap();
503
504        let dict = layer.dict();
505        let Some(Value::Dict(_, db)) = dict.get("db") else {
506            panic!("expected a nested dict");
507        };
508
509        assert_eq!(db.get("host"), Some(&Value::from("post=gres")));
510        assert_eq!(db.get("port"), Some(&Value::from(5432u64)));
511    }
512
513    #[test]
514    fn an_assignment_without_an_equals_names_itself() {
515        let error = Layer::new().set_assignments(["nonsense"]).unwrap_err();
516
517        assert!(error.to_string().contains("`nonsense`"), "{error}");
518    }
519
520    #[test]
521    fn an_unusable_path_is_rejected_at_the_call_site() {
522        let layer = Layer::new();
523
524        assert!(layer.set("", 1u16).is_err());
525        assert!(layer.set("a..b", 1u16).is_err());
526        assert!(layer.set(".a", 1u16).is_err());
527    }
528
529    #[test]
530    fn structured_values_survive_the_round_trip() {
531        #[derive(serde::Serialize)]
532        struct Pool {
533            max_size: u16,
534        }
535
536        let layer = Layer::new();
537        layer.set("pool", Pool { max_size: 7 }).unwrap();
538
539        let dict = layer.dict();
540        let Some(Value::Dict(_, pool)) = dict.get("pool") else {
541            panic!("expected a nested dict");
542        };
543
544        assert_eq!(pool.get("max_size"), Some(&Value::from(7u16)));
545    }
546}