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