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}