Skip to main content

dynamic_config/
value.rs

1//! An owned mirror of the resolved configuration tree.
2//!
3//! A boundary that is not `serde` — a language binding, an exporter, a
4//! templating engine — needs the resolved values as *data*, not as a type
5//! to deserialize into. The underlying loader has such a tree, but its
6//! types are figment's, and this crate's public surface keeps figment
7//! behind [one deliberate door](crate::Source::provider). So the export is
8//! a small owned mirror: seven shapes, no lifetimes, no third-party types
9//! in the signature — and built by walking the resolved tree directly,
10//! never by a JSON round trip.
11//!
12//! # It is also the schemaless configuration
13//!
14//! [`Value`] implements `Deserialize`, which is the only bound the engine
15//! puts on a configuration type — so `Dynamic<Value>`, `Builder::values`
16//! and `load::<Value>` are a configuration with no struct behind it,
17//! reading by path instead of by field. Nothing else in the engine changes:
18//! the layering, the watcher, the cache and the reload hooks never knew
19//! what `T` was. See [the book's schemaless
20//! chapter](https://ctolon.github.io/dynamic-config/schemaless.html) for
21//! what a struct still buys that this does not.
22
23use std::collections::BTreeMap;
24
25use serde::de::DeserializeOwned;
26
27/// One resolved configuration value, owned.
28///
29/// What [`Snapshot::to_value`](crate::Snapshot::to_value) returns. This is
30/// configuration *handover*, not a diagnostic: real values, secrets
31/// included, exactly like deserializing into a struct — the paths-only
32/// rule governs what this crate prints, not what it hands the program.
33///
34/// Which is why `Debug` is hand-written and shape-only: the same data
35/// sits inside [`Snapshot`](crate::Snapshot), whose `Debug` prints keys
36/// and never values, and `{:?}` in a log line is exactly how resolved
37/// secrets leak. Read values through the enum; print them on purpose or
38/// not at all.
39///
40/// **There is deliberately no `Display`.** A schemaless configuration has
41/// no `#[config(secret)]` to derive a redaction list from, so a type that
42/// rendered itself into `{}` would put a password wherever a program
43/// formats a value it did not inspect — and it would do it in the one shape
44/// (`format!`, `write!`, a template) where nothing looks like a decision.
45/// The ways out are all explicit: the accessors, [`get_as`](Self::get_as),
46/// [`render`](Self::render) for a document, and `Serialize` for a
47/// serializer the caller chose.
48#[derive(Clone, PartialEq)]
49pub enum Value {
50    /// An explicit null (or unit) in a source.
51    Null,
52    /// A boolean.
53    Bool(bool),
54    /// Any integer a source can express.
55    ///
56    /// `i128`, so every `i64` and `u64` fits without a sign decision at
57    /// this boundary. The one unrepresentable case — a `u128` above
58    /// `i128::MAX` — arrives as [`Value::Float`], lossily; a configuration
59    /// value up there is measuring something no unit this crate knows
60    /// about.
61    Integer(i128),
62    /// A floating-point number.
63    Float(f64),
64    /// A string; a single character in a source arrives as one too.
65    String(String),
66    /// A sequence.
67    Array(Vec<Value>),
68    /// A table, keyed by field name.
69    Table(BTreeMap<String, Value>),
70}
71
72impl Value {
73    /// The value at a dotted `path` below this one, if every step exists.
74    ///
75    /// Steps are table keys; anything else — an array, a leaf — ends the
76    /// walk with `None`. The empty path is this value itself.
77    #[must_use]
78    pub fn get(&self, path: &str) -> Option<&Value> {
79        if path.is_empty() {
80            return Some(self);
81        }
82
83        path.split('.').try_fold(self, |value, step| match value {
84            Value::Table(table) => table.get(step),
85            _ => None,
86        })
87    }
88
89    /// The value at a dotted `path`, deserialized into `T`.
90    ///
91    /// The convenient door onto a schemaless configuration, and the more
92    /// expensive one. [`get`](Self::get) walks the tree and hands back a
93    /// borrow; this walks it, rebuilds the value figment's deserializer
94    /// wants and runs serde over it — **on every call**. Measured against
95    /// the borrowed read on the same machine in the same run
96    /// (`benches/read_path.rs`), that is around a third again as long for a
97    /// scalar, and it allocates whatever the value it hands back owns: a
98    /// number, nothing; a `String`, one.
99    ///
100    /// The bigger reason to prefer the accessors is not the nanoseconds but
101    /// the `Result`: `get_as` is a conversion that can fail at every read,
102    /// which is a diagnostic-grade shape. Use it where a value is read once
103    /// at startup or per reload — a serde type the accessors cannot express,
104    /// a `Vec<String>`, a struct for one sub-tree — and
105    /// [`get`](Self::get) plus [`as_i64`](Self::as_i64) and friends on a
106    /// request path. It is the same trade
107    /// [`Snapshot::get`](crate::Snapshot::get) makes, written down where the
108    /// schemaless reader will meet it.
109    ///
110    /// ```
111    /// # #[cfg(feature = "json")] {
112    /// use dynamic_config::{Format, Value};
113    ///
114    /// let document = Value::parse(r#"{"pool": {"max_size": 32}}"#, Format::Json).unwrap();
115    ///
116    /// assert_eq!(document.get_as::<u16>("pool.max_size").unwrap(), 32);
117    /// # }
118    /// ```
119    ///
120    /// # Errors
121    ///
122    /// [`ErrorKind::Missing`](crate::ErrorKind::Missing) when nothing
123    /// supplies `path` — including a path that walks *through* a scalar —
124    /// and [`ErrorKind::Type`](crate::ErrorKind::Type) when what is there
125    /// cannot become `T`. The message names the path and the kind of thing
126    /// that was there, never the value.
127    pub fn get_as<T: DeserializeOwned>(&self, path: &str) -> Result<T, crate::Error> {
128        let value = self.get(path).ok_or_else(|| {
129            crate::Error::new(crate::ErrorKind::Missing, "no value at this path").prepend_key(path)
130        })?;
131
132        // Through the crate's one translation, so a password typed into a
133        // numeric field does not come back inside ``found string "hunter2"``.
134        to_figment(value)
135            .deserialize()
136            .map_err(|error: figment::Error| crate::loader::translate(&error).prepend_key(path))
137    }
138
139    /// The boolean here, or `None` if this is anything else.
140    #[must_use]
141    pub fn as_bool(&self) -> Option<bool> {
142        match self {
143            Value::Bool(boolean) => Some(*boolean),
144            _ => None,
145        }
146    }
147
148    /// The integer here, at the width this crate stores it in.
149    ///
150    /// `None` for a float, even one that is a whole number: which of the two
151    /// a source wrote is part of the configuration here, and
152    /// [`Value::Integer`]'s `i128` is what makes that distinction free of a
153    /// sign decision.
154    #[must_use]
155    pub fn as_integer(&self) -> Option<i128> {
156        match self {
157            Value::Integer(number) => Some(*number),
158            _ => None,
159        }
160    }
161
162    /// The integer here as an `i64`, or `None` if it is not one or does not
163    /// fit.
164    ///
165    /// Narrowing rather than saturating: a port number that does not fit is
166    /// a configuration mistake, and a clamped one is that mistake made
167    /// silent.
168    #[must_use]
169    pub fn as_i64(&self) -> Option<i64> {
170        self.as_integer()
171            .and_then(|number| i64::try_from(number).ok())
172    }
173
174    /// The integer here as a `u64`, or `None` if it is not one, is negative,
175    /// or does not fit.
176    #[must_use]
177    pub fn as_u64(&self) -> Option<u64> {
178        self.as_integer()
179            .and_then(|number| u64::try_from(number).ok())
180    }
181
182    /// The float here, or `None` if this is anything else — an integer
183    /// included, for the reason [`as_integer`](Self::as_integer) gives.
184    #[must_use]
185    pub fn as_float(&self) -> Option<f64> {
186        match self {
187            Value::Float(number) => Some(*number),
188            _ => None,
189        }
190    }
191
192    /// The string here, borrowed, or `None` if this is anything else.
193    #[must_use]
194    pub fn as_str(&self) -> Option<&str> {
195        match self {
196            Value::String(text) => Some(text),
197            _ => None,
198        }
199    }
200
201    /// The sequence here, borrowed, or `None` if this is anything else.
202    #[must_use]
203    pub fn as_array(&self) -> Option<&[Value]> {
204        match self {
205            Value::Array(values) => Some(values),
206            _ => None,
207        }
208    }
209
210    /// The table here, borrowed, or `None` if this is anything else.
211    #[must_use]
212    pub fn as_table(&self) -> Option<&BTreeMap<String, Value>> {
213        match self {
214            Value::Table(table) => Some(table),
215            _ => None,
216        }
217    }
218
219    /// The dotted path of every leaf, in order.
220    ///
221    /// What a schemaless configuration has instead of a field list: the keys
222    /// that are actually there, learned at runtime. The same walk
223    /// [`Snapshot::leaf_paths`](crate::Snapshot::leaf_paths) performs, on
224    /// the tree a reader already holds — an array is a leaf, because its
225    /// elements are values rather than configuration keys, and so is an
226    /// empty table, which would otherwise vanish from the listing.
227    ///
228    /// Paths carry no values, so this is the one listing of a resolved
229    /// configuration that is safe to log.
230    ///
231    /// A tree that is not a table has no paths: a document has named keys at
232    /// its root.
233    #[must_use]
234    pub fn leaf_paths(&self) -> Vec<String> {
235        let mut paths = Vec::new();
236
237        if let Value::Table(table) = self {
238            let mut path = Vec::new();
239
240            for (key, value) in table {
241                path.push(key.clone());
242                leaves(value, &mut path, &mut paths);
243                path.pop();
244            }
245        }
246
247        paths
248    }
249
250    /// Parses one `format` document into a tree.
251    ///
252    /// The way in to the parsing this crate already owns, for code that has
253    /// documents to combine *before* the loader sees them: a store crate that
254    /// reads several keys under a prefix, a tool that folds a fragment
255    /// directory into one file. Without it the only way to merge two documents
256    /// outside this crate is to depend on `serde_json`, `toml` and `serde_yaml`
257    /// directly and reimplement what the `json` / `toml` / `yaml` features are
258    /// already compiling.
259    ///
260    /// No section mapping is applied: the result is the document as written,
261    /// top-level keys and all. Sections are what the *loader* does with a
262    /// document, and a merge happens below that line.
263    ///
264    /// ```
265    /// # #[cfg(feature = "json")] {
266    /// use dynamic_config::{Format, Value};
267    ///
268    /// let mut document = Value::parse(r#"{"db": {"host": "a"}}"#, Format::Json).unwrap();
269    /// document.merge(Value::parse(r#"{"db": {"port": 5432}}"#, Format::Json).unwrap());
270    ///
271    /// assert_eq!(document.get("db.host"), Some(&Value::String("a".into())));
272    /// assert_eq!(document.get("db.port"), Some(&Value::Integer(5432)));
273    /// # }
274    /// ```
275    ///
276    /// # Errors
277    ///
278    /// [`ErrorKind::Parse`](crate::ErrorKind::Parse) if the text is not a valid
279    /// `format` document, and [`ErrorKind::Backend`](crate::ErrorKind::Backend)
280    /// if this build has that format's feature off. The message is stripped the
281    /// same way every other backend failure here is — the key and the kind of
282    /// thing that was there, never the value.
283    pub fn parse(text: &str, format: crate::Format) -> Result<Self, crate::Error> {
284        crate::loader::parse_document(text, format).map(|document| {
285            Value::Table(
286                document
287                    .iter()
288                    .map(|(key, value)| (key.clone(), from_figment(value)))
289                    .collect(),
290            )
291        })
292    }
293
294    /// Merges `other` over this value: later wins, tables deep.
295    ///
296    /// The rule the crate already teaches for files, applied to two trees.
297    /// Where both sides have a table the merge descends into it; anywhere else
298    /// `other` replaces what was there, **arrays included** — a later document
299    /// supplying `tags = ["b"]` means those tags and not the earlier ones, which
300    /// is what every layer in this crate already means by it.
301    pub fn merge(&mut self, other: Value) {
302        match (self, other) {
303            (Value::Table(base), Value::Table(overlay)) => {
304                for (key, value) in overlay {
305                    match base.entry(key) {
306                        std::collections::btree_map::Entry::Occupied(mut existing) => {
307                            existing.get_mut().merge(value);
308                        }
309                        std::collections::btree_map::Entry::Vacant(empty) => {
310                            empty.insert(value);
311                        }
312                    }
313                }
314            }
315            (base, overlay) => *base = overlay,
316        }
317    }
318
319    /// Every leaf path both trees supply — what [`merge`](Self::merge) would
320    /// silently resolve.
321    ///
322    /// For the caller whose documents are meant to be *disjoint*: keys read
323    /// from a prefix are sections nobody intended to overlap, so an overlap
324    /// there is a deployment bug worth an error rather than a merge. Paths in
325    /// sorted order, and paths only — this is a diagnostic, so it names what
326    /// collided and never what either side held.
327    ///
328    /// A path where both sides hold a table is not a collision; the tables
329    /// merge. A path where either side holds an array or a scalar is.
330    #[must_use]
331    pub fn overlapping_paths(&self, other: &Value) -> Vec<String> {
332        let mut paths = Vec::new();
333
334        overlaps(self, other, &mut Vec::new(), &mut paths);
335        paths.sort();
336
337        paths
338    }
339
340    /// Renders this tree as the text of a `format` document.
341    ///
342    /// The way back out, so a merged tree can be handed to something that takes
343    /// text — [`Fetched::new`](crate::Fetched::new), a file, a socket.
344    ///
345    /// # Errors
346    ///
347    /// [`ErrorKind::Backend`](crate::ErrorKind::Backend) if this build has that
348    /// format's feature off, and [`ErrorKind::Type`](crate::ErrorKind::Type) if
349    /// the tree is not a table — a document has named keys at its root — or
350    /// holds something the format cannot express, such as a null in TOML.
351    pub fn render(&self, format: crate::Format) -> Result<String, crate::Error> {
352        let Value::Table(table) = self else {
353            return Err(crate::Error::new(
354                crate::ErrorKind::Type,
355                "only a table can be a document; this tree is a scalar or a list",
356            ));
357        };
358
359        let document = table
360            .iter()
361            .map(|(key, value)| (key.clone(), to_figment(value)))
362            .collect();
363
364        crate::write::render(&document, format)
365    }
366}
367
368/// Records the dotted path of every leaf below `value`.
369fn leaves(value: &Value, path: &mut Vec<String>, found: &mut Vec<String>) {
370    match value {
371        Value::Table(table) if !table.is_empty() => {
372            for (key, nested) in table {
373                path.push(key.clone());
374                leaves(nested, path, found);
375                path.pop();
376            }
377        }
378        _ => found.push(path.join(".")),
379    }
380}
381
382/// Records the dotted path of every leaf the two trees both supply.
383fn overlaps(left: &Value, right: &Value, path: &mut Vec<String>, found: &mut Vec<String>) {
384    let (Value::Table(left), Value::Table(right)) = (left, right) else {
385        found.push(path.join("."));
386        return;
387    };
388
389    for (key, value) in left {
390        let Some(other) = right.get(key) else {
391            continue;
392        };
393
394        path.push(key.clone());
395        overlaps(value, other, path, found);
396        path.pop();
397    }
398}
399
400/// Hand-written because `f64` is not `Hash`, and because what a *fingerprint*
401/// wants from a float is not what arithmetic wants: hashing through
402/// [`f64::to_bits`] makes `-0.0` and `0.0` hash differently, which is right
403/// here — they are different bytes in the file, and the cache's question is
404/// "is this the same document", not "is this the same number".
405///
406/// That is also why this is deliberately *not* consistent with `PartialEq`
407/// in the two places IEEE 754 is not: `-0.0 == 0.0` while their hashes
408/// differ, and no `NaN` equals itself while every `NaN` payload hashes
409/// stably. `Value` is not `Eq` for exactly those reasons, so there is no
410/// `Hash`/`Eq` contract to break.
411impl std::hash::Hash for Value {
412    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
413        // The discriminant first, so a shape change alone moves the hash:
414        // without it `Integer(1)` and a one-element table could collide
415        // through their payloads.
416        std::mem::discriminant(self).hash(state);
417
418        match self {
419            Self::Null => {}
420            Self::Bool(value) => value.hash(state),
421            Self::Integer(value) => value.hash(state),
422            Self::Float(value) => value.to_bits().hash(state),
423            Self::String(value) => value.hash(state),
424            Self::Array(values) => values.hash(state),
425            Self::Table(table) => table.hash(state),
426        }
427    }
428}
429
430/// The impl that makes a configuration with no struct behind it possible.
431///
432/// `DeserializeOwned` is the only bound the engine puts on a configuration
433/// type, so this one line is what turns `Dynamic<Value>`,
434/// [`Builder::values`](crate::Builder::values) and `load::<Value>` from
435/// "would not compile" into the schemaless shape — with layering, watching,
436/// the last-known-good cache and the reload hooks all working unchanged,
437/// because none of them ever knew what `T` was.
438///
439/// Deliberately `deserialize_any`: a configuration value is whatever the
440/// source said it was, which is the one place in serde where self-describing
441/// is the right answer. The two numeric edges match the walk in from the
442/// resolved tree exactly — every integer widens to `i128`, and the one
443/// unrepresentable case (a `u128` above `i128::MAX`) arrives as a float —
444/// so a value that reaches this type through serde and one that reaches it
445/// by walking the resolved tree are the same value.
446impl<'de> serde::Deserialize<'de> for Value {
447    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
448        deserializer.deserialize_any(AnyValue)
449    }
450}
451
452struct AnyValue;
453
454impl<'de> serde::de::Visitor<'de> for AnyValue {
455    type Value = Value;
456
457    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458        f.write_str("any configuration value")
459    }
460
461    fn visit_bool<E>(self, value: bool) -> Result<Value, E> {
462        Ok(Value::Bool(value))
463    }
464
465    fn visit_i64<E>(self, value: i64) -> Result<Value, E> {
466        Ok(Value::Integer(i128::from(value)))
467    }
468
469    fn visit_i128<E>(self, value: i128) -> Result<Value, E> {
470        Ok(Value::Integer(value))
471    }
472
473    fn visit_u64<E>(self, value: u64) -> Result<Value, E> {
474        Ok(Value::Integer(i128::from(value)))
475    }
476
477    fn visit_u128<E>(self, value: u128) -> Result<Value, E> {
478        // Lossy above `i128::MAX`, and the same lossy the walk from figment
479        // takes: a configuration value up there is measuring something no
480        // unit this crate knows about.
481        Ok(i128::try_from(value).map_or_else(|_| Value::Float(value as f64), Value::Integer))
482    }
483
484    fn visit_f64<E>(self, value: f64) -> Result<Value, E> {
485        Ok(Value::Float(value))
486    }
487
488    fn visit_char<E>(self, value: char) -> Result<Value, E> {
489        Ok(Value::String(value.to_string()))
490    }
491
492    fn visit_str<E>(self, value: &str) -> Result<Value, E> {
493        Ok(Value::String(value.to_owned()))
494    }
495
496    fn visit_string<E>(self, value: String) -> Result<Value, E> {
497        Ok(Value::String(value))
498    }
499
500    fn visit_unit<E>(self) -> Result<Value, E> {
501        Ok(Value::Null)
502    }
503
504    fn visit_none<E>(self) -> Result<Value, E> {
505        Ok(Value::Null)
506    }
507
508    fn visit_some<D: serde::Deserializer<'de>>(self, deserializer: D) -> Result<Value, D::Error> {
509        deserializer.deserialize_any(self)
510    }
511
512    fn visit_newtype_struct<D: serde::Deserializer<'de>>(
513        self,
514        deserializer: D,
515    ) -> Result<Value, D::Error> {
516        deserializer.deserialize_any(self)
517    }
518
519    fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Value, A::Error> {
520        let mut values = Vec::with_capacity(seq.size_hint().unwrap_or(0));
521
522        while let Some(value) = seq.next_element()? {
523            values.push(value);
524        }
525
526        Ok(Value::Array(values))
527    }
528
529    fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Value, A::Error> {
530        let mut table = BTreeMap::new();
531
532        // Keys are strings because configuration keys are: every format this
533        // crate reads spells them that way, and a non-string key here is a
534        // caller deserializing something that is not a configuration.
535        while let Some((key, value)) = map.next_entry::<String, Value>()? {
536            table.insert(key, value);
537        }
538
539        Ok(Value::Table(table))
540    }
541}
542
543/// The way back out through serde, for [`crate::save`] and
544/// [`crate::changed_paths`] — the two surfaces that take `T: Serialize` and
545/// would otherwise be the only ones a schemaless configuration could not
546/// reach.
547///
548/// Integers narrow exactly as the walk back out narrows them, and for the
549/// same reason: this type widens every integer on the way in so the boundary
550/// needs no sign decision, while a serializer does — `toml` refuses an
551/// `i128` whatever the number in it is.
552///
553/// This is *handover*, like [`crate::Snapshot::to_value`] and unlike
554/// [`Debug`]: it emits real values, secrets included, because that is what
555/// serializing a configuration means. The paths-only rule governs what this
556/// crate prints, not what a caller asks it to write.
557impl serde::Serialize for Value {
558    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
559        use serde::ser::{SerializeMap, SerializeSeq};
560
561        match self {
562            Value::Null => serializer.serialize_unit(),
563            Value::Bool(boolean) => serializer.serialize_bool(*boolean),
564            Value::Integer(number) => match (i64::try_from(*number), u64::try_from(*number)) {
565                (Ok(signed), _) => serializer.serialize_i64(signed),
566                (_, Ok(unsigned)) => serializer.serialize_u64(unsigned),
567                _ => serializer.serialize_i128(*number),
568            },
569            Value::Float(number) => serializer.serialize_f64(*number),
570            Value::String(text) => serializer.serialize_str(text),
571            Value::Array(values) => {
572                let mut sequence = serializer.serialize_seq(Some(values.len()))?;
573
574                for value in values {
575                    sequence.serialize_element(value)?;
576                }
577
578                sequence.end()
579            }
580            Value::Table(table) => {
581                let mut map = serializer.serialize_map(Some(table.len()))?;
582
583                for (key, value) in table {
584                    map.serialize_entry(key, value)?;
585                }
586
587                map.end()
588            }
589        }
590    }
591}
592
593impl std::fmt::Debug for Value {
594    /// Shape and keys, never values — the line every diagnostic in this
595    /// crate holds, held here too because `to_value` hands over the same
596    /// secret-bearing data `Snapshot` guards.
597    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
598        match self {
599            Self::Null => f.write_str("Null"),
600            Self::Bool(_) => f.write_str("Bool(***)"),
601            Self::Integer(_) => f.write_str("Integer(***)"),
602            Self::Float(_) => f.write_str("Float(***)"),
603            Self::String(_) => f.write_str("String(***)"),
604            Self::Array(values) => f.debug_list().entries(values.iter()).finish(),
605            Self::Table(table) => f.debug_map().entries(table.iter()).finish(),
606        }
607    }
608}
609
610/// The walk from figment's tree, tags dropped, no serialization involved.
611pub(crate) fn from_figment(value: &figment::value::Value) -> Value {
612    use figment::value::{Empty, Num};
613
614    match value {
615        figment::value::Value::String(_, string) => Value::String(string.clone()),
616        figment::value::Value::Char(_, character) => Value::String(character.to_string()),
617        figment::value::Value::Bool(_, boolean) => Value::Bool(*boolean),
618        figment::value::Value::Num(_, number) => match number {
619            Num::U8(n) => Value::Integer(i128::from(*n)),
620            Num::U16(n) => Value::Integer(i128::from(*n)),
621            Num::U32(n) => Value::Integer(i128::from(*n)),
622            Num::U64(n) => Value::Integer(i128::from(*n)),
623            Num::USize(n) => Value::Integer(*n as i128),
624            Num::U128(n) => i128::try_from(*n)
625                .map(Value::Integer)
626                .unwrap_or(Value::Float(*n as f64)),
627            Num::I8(n) => Value::Integer(i128::from(*n)),
628            Num::I16(n) => Value::Integer(i128::from(*n)),
629            Num::I32(n) => Value::Integer(i128::from(*n)),
630            Num::I64(n) => Value::Integer(i128::from(*n)),
631            Num::ISize(n) => Value::Integer(*n as i128),
632            Num::I128(n) => Value::Integer(*n),
633            Num::F32(n) => Value::Float(f64::from(*n)),
634            Num::F64(n) => Value::Float(*n),
635        },
636        figment::value::Value::Empty(_, Empty::None | Empty::Unit) => Value::Null,
637        figment::value::Value::Dict(_, dict) => Value::Table(
638            dict.iter()
639                .map(|(key, value)| (key.clone(), from_figment(value)))
640                .collect(),
641        ),
642        figment::value::Value::Array(_, values) => {
643            Value::Array(values.iter().map(from_figment).collect())
644        }
645    }
646}
647
648/// The walk back, for [`Value::render`]: this crate's serializers all take
649/// figment's tree, and one of them is what [`crate::save`] already writes with.
650///
651/// Every value is tagged [`Tag::Default`](figment::value::Tag::Default) —
652/// a tag records which provider supplied a value, and a tree assembled by a
653/// caller was supplied by none of them.
654fn to_figment(value: &Value) -> figment::value::Value {
655    use figment::value::{Empty, Num, Tag};
656
657    match value {
658        Value::Null => figment::value::Value::Empty(Tag::Default, Empty::None),
659        Value::Bool(boolean) => figment::value::Value::Bool(Tag::Default, *boolean),
660        // Narrowed rather than emitted as `I128`: `Value` widens every integer
661        // on the way in so the boundary needs no sign decision, but a
662        // serializer does — `toml` refuses an `i128` outright, whatever the
663        // number in it is. Signed first, so a round trip through a format that
664        // has one integer type comes back the width it went in as.
665        Value::Integer(number) => figment::value::Value::Num(
666            Tag::Default,
667            i64::try_from(*number).map_or_else(
668                |_| u64::try_from(*number).map_or(Num::I128(*number), Num::U64),
669                Num::I64,
670            ),
671        ),
672        Value::Float(number) => figment::value::Value::Num(Tag::Default, Num::F64(*number)),
673        Value::String(text) => figment::value::Value::String(Tag::Default, text.clone()),
674        Value::Array(values) => {
675            figment::value::Value::Array(Tag::Default, values.iter().map(to_figment).collect())
676        }
677        Value::Table(table) => figment::value::Value::Dict(
678            Tag::Default,
679            table
680                .iter()
681                .map(|(key, value)| (key.clone(), to_figment(value)))
682                .collect(),
683        ),
684    }
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690
691    #[test]
692    fn the_walk_preserves_shape_and_numbers() {
693        let source: figment::value::Value = figment::value::Value::serialize(serde_json::json!({
694            "port": 5432,
695            "ratio": 0.5,
696            "tls": true,
697            "host": "db",
698            "tags": ["a", "b"],
699            "pool": { "max": 8 },
700        }))
701        .expect("a literal serializes");
702
703        let value = from_figment(&source);
704
705        assert_eq!(value.get("port"), Some(&Value::Integer(5432)));
706        assert_eq!(value.get("ratio"), Some(&Value::Float(0.5)));
707        assert_eq!(value.get("tls"), Some(&Value::Bool(true)));
708        assert_eq!(value.get("host"), Some(&Value::String("db".into())));
709        assert_eq!(value.get("pool.max"), Some(&Value::Integer(8)));
710        assert_eq!(
711            value.get("tags"),
712            Some(&Value::Array(vec![
713                Value::String("a".into()),
714                Value::String("b".into())
715            ]))
716        );
717    }
718
719    fn hash_of(value: &Value) -> u64 {
720        use std::hash::{Hash, Hasher};
721
722        let mut hasher = std::collections::hash_map::DefaultHasher::new();
723        value.hash(&mut hasher);
724
725        hasher.finish()
726    }
727
728    /// The cache's identity is a hash of this tree, so a value that is the
729    /// same document has to hash the same however it was assembled...
730    #[test]
731    fn an_equal_tree_hashes_equal() {
732        let one = Value::Table(BTreeMap::from([
733            ("host".to_owned(), Value::String("db".to_owned())),
734            ("port".to_owned(), Value::Integer(5432)),
735        ]));
736        let two = one.clone();
737
738        assert_eq!(hash_of(&one), hash_of(&two));
739    }
740
741    /// ...and a different document has to hash differently, including the
742    /// two cases a numeric comparison would call equal.
743    #[test]
744    fn a_signed_zero_is_a_different_document() {
745        assert_eq!(Value::Float(-0.0), Value::Float(0.0), "as numbers");
746        assert_ne!(
747            hash_of(&Value::Float(-0.0)),
748            hash_of(&Value::Float(0.0)),
749            "as bytes in a file, which is what a fingerprint answers for"
750        );
751    }
752
753    #[test]
754    fn a_whole_number_is_not_the_float_that_prints_the_same() {
755        assert_ne!(Value::Integer(1), Value::Float(1.0));
756        assert_ne!(hash_of(&Value::Integer(1)), hash_of(&Value::Float(1.0)));
757    }
758
759    /// The walk out narrows, so the walk back in has to widen to the same
760    /// number — otherwise a round trip through the seam quietly changes the
761    /// document, which the cache's fingerprint would then call a reload.
762    #[test]
763    fn the_walk_back_narrows_without_changing_the_number() {
764        for number in [
765            0,
766            1,
767            -1,
768            i128::from(i64::MIN),
769            i128::from(u64::MAX),
770            i128::MAX,
771        ] {
772            assert_eq!(
773                from_figment(&to_figment(&Value::Integer(number))),
774                Value::Integer(number),
775                "{number}"
776            );
777        }
778    }
779
780    #[test]
781    fn the_walk_back_preserves_every_shape() {
782        let tree = Value::Table(BTreeMap::from([
783            ("null".to_owned(), Value::Null),
784            ("bool".to_owned(), Value::Bool(true)),
785            ("float".to_owned(), Value::Float(0.5)),
786            ("text".to_owned(), Value::String("a".to_owned())),
787            (
788                "list".to_owned(),
789                Value::Array(vec![Value::Integer(1), Value::Null]),
790            ),
791            (
792                "table".to_owned(),
793                Value::Table(BTreeMap::from([("nested".to_owned(), Value::Integer(2))])),
794            ),
795        ]));
796
797        assert_eq!(from_figment(&to_figment(&tree)), tree);
798    }
799
800    /// Reachable by hand, so it says which feature is missing rather than
801    /// failing in a way that reads like a malformed document.
802    #[cfg(not(any(feature = "json", feature = "toml", feature = "yaml")))]
803    #[test]
804    fn a_format_this_build_cannot_read_names_its_feature() {
805        let error = Value::parse("{}", crate::Format::Json).expect_err("no format is enabled");
806
807        assert_eq!(error.kind(), crate::ErrorKind::Backend);
808        assert!(error.message().contains("json"), "{error}");
809
810        let error = Value::Table(BTreeMap::new())
811            .render(crate::Format::Json)
812            .expect_err("no format is enabled");
813
814        assert_eq!(error.kind(), crate::ErrorKind::Backend);
815    }
816
817    /// The `Deserialize` impl is what makes `Dynamic<Value>` compile, so the
818    /// property that matters is that it agrees with the walk: a value that
819    /// arrives through serde and one that arrives by walking the resolved
820    /// tree must be the same value, or the schemaless configuration and the
821    /// exported one disagree about what was in the file.
822    #[test]
823    fn the_serde_road_and_the_walk_agree() {
824        let source: figment::value::Value = figment::value::Value::serialize(serde_json::json!({
825            "port": 5432,
826            "ratio": 0.5,
827            "tls": true,
828            "host": "db",
829            "nothing": (),
830            "tags": ["a", { "nested": 1 }],
831            "pool": { "max": 8, "empty": {} },
832        }))
833        .expect("a literal serializes");
834
835        assert_eq!(
836            from_figment(&source),
837            source.deserialize::<Value>().expect("any value is a Value"),
838        );
839    }
840
841    /// The two numeric edges the walk documents, held on the serde road too.
842    #[test]
843    fn the_serde_road_widens_and_gives_up_at_the_same_places() {
844        use serde::de::value::{Error, I128Deserializer, U128Deserializer, UnitDeserializer};
845        use serde::Deserialize as _;
846
847        let signed = |number| Value::deserialize(I128Deserializer::<Error>::new(number));
848        let unsigned = |number| Value::deserialize(U128Deserializer::<Error>::new(number));
849
850        assert_eq!(signed(i128::MIN).unwrap(), Value::Integer(i128::MIN));
851        assert_eq!(
852            unsigned(u128::try_from(i128::MAX).expect("in range")).unwrap(),
853            Value::Integer(i128::MAX)
854        );
855        assert_eq!(
856            unsigned(u128::MAX).unwrap(),
857            Value::Float(u128::MAX as f64),
858            "the one unrepresentable case arrives lossily, as the walk does it"
859        );
860
861        // A document is a table, but a value below one need not be.
862        assert_eq!(
863            Value::deserialize(UnitDeserializer::<Error>::new()).unwrap(),
864            Value::Null
865        );
866    }
867
868    /// Serializing narrows exactly as the walk out does, so a tree that went
869    /// through serde survives a format with one integer type.
870    #[test]
871    fn serializing_narrows_the_way_the_walk_out_narrows() {
872        for number in [0, 1, -1, i128::from(i64::MIN), i128::from(u64::MAX)] {
873            let rendered =
874                serde_json::to_string(&Value::Integer(number)).expect("a number serializes");
875
876            assert_eq!(rendered, number.to_string());
877        }
878
879        let tree = Value::Table(BTreeMap::from([
880            ("null".to_owned(), Value::Null),
881            ("ratio".to_owned(), Value::Float(0.5)),
882            (
883                "tags".to_owned(),
884                Value::Array(vec![Value::String("a".to_owned())]),
885            ),
886        ]));
887
888        assert_eq!(
889            serde_json::to_value(&tree)
890                .expect("a tree serializes")
891                .to_string(),
892            r#"{"null":null,"ratio":0.5,"tags":["a"]}"#
893        );
894
895        // And back: the round trip through serde is the identity, which is
896        // what `save` + a reload amounts to.
897        assert_eq!(
898            serde_json::from_str::<Value>(&serde_json::to_string(&tree).unwrap()).unwrap(),
899            tree
900        );
901    }
902
903    #[test]
904    fn a_typed_read_reports_the_path_and_the_kind_that_was_there() {
905        let tree = Value::Table(BTreeMap::from([(
906            "pool".to_owned(),
907            Value::Table(BTreeMap::from([(
908                "max".to_owned(),
909                Value::String("not-a-number".to_owned()),
910            )])),
911        )]));
912
913        assert_eq!(
914            tree.get_as::<u16>("pool.max").unwrap_err().kind(),
915            crate::ErrorKind::Type
916        );
917        assert_eq!(
918            tree.get_as::<u16>("pool.max").unwrap_err().path(),
919            "pool.max"
920        );
921        assert_eq!(
922            tree.get_as::<u16>("pool.min").unwrap_err().kind(),
923            crate::ErrorKind::Missing
924        );
925        assert_eq!(tree.get_as::<String>("pool.max").unwrap(), "not-a-number");
926    }
927
928    #[test]
929    fn leaf_paths_stops_at_arrays_and_keeps_empty_tables() {
930        let tree = Value::Table(BTreeMap::from([
931            ("host".to_owned(), Value::String("db".to_owned())),
932            ("empty".to_owned(), Value::Table(BTreeMap::new())),
933            (
934                "tags".to_owned(),
935                Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
936            ),
937            (
938                "pool".to_owned(),
939                Value::Table(BTreeMap::from([("max".to_owned(), Value::Integer(8))])),
940            ),
941        ]));
942
943        assert_eq!(tree.leaf_paths(), ["empty", "host", "pool.max", "tags"]);
944    }
945
946    #[test]
947    fn a_step_through_a_leaf_is_none_and_the_empty_path_is_identity() {
948        let value = Value::Table(BTreeMap::from([("port".to_owned(), Value::Integer(1))]));
949
950        assert_eq!(value.get("port.deeper"), None);
951        assert_eq!(value.get("missing"), None);
952        assert_eq!(value.get(""), Some(&value));
953    }
954}