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}