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