1use std::collections::BTreeMap;
13
14#[derive(Clone, PartialEq)]
27pub enum Value {
28 Null,
30 Bool(bool),
32 Integer(i128),
40 Float(f64),
42 String(String),
44 Array(Vec<Value>),
46 Table(BTreeMap<String, Value>),
48}
49
50impl Value {
51 #[must_use]
56 pub fn get(&self, path: &str) -> Option<&Value> {
57 if path.is_empty() {
58 return Some(self);
59 }
60
61 path.split('.').try_fold(self, |value, step| match value {
62 Value::Table(table) => table.get(step),
63 _ => None,
64 })
65 }
66}
67
68impl std::fmt::Debug for Value {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match self {
74 Self::Null => f.write_str("Null"),
75 Self::Bool(_) => f.write_str("Bool(***)"),
76 Self::Integer(_) => f.write_str("Integer(***)"),
77 Self::Float(_) => f.write_str("Float(***)"),
78 Self::String(_) => f.write_str("String(***)"),
79 Self::Array(values) => f.debug_list().entries(values.iter()).finish(),
80 Self::Table(table) => f.debug_map().entries(table.iter()).finish(),
81 }
82 }
83}
84
85pub(crate) fn from_figment(value: &figment::value::Value) -> Value {
87 use figment::value::{Empty, Num};
88
89 match value {
90 figment::value::Value::String(_, string) => Value::String(string.clone()),
91 figment::value::Value::Char(_, character) => Value::String(character.to_string()),
92 figment::value::Value::Bool(_, boolean) => Value::Bool(*boolean),
93 figment::value::Value::Num(_, number) => match number {
94 Num::U8(n) => Value::Integer(i128::from(*n)),
95 Num::U16(n) => Value::Integer(i128::from(*n)),
96 Num::U32(n) => Value::Integer(i128::from(*n)),
97 Num::U64(n) => Value::Integer(i128::from(*n)),
98 Num::USize(n) => Value::Integer(*n as i128),
99 Num::U128(n) => i128::try_from(*n)
100 .map(Value::Integer)
101 .unwrap_or(Value::Float(*n as f64)),
102 Num::I8(n) => Value::Integer(i128::from(*n)),
103 Num::I16(n) => Value::Integer(i128::from(*n)),
104 Num::I32(n) => Value::Integer(i128::from(*n)),
105 Num::I64(n) => Value::Integer(i128::from(*n)),
106 Num::ISize(n) => Value::Integer(*n as i128),
107 Num::I128(n) => Value::Integer(*n),
108 Num::F32(n) => Value::Float(f64::from(*n)),
109 Num::F64(n) => Value::Float(*n),
110 },
111 figment::value::Value::Empty(_, Empty::None | Empty::Unit) => Value::Null,
112 figment::value::Value::Dict(_, dict) => Value::Table(
113 dict.iter()
114 .map(|(key, value)| (key.clone(), from_figment(value)))
115 .collect(),
116 ),
117 figment::value::Value::Array(_, values) => {
118 Value::Array(values.iter().map(from_figment).collect())
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn the_walk_preserves_shape_and_numbers() {
129 let source: figment::value::Value = figment::value::Value::serialize(serde_json::json!({
130 "port": 5432,
131 "ratio": 0.5,
132 "tls": true,
133 "host": "db",
134 "tags": ["a", "b"],
135 "pool": { "max": 8 },
136 }))
137 .expect("a literal serializes");
138
139 let value = from_figment(&source);
140
141 assert_eq!(value.get("port"), Some(&Value::Integer(5432)));
142 assert_eq!(value.get("ratio"), Some(&Value::Float(0.5)));
143 assert_eq!(value.get("tls"), Some(&Value::Bool(true)));
144 assert_eq!(value.get("host"), Some(&Value::String("db".into())));
145 assert_eq!(value.get("pool.max"), Some(&Value::Integer(8)));
146 assert_eq!(
147 value.get("tags"),
148 Some(&Value::Array(vec![
149 Value::String("a".into()),
150 Value::String("b".into())
151 ]))
152 );
153 }
154
155 #[test]
156 fn a_step_through_a_leaf_is_none_and_the_empty_path_is_identity() {
157 let value = Value::Table(BTreeMap::from([("port".to_owned(), Value::Integer(1))]));
158
159 assert_eq!(value.get("port.deeper"), None);
160 assert_eq!(value.get("missing"), None);
161 assert_eq!(value.get(""), Some(&value));
162 }
163}