use std::fmt;
use serde::de::{self, Deserializer, IntoDeserializer, MapAccess, SeqAccess, Visitor};
use crate::value::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Error {
kind: crate::error::ErrorKind,
message: String,
path: Vec<String>,
}
impl Error {
fn prefixed(mut self, key: &str) -> Self {
self.path.insert(0, key.to_owned());
self
}
pub(crate) fn path(&self) -> String {
self.path.join(".")
}
pub(crate) fn into_error(self) -> crate::Error {
let error = crate::Error::new(self.kind, self.message.clone());
if self.path.is_empty() {
error
} else {
error.prepend_key(self.path())
}
}
fn of(kind: crate::error::ErrorKind, message: String) -> Self {
Error {
kind,
message,
path: Vec::new(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.path.is_empty() {
formatter.write_str(&self.message)
} else {
write!(formatter, "{}: {}", self.path(), self.message)
}
}
}
impl std::error::Error for Error {}
impl de::Error for Error {
fn custom<T: fmt::Display>(message: T) -> Self {
Error::of(crate::error::ErrorKind::Type, message.to_string())
}
fn invalid_type(unexpected: de::Unexpected<'_>, expected: &dyn de::Expected) -> Self {
Error::of(
crate::error::ErrorKind::Type,
format!("invalid type: {}, expected {expected}", kind_of(unexpected)),
)
}
fn invalid_value(unexpected: de::Unexpected<'_>, expected: &dyn de::Expected) -> Self {
Error::of(
crate::error::ErrorKind::Type,
format!(
"invalid value: {}, expected {expected}",
kind_of(unexpected)
),
)
}
fn invalid_length(length: usize, expected: &dyn de::Expected) -> Self {
Error::of(
crate::error::ErrorKind::Type,
format!("invalid length {length}, expected {expected}"),
)
}
fn missing_field(field: &'static str) -> Self {
Error::of(
crate::error::ErrorKind::Missing,
format!("missing field `{field}`"),
)
.prefixed(field)
}
fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
Error::of(
crate::error::ErrorKind::Type,
format!("unknown field `{field}`, expected one of {expected:?}"),
)
}
fn duplicate_field(field: &'static str) -> Self {
Error::of(
crate::error::ErrorKind::Type,
format!("duplicate field `{field}`"),
)
}
fn unknown_variant(variant: &str, expected: &'static [&'static str]) -> Self {
Error::of(
crate::error::ErrorKind::Type,
format!("unknown variant `{variant}`, expected one of {expected:?}"),
)
}
}
fn kind_of(unexpected: de::Unexpected<'_>) -> &'static str {
use de::Unexpected;
match unexpected {
Unexpected::Bool(_) => "a boolean",
Unexpected::Unsigned(_) => "an unsigned integer",
Unexpected::Signed(_) => "a signed integer",
Unexpected::Float(_) => "a float",
Unexpected::Char(_) => "a character",
Unexpected::Str(_) => "a string",
Unexpected::Bytes(_) => "a byte string",
Unexpected::Unit => "a unit",
Unexpected::Option => "an option",
Unexpected::NewtypeStruct => "a newtype struct",
Unexpected::Seq => "a list",
Unexpected::Map => "a table",
Unexpected::Enum => "an enum",
Unexpected::UnitVariant => "a unit variant",
Unexpected::NewtypeVariant => "a newtype variant",
Unexpected::TupleVariant => "a tuple variant",
Unexpected::StructVariant => "a struct variant",
Unexpected::Other(_) => "something else",
}
}
type Result<T> = std::result::Result<T, Error>;
pub(crate) struct Reader<'a>(pub(crate) &'a Value);
impl<'de> Deserializer<'de> for Reader<'de> {
type Error = Error;
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
match self.0 {
Value::Null => visitor.visit_unit(),
Value::Bool(boolean) => visitor.visit_bool(*boolean),
Value::Integer(number) => integer(*number, visitor),
Value::Float(number) => visitor.visit_f64(*number),
Value::String(text) => visitor.visit_str(text),
Value::Array(values) => visitor.visit_seq(Sequence {
values: values.iter().enumerate(),
remaining: values.len(),
}),
Value::Table(table) => visitor.visit_map(Table {
entries: table.iter(),
value: None,
}),
}
}
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
match self.0 {
Value::Null => visitor.visit_none(),
_ => visitor.visit_some(self),
}
}
fn deserialize_enum<V: Visitor<'de>>(
self,
_name: &'static str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value> {
use serde::de::value::MapAccessDeserializer;
match self.0 {
Value::String(text) => visitor.visit_enum(text.as_str().into_deserializer()),
Value::Table(table) => visitor.visit_enum(MapAccessDeserializer::new(Table {
entries: table.iter(),
value: None,
})),
Value::Integer(number) => match u32::try_from(*number) {
Ok(index) => visitor.visit_enum(index.into_deserializer()),
Err(_) => self.deserialize_any(visitor),
},
_ => self.deserialize_any(visitor),
}
}
fn deserialize_newtype_struct<V: Visitor<'de>>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value> {
visitor.visit_newtype_struct(self)
}
fn is_human_readable(&self) -> bool {
true
}
serde::forward_to_deserialize_any! {
bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq bytes
byte_buf map unit struct ignored_any unit_struct tuple_struct tuple
identifier
}
}
fn integer<'de, V: Visitor<'de>>(number: i128, visitor: V) -> Result<V::Value> {
if let Ok(unsigned) = u64::try_from(number) {
return visitor.visit_u64(unsigned);
}
if let Ok(signed) = i64::try_from(number) {
return visitor.visit_i64(signed);
}
visitor.visit_i128(number)
}
struct Table<'a> {
entries: std::collections::btree_map::Iter<'a, String, Value>,
value: Option<(&'a str, &'a Value)>,
}
impl<'de> MapAccess<'de> for Table<'de> {
type Error = Error;
fn next_key_seed<K: de::DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>> {
let Some((key, value)) = self.entries.next() else {
return Ok(None);
};
self.value = Some((key, value));
seed.deserialize(key.as_str().into_deserializer())
.map(Some)
.map_err(|error: Error| error.prefixed(key))
}
fn next_value_seed<V: de::DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
let (key, value) = self
.value
.take()
.expect("a value is only asked for after its key");
seed.deserialize(Reader(value))
.map_err(|error: Error| error.prefixed(key))
}
fn size_hint(&self) -> Option<usize> {
Some(self.entries.len())
}
}
struct Sequence<'a> {
values: std::iter::Enumerate<std::slice::Iter<'a, Value>>,
remaining: usize,
}
impl<'de> SeqAccess<'de> for Sequence<'de> {
type Error = Error;
fn next_element_seed<T: de::DeserializeSeed<'de>>(
&mut self,
seed: T,
) -> Result<Option<T::Value>> {
let Some((index, value)) = self.values.next() else {
return Ok(None);
};
self.remaining -= 1;
seed.deserialize(Reader(value))
.map(Some)
.map_err(|error: Error| error.prefixed(&index.to_string()))
}
fn size_hint(&self) -> Option<usize> {
Some(self.remaining)
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::net::IpAddr;
use std::path::PathBuf;
use proptest::prelude::*;
use serde::Deserialize;
use super::Reader;
use crate::value::Value;
fn ours<T: serde::de::DeserializeOwned>(value: &Value) -> Result<T, String> {
T::deserialize(Reader(value)).map_err(|error| error.path())
}
fn original<T: serde::de::DeserializeOwned>(value: &Value) -> Result<T, String> {
T::deserialize(&as_a_provider_would(value)).map_err(|error: figment::Error| {
crate::backend::figment::error::error_path(&error).join(".")
})
}
fn as_a_provider_would(value: &Value) -> figment::value::Value {
use figment::value::{Empty, Tag};
match value {
Value::Null => figment::value::Value::Empty(Tag::Default, Empty::Unit),
Value::Array(values) => figment::value::Value::Array(
Tag::Default,
values.iter().map(as_a_provider_would).collect(),
),
Value::Table(table) => figment::value::Value::Dict(
Tag::Default,
table
.iter()
.map(|(key, value)| (key.clone(), as_a_provider_would(value)))
.collect(),
),
other => crate::backend::figment::to_figment(other),
}
}
fn agrees(value: &Value) -> Result<(), String> {
macro_rules! compare {
($($type:ty),* $(,)?) => {$(
if ours::<$type>(value) != original::<$type>(value) {
return Err(format!(
"{} read {:?} as {:?}, the original as {:?}",
stringify!($type),
value,
ours::<$type>(value),
original::<$type>(value),
));
}
)*};
}
#[derive(Debug, PartialEq, Deserialize)]
struct Nested {
host: String,
port: u16,
#[serde(default)]
tags: Vec<String>,
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Mode {
Fast,
Slow,
}
compare!(
bool,
u8,
u16,
u64,
i8,
i64,
i128,
f64,
char,
String,
Option<u16>,
Option<String>,
Vec<String>,
Vec<u16>,
BTreeMap<String, u32>,
(u16, String),
IpAddr,
PathBuf,
Mode,
Nested,
serde_json::Value,
);
Ok(())
}
fn trees() -> impl Strategy<Value = Value> {
let leaf = prop_oneof![
Just(Value::Null),
any::<bool>().prop_map(Value::Bool),
any::<i64>().prop_map(|number| Value::Integer(i128::from(number))),
any::<i128>().prop_map(Value::Integer),
any::<f64>().prop_map(Value::Float),
"[a-z0-9.:_-]{0,12}".prop_map(Value::String),
prop_oneof![
Just("8080".to_owned()),
Just("true".to_owned()),
Just("127.0.0.1".to_owned()),
Just("fast".to_owned()),
Just("/etc/app".to_owned()),
]
.prop_map(Value::String),
];
leaf.prop_recursive(4, 24, 4, |inner| {
prop_oneof![
prop::collection::vec(inner.clone(), 0..4).prop_map(Value::Array),
prop::collection::btree_map("[a-z]{1,6}|host|port|tags", inner, 0..4)
.prop_map(Value::Table),
]
})
}
proptest! {
#[test]
fn every_tree_reads_the_same(value in trees()) {
if let Err(divergence) = agrees(&value) {
return Err(TestCaseError::fail(divergence));
}
}
}
#[test]
fn the_shapes_a_configuration_takes_read_the_same() {
let table = |pairs: &[(&str, Value)]| {
Value::Table(
pairs
.iter()
.map(|(key, value)| ((*key).to_owned(), value.clone()))
.collect(),
)
};
let cases = [
Value::Null,
Value::Bool(true),
Value::Integer(8080),
Value::Integer(-1),
Value::Integer(i128::from(u64::MAX)),
Value::Integer(i128::MAX),
Value::Float(1.5),
Value::String("8080".to_owned()),
Value::String("fast".to_owned()),
Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
Value::Array(vec![Value::String("a".to_owned())]),
table(&[
("host", Value::String("localhost".to_owned())),
("port", Value::Integer(5432)),
]),
table(&[
("host", Value::String("localhost".to_owned())),
("port", Value::String("not a port".to_owned())),
]),
table(&[("host", Value::String("localhost".to_owned()))]),
table(&[("fast", Value::Null)]),
];
for case in cases {
agrees(&case).expect("the reading moved away from the original");
}
}
}