use std::collections::{BTreeMap, HashSet};
use serde::de::value::{Error as DeError, MapDeserializer, SeqDeserializer};
use serde::de::{self, Error as _, IntoDeserializer, Visitor};
#[derive(Debug)]
pub(crate) enum StrNode {
Str(String),
#[cfg_attr(not(feature = "json"), allow(dead_code))]
Json(String),
List(Vec<String>),
Map(BTreeMap<String, StrNode>),
}
impl StrNode {
pub(crate) fn from_entries(
mut entries: Vec<(Vec<String>, String)>,
list_keys: &HashSet<String>,
json_values: bool,
) -> Self {
entries.sort_by(|a, b| a.0.cmp(&b.0));
let mut root = BTreeMap::new();
for (segments, value) in entries {
let is_list = list_keys.contains(&segments.join("."));
let node = Self::leaf(value, is_list, json_values);
insert_nested(&mut root, &segments, node);
}
StrNode::Map(root)
}
pub(crate) fn feed(
self,
seed: &mut dyn for<'de> FnMut(
&mut dyn erased_serde::Deserializer<'de>,
) -> Result<(), erased_serde::Error>,
) -> Result<(), erased_serde::Error> {
let mut erased = <dyn erased_serde::Deserializer>::erase(self);
seed(&mut erased)
}
fn leaf(value: String, is_list: bool, json_values: bool) -> Self {
if !is_list {
return if json_values {
StrNode::Json(value)
} else {
StrNode::Str(value)
};
}
if value.is_empty() {
return StrNode::List(Vec::new());
}
StrNode::List(value.split(',').map(|s| s.trim().to_string()).collect())
}
}
fn insert_nested(root: &mut BTreeMap<String, StrNode>, path: &[String], value: StrNode) {
let Some((head, tail)) = path.split_first() else {
return;
};
if tail.is_empty() {
root.insert(head.clone(), value);
return;
}
let entry = root
.entry(head.clone())
.or_insert_with(|| StrNode::Map(BTreeMap::new()));
if !matches!(entry, StrNode::Map(_)) {
*entry = StrNode::Map(BTreeMap::new());
}
if let StrNode::Map(child) = entry {
insert_nested(child, tail, value);
}
}
impl<'de> IntoDeserializer<'de, DeError> for StrNode {
type Deserializer = Self;
fn into_deserializer(self) -> Self {
self
}
}
macro_rules! deserialize_structured {
($($method:ident($($arg:ident: $ty:ty),*)),* $(,)?) => {
$(
#[cfg_attr(not(feature = "json"), allow(unused_variables))]
fn $method<V: Visitor<'de>>(self, $($arg: $ty,)* visitor: V) -> Result<V::Value, DeError> {
#[cfg(feature = "json")]
if let StrNode::Json(s) = self {
return de::Deserializer::$method(parse_json(&s)?, $($arg,)* visitor)
.map_err(DeError::custom);
}
self.deserialize_any(visitor)
}
)*
};
}
#[cfg(feature = "json")]
fn parse_json(text: &str) -> Result<serde_json::Value, DeError> {
serde_json::from_str(text)
.map_err(|e| DeError::custom(format!("invalid JSON value {text:?}: {e}")))
}
macro_rules! deserialize_parsed {
($($method:ident => $visit:ident: $ty:ty),* $(,)?) => {
$(
fn $method<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
match self {
StrNode::Str(s) | StrNode::Json(s) => match s.trim().parse::<$ty>() {
Ok(v) => visitor.$visit(v),
Err(e) => Err(DeError::custom(format!(
"invalid value {s:?}, expected {}: {e}",
stringify!($ty)
))),
},
other => other.deserialize_any(visitor),
}
}
)*
};
}
impl<'de> de::Deserializer<'de> for StrNode {
type Error = DeError;
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
match self {
StrNode::Str(s) | StrNode::Json(s) => visitor.visit_string(s),
StrNode::List(items) => {
let mut seq = SeqDeserializer::new(items.into_iter().map(StrNode::Str));
let value = visitor.visit_seq(&mut seq)?;
seq.end()?;
Ok(value)
}
StrNode::Map(map) => {
let mut map = MapDeserializer::new(map.into_iter());
let value = visitor.visit_map(&mut map)?;
map.end()?;
Ok(value)
}
}
}
deserialize_parsed! {
deserialize_bool => visit_bool: bool,
deserialize_i8 => visit_i8: i8,
deserialize_i16 => visit_i16: i16,
deserialize_i32 => visit_i32: i32,
deserialize_i64 => visit_i64: i64,
deserialize_i128 => visit_i128: i128,
deserialize_u8 => visit_u8: u8,
deserialize_u16 => visit_u16: u16,
deserialize_u32 => visit_u32: u32,
deserialize_u64 => visit_u64: u64,
deserialize_u128 => visit_u128: u128,
deserialize_f32 => visit_f32: f32,
deserialize_f64 => visit_f64: f64,
}
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, DeError> {
visitor.visit_some(self)
}
fn deserialize_newtype_struct<V: Visitor<'de>>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, DeError> {
visitor.visit_newtype_struct(self)
}
#[cfg_attr(not(feature = "json"), allow(unused_variables))]
fn deserialize_enum<V: Visitor<'de>>(
self,
name: &'static str,
variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, DeError> {
match self {
#[cfg(feature = "json")]
StrNode::Json(s) if s.trim_start().starts_with('{') => {
de::Deserializer::deserialize_enum(parse_json(&s)?, name, variants, visitor)
.map_err(DeError::custom)
}
StrNode::Str(s) | StrNode::Json(s) => visitor.visit_enum(s.into_deserializer()),
other => other.deserialize_any(visitor),
}
}
deserialize_structured! {
deserialize_seq(),
deserialize_tuple(len: usize),
deserialize_tuple_struct(name: &'static str, len: usize),
deserialize_map(),
deserialize_struct(name: &'static str, fields: &'static [&'static str]),
}
serde::forward_to_deserialize_any! {
char str string bytes byte_buf unit unit_struct identifier ignored_any
}
}
#[cfg(test)]
mod tests {
use serde::Deserialize;
use super::*;
#[test]
fn string_leaf_is_returned_verbatim() {
for raw in ["123456", "true", "null", r#""abc""#, "", " padded "] {
let s = String::deserialize(StrNode::Str(raw.to_string())).unwrap();
assert_eq!(s, raw);
}
}
#[test]
fn string_leaf_is_parsed_for_scalars() {
assert_eq!(u64::deserialize(StrNode::Str("42".into())).unwrap(), 42);
assert_eq!(i8::deserialize(StrNode::Str("-7".into())).unwrap(), -7);
assert!(bool::deserialize(StrNode::Str("true".into())).unwrap());
assert!(u8::deserialize(StrNode::Str("256".into())).is_err());
assert!(bool::deserialize(StrNode::Str("yes".into())).is_err());
}
#[test]
fn unit_enum_variant_from_string_leaf() {
#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
enum Level {
Info,
Debug,
}
let level = Level::deserialize(StrNode::Str("debug".into())).unwrap();
assert_eq!(level, Level::Debug);
assert!(Level::deserialize(StrNode::Str("loud".into())).is_err());
}
}