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 `path` from text, read the way an environment variable is.
94    ///
95    /// `"8080"` becomes a number, `"true"` a boolean, `"[a, b]"` a list — the
96    /// same loose reading the environment layer gets, so a value means the same
97    /// thing whether it arrives as `APP_DB_PORT=8080` or `--set db.port=8080`.
98    ///
99    /// # Errors
100    ///
101    /// If `path` is empty or has a blank segment.
102    ///
103    pub fn set_text(&self, path: &str, text: &str) -> Result<(), Error> {
104        check_path(path)?;
105
106        // Parsed the way the environment layer parses, fallback included:
107        // `figment`'s parser does not fail today, but "cannot fail" is its
108        // implementation detail, not this crate's to promise on.
109        let value = text
110            .parse::<Value>()
111            .unwrap_or_else(|_| Value::from(text.to_owned()));
112
113        self.lock().insert(path.to_owned(), value);
114
115        Ok(())
116    }
117
118    /// Sets every `key=value` in `assignments`.
119    ///
120    /// The shape behind a `--set key=value` flag. A pair with no `=` is an
121    /// error naming the offending argument rather than a silently ignored one.
122    ///
123    /// # Errors
124    ///
125    /// If an assignment has no `=`, or its key is not a usable path.
126    ///
127    pub fn set_assignments<I, S>(&self, assignments: I) -> Result<(), Error>
128    where
129        I: IntoIterator<Item = S>,
130        S: AsRef<str>,
131    {
132        for assignment in assignments {
133            let assignment = assignment.as_ref();
134
135            let Some((path, value)) = assignment.split_once('=') else {
136                return Err(Error::new(
137                    ErrorKind::Type,
138                    format!("`{assignment}` is not a `key=value` assignment"),
139                ));
140            };
141
142            self.set_text(path.trim(), value)?;
143        }
144
145        Ok(())
146    }
147
148    /// Copies clap arguments into this layer, by `(argument id, key path)`.
149    ///
150    /// Only arguments that came from the **command line** are taken. clap's own
151    /// `default_value` looks identical to a typed flag in `ArgMatches`, and
152    /// letting a clap default outrank a configuration file would invert the
153    /// whole precedence order — the file would be ignored in favour of a
154    /// fallback nobody asked for.
155    ///
156    /// Values are read the way environment variables are, so `--port 8080` and
157    /// `APP_DB_PORT=8080` mean the same thing whatever type the argument was
158    /// declared with.
159    ///
160    /// ```no_run
161    /// # use dynamic_config::Layer;
162    /// # fn example(matches: &clap::ArgMatches, flags: &Layer) -> Result<(), dynamic_config::Error> {
163    /// flags.bind_clap(matches, &[("db-host", "host"), ("db-port", "port")])?;
164    /// # Ok(())
165    /// # }
166    /// ```
167    ///
168    /// # Errors
169    ///
170    /// If a key path is unusable, or an argument's value is not valid UTF-8.
171    ///
172    #[cfg(feature = "clap")]
173    #[cfg_attr(docsrs, doc(cfg(feature = "clap")))]
174    pub fn bind_clap(
175        &self,
176        matches: &clap::ArgMatches,
177        bindings: &[(&str, &str)],
178    ) -> Result<(), Error> {
179        for (argument, path) in bindings {
180            if matches.value_source(argument) != Some(clap::parser::ValueSource::CommandLine) {
181                continue;
182            }
183
184            let Some(mut values) = matches.get_raw(argument) else {
185                continue;
186            };
187
188            // A repeated argument is a list; a single one is a scalar, so that
189            // `--port 8080` fills a `u16` rather than a one-element `Vec`.
190            let raw: Vec<&std::ffi::OsStr> = values.by_ref().collect();
191
192            let text = match raw.as_slice() {
193                [] => continue,
194                [single] => utf8(single, argument)?.to_owned(),
195                many => {
196                    let mut rendered = String::from("[");
197
198                    for (index, value) in many.iter().enumerate() {
199                        if index > 0 {
200                            rendered.push(',');
201                        }
202
203                        rendered.push_str(utf8(value, argument)?);
204                    }
205
206                    rendered.push(']');
207                    rendered
208                }
209            };
210
211            self.set_text(path, &text)?;
212        }
213
214        Ok(())
215    }
216
217    /// Removes `path`, reporting whether anything was there.
218    ///
219    pub fn unset(&self, path: &str) -> bool {
220        self.lock().remove(path).is_some()
221    }
222
223    /// Removes everything.
224    ///
225    pub fn clear(&self) {
226        self.lock().clear();
227    }
228
229    /// Whether the layer would contribute anything.
230    ///
231    pub fn is_empty(&self) -> bool {
232        self.lock().is_empty()
233    }
234
235    /// Recovers from poisoning rather than propagating it.
236    ///
237    /// The map behind this lock has no invariant a panic could break — it is a
238    /// `BTreeMap` of paths to values, written one entry at a time. Propagating
239    /// the poison would mean one panicked caller turns every later `load()`
240    /// into a panic, which is a poor trade for a configuration library. The
241    /// same choice is made everywhere else in the crate.
242    fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, Value>> {
243        self.entries
244            .lock()
245            .unwrap_or_else(std::sync::PoisonError::into_inner)
246    }
247
248    /// Expands the dotted paths into the nested shape the loader wants.
249    fn dict(&self) -> Dict {
250        let mut root = Dict::new();
251
252        for (path, value) in self.lock().iter() {
253            insert_path(&mut root, path, value.clone());
254        }
255
256        root
257    }
258
259    /// A figment provider emitting this layer under `profile`.
260    pub(crate) fn provider<'a>(&'a self, profile: &str, name: &'static str) -> LayerProvider<'a> {
261        LayerProvider {
262            layer: self,
263            profile: Profile::from(profile),
264            name,
265        }
266    }
267}
268
269/// Configuration is text, and an argument that is not valid UTF-8 cannot be a
270/// configuration value whatever its bytes mean elsewhere.
271#[cfg(feature = "clap")]
272fn utf8<'a>(value: &'a std::ffi::OsStr, argument: &str) -> Result<&'a str, Error> {
273    value.to_str().ok_or_else(|| {
274        Error::new(
275            ErrorKind::Type,
276            format!("`--{argument}` is not valid UTF-8"),
277        )
278    })
279}
280
281/// Writes `value` at a dotted path, creating intermediate tables.
282///
283/// A non-table sitting where a table is needed is replaced: the caller asked
284/// for a nested key, so the scalar that was there cannot also be honoured.
285/// Rejects a path that names nothing: empty, or with an empty segment.
286///
287/// Shared with the environment bindings, which have exactly the same rule for
288/// exactly the same reason.
289pub(crate) fn check_path(path: &str) -> Result<(), Error> {
290    if path.is_empty() || path.split('.').any(str::is_empty) {
291        return Err(Error::new(
292            ErrorKind::Type,
293            format!("`{path}` is not a usable key path"),
294        ));
295    }
296
297    Ok(())
298}
299
300pub(crate) fn insert_path(root: &mut Dict, path: &str, value: Value) {
301    let mut segments = path.split('.').peekable();
302    let mut current = root;
303
304    while let Some(segment) = segments.next() {
305        if segments.peek().is_none() {
306            current.insert(segment.to_owned(), value);
307            return;
308        }
309
310        let entry = current
311            .entry(segment.to_owned())
312            .or_insert_with(|| Value::from(Dict::new()));
313
314        if !matches!(entry, Value::Dict(..)) {
315            *entry = Value::from(Dict::new());
316        }
317
318        let Value::Dict(_, nested) = entry else {
319            unreachable!("just replaced with a dict")
320        };
321
322        current = nested;
323    }
324}
325
326pub(crate) struct LayerProvider<'a> {
327    layer: &'a Layer,
328    profile: Profile,
329    name: &'static str,
330}
331
332impl Provider for LayerProvider<'_> {
333    fn metadata(&self) -> Metadata {
334        Metadata::named(self.name)
335    }
336
337    fn data(&self) -> figment::Result<Map<Profile, Dict>> {
338        let mut map = Map::new();
339        map.insert(self.profile.clone(), self.layer.dict());
340
341        Ok(map)
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn a_fresh_layer_contributes_nothing() {
351        assert!(Layer::new().is_empty());
352    }
353
354    #[test]
355    fn setting_the_same_path_replaces_it() {
356        let layer = Layer::new();
357        layer.set("port", 1u16).unwrap();
358        layer.set("port", 2u16).unwrap();
359
360        let dict = layer.dict();
361        assert_eq!(dict.get("port"), Some(&Value::from(2u16)));
362    }
363
364    #[test]
365    fn a_dotted_path_becomes_a_nested_table() {
366        let layer = Layer::new();
367        layer.set("pool.max_size", 32u16).unwrap();
368
369        let dict = layer.dict();
370        let Some(Value::Dict(_, pool)) = dict.get("pool") else {
371            panic!("expected a nested dict, got {dict:?}");
372        };
373
374        assert_eq!(pool.get("max_size"), Some(&Value::from(32u16)));
375    }
376
377    #[test]
378    fn siblings_under_one_parent_do_not_clobber_each_other() {
379        let layer = Layer::new();
380        layer.set("pool.max_size", 32u16).unwrap();
381        layer.set("pool.min_size", 4u16).unwrap();
382
383        let dict = layer.dict();
384        let Some(Value::Dict(_, pool)) = dict.get("pool") else {
385            panic!("expected a nested dict");
386        };
387
388        assert_eq!(pool.len(), 2);
389    }
390
391    #[test]
392    fn a_scalar_standing_where_a_table_is_needed_is_replaced() {
393        let layer = Layer::new();
394        layer.set("pool", 1u16).unwrap();
395        layer.set("pool.max_size", 32u16).unwrap();
396
397        let dict = layer.dict();
398        assert!(matches!(dict.get("pool"), Some(Value::Dict(..))));
399    }
400
401    #[test]
402    fn unset_and_clear_both_report_honestly() {
403        let layer = Layer::new();
404        layer.set("a", 1u16).unwrap();
405
406        assert!(layer.unset("a"));
407        assert!(!layer.unset("a"));
408        assert!(layer.is_empty());
409
410        layer.set("b", 1u16).unwrap();
411        layer.clear();
412        assert!(layer.is_empty());
413    }
414
415    #[test]
416    fn text_is_read_the_way_an_environment_variable_is() {
417        let layer = Layer::new();
418        layer.set_text("port", "8080").unwrap();
419        layer.set_text("enabled", "true").unwrap();
420        layer.set_text("host", "localhost").unwrap();
421
422        let dict = layer.dict();
423
424        assert_eq!(dict.get("port"), Some(&Value::from(8080u64)));
425        assert_eq!(dict.get("enabled"), Some(&Value::from(true)));
426        assert_eq!(dict.get("host"), Some(&Value::from("localhost")));
427    }
428
429    #[test]
430    fn assignments_are_split_on_the_first_equals() {
431        let layer = Layer::new();
432        layer
433            .set_assignments(["db.host=post=gres", "db.port=5432"])
434            .unwrap();
435
436        let dict = layer.dict();
437        let Some(Value::Dict(_, db)) = dict.get("db") else {
438            panic!("expected a nested dict");
439        };
440
441        assert_eq!(db.get("host"), Some(&Value::from("post=gres")));
442        assert_eq!(db.get("port"), Some(&Value::from(5432u64)));
443    }
444
445    #[test]
446    fn an_assignment_without_an_equals_names_itself() {
447        let error = Layer::new().set_assignments(["nonsense"]).unwrap_err();
448
449        assert!(error.to_string().contains("`nonsense`"), "{error}");
450    }
451
452    #[test]
453    fn an_unusable_path_is_rejected_at_the_call_site() {
454        let layer = Layer::new();
455
456        assert!(layer.set("", 1u16).is_err());
457        assert!(layer.set("a..b", 1u16).is_err());
458        assert!(layer.set(".a", 1u16).is_err());
459    }
460
461    #[test]
462    fn structured_values_survive_the_round_trip() {
463        #[derive(serde::Serialize)]
464        struct Pool {
465            max_size: u16,
466        }
467
468        let layer = Layer::new();
469        layer.set("pool", Pool { max_size: 7 }).unwrap();
470
471        let dict = layer.dict();
472        let Some(Value::Dict(_, pool)) = dict.get("pool") else {
473            panic!("expected a nested dict");
474        };
475
476        assert_eq!(pool.get("max_size"), Some(&Value::from(7u16)));
477    }
478}