use std::collections::BTreeMap;
use serde::de::DeserializeOwned;
#[derive(Clone, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Integer(i128),
Float(f64),
String(String),
Array(Vec<Value>),
Table(BTreeMap<String, Value>),
}
impl Value {
#[must_use]
pub fn get(&self, path: &str) -> Option<&Value> {
if path.is_empty() {
return Some(self);
}
path.split('.').try_fold(self, |value, step| match value {
Value::Table(table) => table.get(step),
_ => None,
})
}
pub fn get_as<T: DeserializeOwned>(&self, path: &str) -> Result<T, crate::Error> {
let value = self.get(path).ok_or_else(|| {
crate::Error::new(crate::ErrorKind::Missing, "no value at this path").prepend_key(path)
})?;
to_figment(value)
.deserialize()
.map_err(|error: figment::Error| crate::loader::translate(&error).prepend_key(path))
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
Value::Bool(boolean) => Some(*boolean),
_ => None,
}
}
#[must_use]
pub fn as_integer(&self) -> Option<i128> {
match self {
Value::Integer(number) => Some(*number),
_ => None,
}
}
#[must_use]
pub fn as_i64(&self) -> Option<i64> {
self.as_integer()
.and_then(|number| i64::try_from(number).ok())
}
#[must_use]
pub fn as_u64(&self) -> Option<u64> {
self.as_integer()
.and_then(|number| u64::try_from(number).ok())
}
#[must_use]
pub fn as_float(&self) -> Option<f64> {
match self {
Value::Float(number) => Some(*number),
_ => None,
}
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Value::String(text) => Some(text),
_ => None,
}
}
#[must_use]
pub fn as_array(&self) -> Option<&[Value]> {
match self {
Value::Array(values) => Some(values),
_ => None,
}
}
#[must_use]
pub fn as_table(&self) -> Option<&BTreeMap<String, Value>> {
match self {
Value::Table(table) => Some(table),
_ => None,
}
}
#[must_use]
pub fn leaf_paths(&self) -> Vec<String> {
let mut paths = Vec::new();
if let Value::Table(table) = self {
let mut path = Vec::new();
for (key, value) in table {
path.push(key.clone());
leaves(value, &mut path, &mut paths);
path.pop();
}
}
paths
}
pub fn parse(text: &str, format: crate::Format) -> Result<Self, crate::Error> {
crate::loader::parse_document(text, format).map(|document| {
Value::Table(
document
.iter()
.map(|(key, value)| (key.clone(), from_figment(value)))
.collect(),
)
})
}
pub fn merge(&mut self, other: Value) {
match (self, other) {
(Value::Table(base), Value::Table(overlay)) => {
for (key, value) in overlay {
match base.entry(key) {
std::collections::btree_map::Entry::Occupied(mut existing) => {
existing.get_mut().merge(value);
}
std::collections::btree_map::Entry::Vacant(empty) => {
empty.insert(value);
}
}
}
}
(base, overlay) => *base = overlay,
}
}
#[must_use]
pub fn overlapping_paths(&self, other: &Value) -> Vec<String> {
let mut paths = Vec::new();
overlaps(self, other, &mut Vec::new(), &mut paths);
paths.sort();
paths
}
pub fn render(&self, format: crate::Format) -> Result<String, crate::Error> {
let Value::Table(table) = self else {
return Err(crate::Error::new(
crate::ErrorKind::Type,
"only a table can be a document; this tree is a scalar or a list",
));
};
let document = table
.iter()
.map(|(key, value)| (key.clone(), to_figment(value)))
.collect();
crate::write::render(&document, format)
}
}
fn leaves(value: &Value, path: &mut Vec<String>, found: &mut Vec<String>) {
match value {
Value::Table(table) if !table.is_empty() => {
for (key, nested) in table {
path.push(key.clone());
leaves(nested, path, found);
path.pop();
}
}
_ => found.push(path.join(".")),
}
}
fn overlaps(left: &Value, right: &Value, path: &mut Vec<String>, found: &mut Vec<String>) {
let (Value::Table(left), Value::Table(right)) = (left, right) else {
found.push(path.join("."));
return;
};
for (key, value) in left {
let Some(other) = right.get(key) else {
continue;
};
path.push(key.clone());
overlaps(value, other, path, found);
path.pop();
}
}
impl std::hash::Hash for Value {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Self::Null => {}
Self::Bool(value) => value.hash(state),
Self::Integer(value) => value.hash(state),
Self::Float(value) => value.to_bits().hash(state),
Self::String(value) => value.hash(state),
Self::Array(values) => values.hash(state),
Self::Table(table) => table.hash(state),
}
}
}
impl<'de> serde::Deserialize<'de> for Value {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(AnyValue)
}
}
struct AnyValue;
impl<'de> serde::de::Visitor<'de> for AnyValue {
type Value = Value;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("any configuration value")
}
fn visit_bool<E>(self, value: bool) -> Result<Value, E> {
Ok(Value::Bool(value))
}
fn visit_i64<E>(self, value: i64) -> Result<Value, E> {
Ok(Value::Integer(i128::from(value)))
}
fn visit_i128<E>(self, value: i128) -> Result<Value, E> {
Ok(Value::Integer(value))
}
fn visit_u64<E>(self, value: u64) -> Result<Value, E> {
Ok(Value::Integer(i128::from(value)))
}
fn visit_u128<E>(self, value: u128) -> Result<Value, E> {
Ok(i128::try_from(value).map_or_else(|_| Value::Float(value as f64), Value::Integer))
}
fn visit_f64<E>(self, value: f64) -> Result<Value, E> {
Ok(Value::Float(value))
}
fn visit_char<E>(self, value: char) -> Result<Value, E> {
Ok(Value::String(value.to_string()))
}
fn visit_str<E>(self, value: &str) -> Result<Value, E> {
Ok(Value::String(value.to_owned()))
}
fn visit_string<E>(self, value: String) -> Result<Value, E> {
Ok(Value::String(value))
}
fn visit_unit<E>(self) -> Result<Value, E> {
Ok(Value::Null)
}
fn visit_none<E>(self) -> Result<Value, E> {
Ok(Value::Null)
}
fn visit_some<D: serde::Deserializer<'de>>(self, deserializer: D) -> Result<Value, D::Error> {
deserializer.deserialize_any(self)
}
fn visit_newtype_struct<D: serde::Deserializer<'de>>(
self,
deserializer: D,
) -> Result<Value, D::Error> {
deserializer.deserialize_any(self)
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Value, A::Error> {
let mut values = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(value) = seq.next_element()? {
values.push(value);
}
Ok(Value::Array(values))
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Value, A::Error> {
let mut table = BTreeMap::new();
while let Some((key, value)) = map.next_entry::<String, Value>()? {
table.insert(key, value);
}
Ok(Value::Table(table))
}
}
impl serde::Serialize for Value {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::{SerializeMap, SerializeSeq};
match self {
Value::Null => serializer.serialize_unit(),
Value::Bool(boolean) => serializer.serialize_bool(*boolean),
Value::Integer(number) => match (i64::try_from(*number), u64::try_from(*number)) {
(Ok(signed), _) => serializer.serialize_i64(signed),
(_, Ok(unsigned)) => serializer.serialize_u64(unsigned),
_ => serializer.serialize_i128(*number),
},
Value::Float(number) => serializer.serialize_f64(*number),
Value::String(text) => serializer.serialize_str(text),
Value::Array(values) => {
let mut sequence = serializer.serialize_seq(Some(values.len()))?;
for value in values {
sequence.serialize_element(value)?;
}
sequence.end()
}
Value::Table(table) => {
let mut map = serializer.serialize_map(Some(table.len()))?;
for (key, value) in table {
map.serialize_entry(key, value)?;
}
map.end()
}
}
}
}
impl std::fmt::Debug for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Null => f.write_str("Null"),
Self::Bool(_) => f.write_str("Bool(***)"),
Self::Integer(_) => f.write_str("Integer(***)"),
Self::Float(_) => f.write_str("Float(***)"),
Self::String(_) => f.write_str("String(***)"),
Self::Array(values) => f.debug_list().entries(values.iter()).finish(),
Self::Table(table) => f.debug_map().entries(table.iter()).finish(),
}
}
}
pub(crate) fn from_figment(value: &figment::value::Value) -> Value {
use figment::value::{Empty, Num};
match value {
figment::value::Value::String(_, string) => Value::String(string.clone()),
figment::value::Value::Char(_, character) => Value::String(character.to_string()),
figment::value::Value::Bool(_, boolean) => Value::Bool(*boolean),
figment::value::Value::Num(_, number) => match number {
Num::U8(n) => Value::Integer(i128::from(*n)),
Num::U16(n) => Value::Integer(i128::from(*n)),
Num::U32(n) => Value::Integer(i128::from(*n)),
Num::U64(n) => Value::Integer(i128::from(*n)),
Num::USize(n) => Value::Integer(*n as i128),
Num::U128(n) => i128::try_from(*n)
.map(Value::Integer)
.unwrap_or(Value::Float(*n as f64)),
Num::I8(n) => Value::Integer(i128::from(*n)),
Num::I16(n) => Value::Integer(i128::from(*n)),
Num::I32(n) => Value::Integer(i128::from(*n)),
Num::I64(n) => Value::Integer(i128::from(*n)),
Num::ISize(n) => Value::Integer(*n as i128),
Num::I128(n) => Value::Integer(*n),
Num::F32(n) => Value::Float(f64::from(*n)),
Num::F64(n) => Value::Float(*n),
},
figment::value::Value::Empty(_, Empty::None | Empty::Unit) => Value::Null,
figment::value::Value::Dict(_, dict) => Value::Table(
dict.iter()
.map(|(key, value)| (key.clone(), from_figment(value)))
.collect(),
),
figment::value::Value::Array(_, values) => {
Value::Array(values.iter().map(from_figment).collect())
}
}
}
fn to_figment(value: &Value) -> figment::value::Value {
use figment::value::{Empty, Num, Tag};
match value {
Value::Null => figment::value::Value::Empty(Tag::Default, Empty::None),
Value::Bool(boolean) => figment::value::Value::Bool(Tag::Default, *boolean),
Value::Integer(number) => figment::value::Value::Num(
Tag::Default,
i64::try_from(*number).map_or_else(
|_| u64::try_from(*number).map_or(Num::I128(*number), Num::U64),
Num::I64,
),
),
Value::Float(number) => figment::value::Value::Num(Tag::Default, Num::F64(*number)),
Value::String(text) => figment::value::Value::String(Tag::Default, text.clone()),
Value::Array(values) => {
figment::value::Value::Array(Tag::Default, values.iter().map(to_figment).collect())
}
Value::Table(table) => figment::value::Value::Dict(
Tag::Default,
table
.iter()
.map(|(key, value)| (key.clone(), to_figment(value)))
.collect(),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_walk_preserves_shape_and_numbers() {
let source: figment::value::Value = figment::value::Value::serialize(serde_json::json!({
"port": 5432,
"ratio": 0.5,
"tls": true,
"host": "db",
"tags": ["a", "b"],
"pool": { "max": 8 },
}))
.expect("a literal serializes");
let value = from_figment(&source);
assert_eq!(value.get("port"), Some(&Value::Integer(5432)));
assert_eq!(value.get("ratio"), Some(&Value::Float(0.5)));
assert_eq!(value.get("tls"), Some(&Value::Bool(true)));
assert_eq!(value.get("host"), Some(&Value::String("db".into())));
assert_eq!(value.get("pool.max"), Some(&Value::Integer(8)));
assert_eq!(
value.get("tags"),
Some(&Value::Array(vec![
Value::String("a".into()),
Value::String("b".into())
]))
);
}
fn hash_of(value: &Value) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
#[test]
fn an_equal_tree_hashes_equal() {
let one = Value::Table(BTreeMap::from([
("host".to_owned(), Value::String("db".to_owned())),
("port".to_owned(), Value::Integer(5432)),
]));
let two = one.clone();
assert_eq!(hash_of(&one), hash_of(&two));
}
#[test]
fn a_signed_zero_is_a_different_document() {
assert_eq!(Value::Float(-0.0), Value::Float(0.0), "as numbers");
assert_ne!(
hash_of(&Value::Float(-0.0)),
hash_of(&Value::Float(0.0)),
"as bytes in a file, which is what a fingerprint answers for"
);
}
#[test]
fn a_whole_number_is_not_the_float_that_prints_the_same() {
assert_ne!(Value::Integer(1), Value::Float(1.0));
assert_ne!(hash_of(&Value::Integer(1)), hash_of(&Value::Float(1.0)));
}
#[test]
fn the_walk_back_narrows_without_changing_the_number() {
for number in [
0,
1,
-1,
i128::from(i64::MIN),
i128::from(u64::MAX),
i128::MAX,
] {
assert_eq!(
from_figment(&to_figment(&Value::Integer(number))),
Value::Integer(number),
"{number}"
);
}
}
#[test]
fn the_walk_back_preserves_every_shape() {
let tree = Value::Table(BTreeMap::from([
("null".to_owned(), Value::Null),
("bool".to_owned(), Value::Bool(true)),
("float".to_owned(), Value::Float(0.5)),
("text".to_owned(), Value::String("a".to_owned())),
(
"list".to_owned(),
Value::Array(vec![Value::Integer(1), Value::Null]),
),
(
"table".to_owned(),
Value::Table(BTreeMap::from([("nested".to_owned(), Value::Integer(2))])),
),
]));
assert_eq!(from_figment(&to_figment(&tree)), tree);
}
#[cfg(not(any(feature = "json", feature = "toml", feature = "yaml")))]
#[test]
fn a_format_this_build_cannot_read_names_its_feature() {
let error = Value::parse("{}", crate::Format::Json).expect_err("no format is enabled");
assert_eq!(error.kind(), crate::ErrorKind::Backend);
assert!(error.message().contains("json"), "{error}");
let error = Value::Table(BTreeMap::new())
.render(crate::Format::Json)
.expect_err("no format is enabled");
assert_eq!(error.kind(), crate::ErrorKind::Backend);
}
#[test]
fn the_serde_road_and_the_walk_agree() {
let source: figment::value::Value = figment::value::Value::serialize(serde_json::json!({
"port": 5432,
"ratio": 0.5,
"tls": true,
"host": "db",
"nothing": (),
"tags": ["a", { "nested": 1 }],
"pool": { "max": 8, "empty": {} },
}))
.expect("a literal serializes");
assert_eq!(
from_figment(&source),
source.deserialize::<Value>().expect("any value is a Value"),
);
}
#[test]
fn the_serde_road_widens_and_gives_up_at_the_same_places() {
use serde::de::value::{Error, I128Deserializer, U128Deserializer, UnitDeserializer};
use serde::Deserialize as _;
let signed = |number| Value::deserialize(I128Deserializer::<Error>::new(number));
let unsigned = |number| Value::deserialize(U128Deserializer::<Error>::new(number));
assert_eq!(signed(i128::MIN).unwrap(), Value::Integer(i128::MIN));
assert_eq!(
unsigned(u128::try_from(i128::MAX).expect("in range")).unwrap(),
Value::Integer(i128::MAX)
);
assert_eq!(
unsigned(u128::MAX).unwrap(),
Value::Float(u128::MAX as f64),
"the one unrepresentable case arrives lossily, as the walk does it"
);
assert_eq!(
Value::deserialize(UnitDeserializer::<Error>::new()).unwrap(),
Value::Null
);
}
#[test]
fn serializing_narrows_the_way_the_walk_out_narrows() {
for number in [0, 1, -1, i128::from(i64::MIN), i128::from(u64::MAX)] {
let rendered =
serde_json::to_string(&Value::Integer(number)).expect("a number serializes");
assert_eq!(rendered, number.to_string());
}
let tree = Value::Table(BTreeMap::from([
("null".to_owned(), Value::Null),
("ratio".to_owned(), Value::Float(0.5)),
(
"tags".to_owned(),
Value::Array(vec![Value::String("a".to_owned())]),
),
]));
assert_eq!(
serde_json::to_value(&tree)
.expect("a tree serializes")
.to_string(),
r#"{"null":null,"ratio":0.5,"tags":["a"]}"#
);
assert_eq!(
serde_json::from_str::<Value>(&serde_json::to_string(&tree).unwrap()).unwrap(),
tree
);
}
#[test]
fn a_typed_read_reports_the_path_and_the_kind_that_was_there() {
let tree = Value::Table(BTreeMap::from([(
"pool".to_owned(),
Value::Table(BTreeMap::from([(
"max".to_owned(),
Value::String("not-a-number".to_owned()),
)])),
)]));
assert_eq!(
tree.get_as::<u16>("pool.max").unwrap_err().kind(),
crate::ErrorKind::Type
);
assert_eq!(
tree.get_as::<u16>("pool.max").unwrap_err().path(),
"pool.max"
);
assert_eq!(
tree.get_as::<u16>("pool.min").unwrap_err().kind(),
crate::ErrorKind::Missing
);
assert_eq!(tree.get_as::<String>("pool.max").unwrap(), "not-a-number");
}
#[test]
fn leaf_paths_stops_at_arrays_and_keeps_empty_tables() {
let tree = Value::Table(BTreeMap::from([
("host".to_owned(), Value::String("db".to_owned())),
("empty".to_owned(), Value::Table(BTreeMap::new())),
(
"tags".to_owned(),
Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
),
(
"pool".to_owned(),
Value::Table(BTreeMap::from([("max".to_owned(), Value::Integer(8))])),
),
]));
assert_eq!(tree.leaf_paths(), ["empty", "host", "pool.max", "tags"]);
}
#[test]
fn a_step_through_a_leaf_is_none_and_the_empty_path_is_identity() {
let value = Value::Table(BTreeMap::from([("port".to_owned(), Value::Integer(1))]));
assert_eq!(value.get("port.deeper"), None);
assert_eq!(value.get("missing"), None);
assert_eq!(value.get(""), Some(&value));
}
}