use serde::de::{self, DeserializeSeed, EnumAccess, Error as DeError, MapAccess, SeqAccess, VariantAccess, Visitor};
use serde::Deserialize;
use super::string::{
parse_double_quoted_string, parse_folded, parse_literal, parse_single_quoted_string,
};
use super::{AliasedYaml, ArrayData, HashData, Yaml};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
#[derive(Debug)]
pub struct YamlDeserializeError(pub String);
impl std::fmt::Display for YamlDeserializeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for YamlDeserializeError {}
impl de::Error for YamlDeserializeError {
fn custom<T: std::fmt::Display>(msg: T) -> Self {
YamlDeserializeError(msg.to_string())
}
}
fn yaml_to_string(yaml: &Yaml) -> Option<String> {
match yaml {
Yaml::UnquotedString(s) => Some(s.clone()),
Yaml::DoubleQuotedString(parts) => Some(parse_double_quoted_string(parts)),
Yaml::LiteralString(lines, chomping) => Some(parse_literal(
lines.iter().map(|s| s.as_str()).collect(),
*chomping,
)),
Yaml::FoldedString(lines, chomping) => Some(parse_folded(
lines.iter().map(|s| s.as_str()).collect(),
*chomping,
)),
Yaml::SingleQuotedString(parts) => Some(parse_single_quoted_string(parts)),
Yaml::InlineHash(_)
| Yaml::Hash(_)
| Yaml::InlineArray(_)
| Yaml::Array(_)
| Yaml::Anchor(_) => None,
}
}
type Anchors = Rc<RefCell<HashMap<String, Yaml>>>;
fn register(anchors: &Anchors, aliased: &AliasedYaml) {
if let Some(name) = &aliased.alias {
anchors.borrow_mut().insert(name.clone(), aliased.value.clone());
}
}
fn resolve(anchors: &Anchors, name: &str) -> Result<Yaml, YamlDeserializeError> {
anchors
.borrow()
.get(name)
.cloned()
.ok_or_else(|| YamlDeserializeError(format!("undefined anchor: {name}")))
}
fn hash_entries(anchors: &Anchors, data: Vec<HashData>) -> Vec<(String, Yaml)> {
data.into_iter()
.filter_map(|d| match d {
HashData::Element(e) => {
register(anchors, &e.value);
Some((e.key, e.value.value))
}
HashData::InlineComment(_) | HashData::Comment(_) => None,
})
.collect()
}
fn seq_values(anchors: &Anchors, elements: Vec<ArrayData>) -> Vec<Yaml> {
elements
.into_iter()
.filter_map(|d| match d {
ArrayData::Element(e) => {
register(anchors, &e);
Some(e.value)
}
ArrayData::InlineComment(_) | ArrayData::Comment(_) => None,
})
.collect()
}
fn into_resolved(yaml: Yaml, anchors: Anchors) -> Result<(Yaml, Anchors), YamlDeserializeError> {
match yaml {
Yaml::Anchor(ref name) => {
let resolved = resolve(&anchors, name)?;
Ok((resolved, anchors))
}
other => Ok((other, anchors)),
}
}
pub struct YamlDeserializer {
yaml: Yaml,
anchors: Anchors,
}
impl YamlDeserializer {
pub fn new(yaml: Yaml) -> Self {
Self { yaml, anchors: Rc::new(RefCell::new(HashMap::new())) }
}
fn with(yaml: Yaml, anchors: Anchors) -> Self {
Self { yaml, anchors }
}
}
pub fn from_yaml<'de, T: Deserialize<'de>>(yaml: Yaml) -> Result<T, YamlDeserializeError> {
T::deserialize(YamlDeserializer::new(yaml))
}
pub fn from_yaml_str<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, YamlDeserializeError> {
use super::{parse_yaml_file, DocumentData};
let doc = parse_yaml_file(s).map_err(|e| YamlDeserializeError(format!("{e}")))?;
let yaml = doc
.items
.into_iter()
.find_map(|item| match item {
DocumentData::Yaml(y) => Some(y),
_ => None,
})
.ok_or_else(|| YamlDeserializeError("no YAML content in document".to_string()))?;
from_yaml(yaml)
}
impl<'de> de::Deserializer<'de> for YamlDeserializer {
type Error = YamlDeserializeError;
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
let anchors = self.anchors;
match self.yaml {
Yaml::UnquotedString(s) => {
if matches!(s.as_str(), "~" | "null" | "Null" | "NULL") {
return visitor.visit_unit();
}
if s == "true" {
return visitor.visit_bool(true);
}
if s == "false" {
return visitor.visit_bool(false);
}
if let Ok(i) = s.parse::<i64>() {
return visitor.visit_i64(i);
}
if let Ok(u) = s.parse::<u64>() {
return visitor.visit_u64(u);
}
if let Ok(f) = s.parse::<f64>() {
return visitor.visit_f64(f);
}
visitor.visit_string(s)
}
Yaml::DoubleQuotedString(ref parts) => {
visitor.visit_string(parse_double_quoted_string(parts))
}
Yaml::LiteralString(ref lines, chomping) => visitor.visit_string(parse_literal(
lines.iter().map(|s| s.as_str()).collect(),
chomping,
)),
Yaml::FoldedString(ref lines, chomping) => visitor.visit_string(parse_folded(
lines.iter().map(|s| s.as_str()).collect(),
chomping,
)),
Yaml::SingleQuotedString(ref parts) => {
visitor.visit_string(parse_single_quoted_string(parts))
}
Yaml::Array(elements) => {
let values = seq_values(&anchors, elements);
visitor.visit_seq(VecSeqAccess::new(values, anchors))
}
Yaml::InlineArray(elements) => visitor.visit_seq(VecSeqAccess::new(elements, anchors)),
Yaml::Hash(data) => {
let entries = hash_entries(&anchors, data);
visitor.visit_map(VecMapAccess::new(entries, anchors))
}
Yaml::InlineHash(data) => visitor.visit_map(VecMapAccess::new(data, anchors)),
Yaml::Anchor(name) => {
let resolved = resolve(&anchors, &name)?;
YamlDeserializer::with(resolved, anchors).deserialize_any(visitor)
}
}
}
fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
let (yaml, _) = into_resolved(self.yaml, self.anchors)?;
match yaml {
Yaml::UnquotedString(ref s) => match s.as_str() {
"true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" => {
visitor.visit_bool(true)
}
"false" | "False" | "FALSE" | "no" | "No" | "NO" | "off" | "Off" | "OFF" => {
visitor.visit_bool(false)
}
_ => Err(Self::Error::custom(format!("expected bool, got {:?}", s))),
},
_ => Err(Self::Error::custom("expected bool")),
}
}
fn deserialize_i8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_i64(visitor)
}
fn deserialize_i16<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_i64(visitor)
}
fn deserialize_i32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_i64(visitor)
}
fn deserialize_i64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
let (yaml, _) = into_resolved(self.yaml, self.anchors)?;
if let Some(s) = yaml_to_string(&yaml) {
s.parse::<i64>()
.map_err(|e| Self::Error::custom(e))
.and_then(|v| visitor.visit_i64(v))
} else {
Err(Self::Error::custom("expected integer"))
}
}
fn deserialize_u8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_u64(visitor)
}
fn deserialize_u16<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_u64(visitor)
}
fn deserialize_u32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_u64(visitor)
}
fn deserialize_u64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
let (yaml, _) = into_resolved(self.yaml, self.anchors)?;
if let Some(s) = yaml_to_string(&yaml) {
s.parse::<u64>()
.map_err(|e| Self::Error::custom(e))
.and_then(|v| visitor.visit_u64(v))
} else {
Err(Self::Error::custom("expected unsigned integer"))
}
}
fn deserialize_f32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_f64(visitor)
}
fn deserialize_f64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
let (yaml, _) = into_resolved(self.yaml, self.anchors)?;
if let Some(s) = yaml_to_string(&yaml) {
s.parse::<f64>()
.map_err(|e| Self::Error::custom(e))
.and_then(|v| visitor.visit_f64(v))
} else {
Err(Self::Error::custom("expected float"))
}
}
fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_str(visitor)
}
fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
let (yaml, _) = into_resolved(self.yaml, self.anchors)?;
if let Some(s) = yaml_to_string(&yaml) {
visitor.visit_string(s)
} else {
Err(Self::Error::custom("expected string"))
}
}
fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_str(visitor)
}
fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_seq(visitor)
}
fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_seq(visitor)
}
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
let (yaml, anchors) = into_resolved(self.yaml, self.anchors)?;
match &yaml {
Yaml::UnquotedString(s) if matches!(s.as_str(), "~" | "null" | "Null" | "NULL") => {
visitor.visit_none()
}
_ => visitor.visit_some(YamlDeserializer::with(yaml, anchors)),
}
}
fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
visitor.visit_unit()
}
fn deserialize_unit_struct<V: Visitor<'de>>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Self::Error> {
visitor.visit_unit()
}
fn deserialize_newtype_struct<V: Visitor<'de>>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Self::Error> {
visitor.visit_newtype_struct(self)
}
fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
let (yaml, anchors) = into_resolved(self.yaml, self.anchors)?;
match yaml {
Yaml::Array(elements) => {
let values = seq_values(&anchors, elements);
visitor.visit_seq(VecSeqAccess::new(values, anchors))
}
Yaml::InlineArray(elements) => visitor.visit_seq(VecSeqAccess::new(elements, anchors)),
_ => Err(Self::Error::custom("expected sequence")),
}
}
fn deserialize_tuple<V: Visitor<'de>>(
self,
_len: usize,
visitor: V,
) -> Result<V::Value, Self::Error> {
self.deserialize_seq(visitor)
}
fn deserialize_tuple_struct<V: Visitor<'de>>(
self,
_name: &'static str,
len: usize,
visitor: V,
) -> Result<V::Value, Self::Error> {
self.deserialize_tuple(len, visitor)
}
fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
let (yaml, anchors) = into_resolved(self.yaml, self.anchors)?;
match yaml {
Yaml::Hash(data) => {
let entries = hash_entries(&anchors, data);
visitor.visit_map(VecMapAccess::new(entries, anchors))
}
Yaml::InlineHash(data) => visitor.visit_map(VecMapAccess::new(data, anchors)),
_ => Err(Self::Error::custom("expected map")),
}
}
fn deserialize_struct<V: Visitor<'de>>(
self,
_name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> {
self.deserialize_map(visitor)
}
fn deserialize_enum<V: Visitor<'de>>(
self,
_name: &'static str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> {
let (yaml, anchors) = into_resolved(self.yaml, self.anchors)?;
match yaml {
ref y @ (Yaml::UnquotedString(_)
| Yaml::DoubleQuotedString(_)
| Yaml::SingleQuotedString(_)
| Yaml::LiteralString(_, _)
| Yaml::FoldedString(_, _)) => {
let s = yaml_to_string(y)
.ok_or_else(|| Self::Error::custom("expected string for enum variant"))?;
visitor.visit_enum(StrEnumAccess {
variant: s,
value: Yaml::UnquotedString("~".to_string()),
anchors,
})
}
Yaml::Hash(data) => {
let mut entries = hash_entries(&anchors, data);
if entries.len() == 1 {
let (variant, value) = entries.remove(0);
visitor.visit_enum(StrEnumAccess { variant, value, anchors })
} else {
Err(Self::Error::custom(
"expected single-key map for tagged enum variant",
))
}
}
Yaml::InlineHash(mut data) => {
if data.len() == 1 {
let (variant, value) = data.remove(0);
visitor.visit_enum(StrEnumAccess { variant, value, anchors })
} else {
Err(Self::Error::custom(
"expected single-key inline hash for tagged enum variant",
))
}
}
Yaml::InlineArray(_) | Yaml::Array(_) | Yaml::Anchor(_) => {
Err(Self::Error::custom("expected string or map for enum"))
}
}
}
fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
self.deserialize_str(visitor)
}
fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
visitor.visit_unit()
}
}
struct VecSeqAccess {
iter: std::vec::IntoIter<Yaml>,
anchors: Anchors,
}
impl VecSeqAccess {
fn new(v: Vec<Yaml>, anchors: Anchors) -> Self {
Self { iter: v.into_iter(), anchors }
}
}
impl<'de> SeqAccess<'de> for VecSeqAccess {
type Error = YamlDeserializeError;
fn next_element_seed<T: DeserializeSeed<'de>>(
&mut self,
seed: T,
) -> Result<Option<T::Value>, Self::Error> {
match self.iter.next() {
Some(yaml) => seed.deserialize(YamlDeserializer::with(yaml, Rc::clone(&self.anchors))).map(Some),
None => Ok(None),
}
}
}
struct VecMapAccess {
iter: std::vec::IntoIter<(String, Yaml)>,
pending_value: Option<Yaml>,
anchors: Anchors,
}
impl VecMapAccess {
fn new(v: Vec<(String, Yaml)>, anchors: Anchors) -> Self {
Self {
iter: v.into_iter(),
pending_value: None,
anchors,
}
}
}
impl<'de> MapAccess<'de> for VecMapAccess {
type Error = YamlDeserializeError;
fn next_key_seed<K: DeserializeSeed<'de>>(
&mut self,
seed: K,
) -> Result<Option<K::Value>, Self::Error> {
match self.iter.next() {
Some((k, v)) => {
self.pending_value = Some(v);
seed.deserialize(YamlDeserializer::with(Yaml::UnquotedString(k), Rc::clone(&self.anchors)))
.map(Some)
}
None => Ok(None),
}
}
fn next_value_seed<V: DeserializeSeed<'de>>(
&mut self,
seed: V,
) -> Result<V::Value, Self::Error> {
let v = self
.pending_value
.take()
.expect("next_value_seed called without next_key_seed");
seed.deserialize(YamlDeserializer::with(v, Rc::clone(&self.anchors)))
}
}
struct StrEnumAccess {
variant: String,
value: Yaml,
anchors: Anchors,
}
impl<'de> EnumAccess<'de> for StrEnumAccess {
type Error = YamlDeserializeError;
type Variant = YamlDeserializer;
fn variant_seed<V: DeserializeSeed<'de>>(
self,
seed: V,
) -> Result<(V::Value, YamlDeserializer), Self::Error> {
let variant = seed.deserialize(YamlDeserializer::with(
Yaml::UnquotedString(self.variant),
Rc::clone(&self.anchors),
))?;
Ok((variant, YamlDeserializer::with(self.value, self.anchors)))
}
}
impl<'de> VariantAccess<'de> for YamlDeserializer {
type Error = YamlDeserializeError;
fn unit_variant(self) -> Result<(), Self::Error> {
Ok(())
}
fn newtype_variant_seed<T: DeserializeSeed<'de>>(
self,
seed: T,
) -> Result<T::Value, Self::Error> {
seed.deserialize(self)
}
fn tuple_variant<V: Visitor<'de>>(
self,
len: usize,
visitor: V,
) -> Result<V::Value, Self::Error> {
de::Deserializer::deserialize_tuple(self, len, visitor)
}
fn struct_variant<V: Visitor<'de>>(
self,
fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> {
de::Deserializer::deserialize_struct(self, "", fields, visitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::yaml::serializer::YamlSerializer;
use crate::yaml::{DoubleQuotedStringPart, Pretty};
use serde::{Deserialize, Serialize};
fn round_trip<T>(value: &T) -> T
where
T: Serialize + for<'de> Deserialize<'de> + std::fmt::Debug + PartialEq,
{
let yaml = value.serialize(YamlSerializer).unwrap().pretty();
from_yaml(yaml).unwrap()
}
fn round_trip_check<T>(value: T)
where
T: Serialize + for<'de> Deserialize<'de> + std::fmt::Debug + PartialEq + Clone,
{
assert_eq!(value.clone(), round_trip(&value));
}
#[test]
fn bool_true() {
round_trip_check(true);
}
#[test]
fn bool_false() {
round_trip_check(false);
}
#[test]
fn integer_positive() {
round_trip_check(42i64);
}
#[test]
fn integer_negative() {
round_trip_check(-7i64);
}
#[test]
fn integer_zero() {
round_trip_check(0i64);
}
#[test]
fn unsigned() {
round_trip_check(u64::MAX);
}
#[test]
fn float() {
round_trip_check(3.14f64);
}
#[test]
fn string_simple() {
round_trip_check("hello".to_string());
}
#[test]
fn string_with_double_quote() {
round_trip_check(r#"say "hello""#.to_string());
}
#[test]
fn string_with_backslash() {
round_trip_check("path\\to\\file".to_string());
}
#[test]
fn string_multiline() {
round_trip_check("line one\nline two\nline three\n".to_string());
}
#[test]
fn string_long() {
let s = format!("{}\n", "word ".repeat(38).trim_end()); round_trip_check(s);
}
#[test]
fn option_some() {
round_trip_check(Some(42i64));
}
#[test]
fn option_none() {
round_trip_check(Option::<i64>::None);
}
#[test]
fn unit() {
round_trip_check(());
}
#[test]
fn vec_of_ints() {
round_trip_check(vec![1i64, 2, 3, 4, 5]);
}
#[test]
fn vec_of_strings() {
round_trip_check(vec!["alpha".to_string(), "beta".to_string()]);
}
#[test]
fn empty_vec() {
round_trip_check(Vec::<i64>::new());
}
#[test]
fn vec_stays_block_as_top_level_value() {
let yaml = vec![1i64, 2, 3]
.serialize(YamlSerializer)
.unwrap()
.pretty();
assert_eq!(
yaml,
Yaml::Array(vec![
ArrayData::Element(AliasedYaml { alias: None, value: Yaml::UnquotedString("1".to_string()) }),
ArrayData::Element(AliasedYaml { alias: None, value: Yaml::UnquotedString("2".to_string()) }),
ArrayData::Element(AliasedYaml { alias: None, value: Yaml::UnquotedString("3".to_string()) }),
]),
"top-level vec should stay block"
);
}
#[test]
fn vec_stays_block_when_long() {
let yaml = vec!["a".repeat(30), "b".repeat(30), "c".repeat(30)]
.serialize(YamlSerializer)
.unwrap()
.pretty();
assert!(
matches!(yaml, Yaml::Array(_)),
"long string vec should stay block, got {:?}",
yaml
);
}
#[test]
fn tuple_is_always_inline() {
let yaml = ("hello".to_string(), 42i64)
.serialize(YamlSerializer)
.unwrap();
assert_eq!(
yaml,
Yaml::InlineArray(vec![
Yaml::UnquotedString("hello".to_string()),
Yaml::UnquotedString("42".to_string()),
])
);
}
#[test]
fn tuple_round_trip() {
round_trip_check(("hello".to_string(), 42i64, true));
}
#[test]
fn tuple_with_vec_arg() {
let value = ("is_even".to_string(), vec![6i64], true);
let yaml = value.serialize(YamlSerializer).unwrap().pretty();
assert_eq!(
yaml,
Yaml::InlineArray(vec![
Yaml::UnquotedString("is_even".to_string()),
Yaml::InlineArray(vec![Yaml::UnquotedString("6".to_string())]),
Yaml::UnquotedString("true".to_string()),
]),
"inner vec should auto-inline and outer is always inline"
);
round_trip_check(value);
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
struct Point {
x: i64,
y: i64,
}
#[test]
fn struct_round_trip() {
round_trip_check(Point { x: 3, y: -1 });
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
struct Nested {
name: String,
point: Point,
tags: Vec<String>,
}
#[test]
fn nested_struct_round_trip() {
round_trip_check(Nested {
name: "origin".to_string(),
point: Point { x: 0, y: 0 },
tags: vec!["a".to_string(), "b".to_string()],
});
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
struct WithOption {
value: Option<i64>,
label: Option<String>,
}
#[test]
fn struct_with_option_some() {
round_trip_check(WithOption {
value: Some(7),
label: Some("hello".to_string()),
});
}
#[test]
fn struct_with_option_none() {
round_trip_check(WithOption {
value: None,
label: None,
});
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
enum Color {
Red,
Green,
Blue,
}
#[test]
fn unit_enum_round_trip() {
round_trip_check(Color::Red);
round_trip_check(Color::Green);
round_trip_check(Color::Blue);
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(untagged)]
enum UntaggedVal {
Int(i64),
Text(String),
List(Vec<i64>),
}
#[test]
fn untagged_int() {
round_trip_check(UntaggedVal::Int(42));
}
#[test]
fn untagged_list() {
round_trip_check(UntaggedVal::List(vec![1, 2, 3]));
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
enum PyBool {
True,
False,
}
#[test]
fn python_bool_true_round_trip() {
round_trip_check(PyBool::True);
}
#[test]
fn python_bool_false_round_trip() {
round_trip_check(PyBool::False);
}
#[test]
fn python_bool_from_unquoted() {
let yaml = Yaml::UnquotedString("True".to_string());
let result: PyBool = from_yaml(yaml).unwrap();
assert_eq!(result, PyBool::True);
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(untagged)]
enum FnCallData {
WithSeed(String, Vec<i64>, PyBool, Option<usize>),
Normal(String, Vec<i64>, PyBool),
}
#[test]
fn function_call_normal_round_trip() {
round_trip_check(FnCallData::Normal(
"is_even".to_string(),
vec![6],
PyBool::True,
));
}
#[test]
fn function_call_with_seed_round_trip() {
round_trip_check(FnCallData::WithSeed(
"double".to_string(),
vec![3],
PyBool::False,
Some(42),
));
}
#[test]
fn function_call_normal_is_inline() {
let value = FnCallData::Normal("is_even".to_string(), vec![6], PyBool::True);
let yaml = value.serialize(YamlSerializer).unwrap().pretty();
assert_eq!(
yaml,
Yaml::InlineArray(vec![
Yaml::UnquotedString("is_even".to_string()),
Yaml::InlineArray(vec![Yaml::UnquotedString("6".to_string())]),
Yaml::UnquotedString("True".to_string()),
]),
"FnCallData::Normal should be a 3-element InlineArray"
);
}
#[derive(Deserialize, Debug, PartialEq)]
struct ChooseOne {
options: Vec<String>,
solution: String,
}
#[test]
fn anchor_in_array_resolved_by_alias() {
let yaml_str = "---
options:
- &correct answer_a
- answer_b
solution: *correct
";
let result: ChooseOne = from_yaml_str(yaml_str).unwrap();
assert_eq!(result.solution, "answer_a");
assert_eq!(result.options, vec!["answer_a", "answer_b"]);
}
#[test]
fn undefined_anchor_returns_err() {
let yaml_str = "---
solution: *missing
options: []";
let result = from_yaml_str::<ChooseOne>(yaml_str);
assert!(result.is_err());
assert!(result.unwrap_err().0.contains("undefined anchor"));
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct FlatMeta { id: u32 }
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
enum FlatInner { Foo(FooData), Bar(BarData) }
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct FooData { x: u32 }
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct BarData { y: String }
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(untagged)]
enum FlatOuter { A(FlatInner), B(FlatBData) }
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct FlatBData { name: String }
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct FlatItem {
metadata: FlatMeta,
#[serde(flatten)]
content: FlatOuter,
}
#[test]
fn literal_block_preserves_inner_indent() {
#[derive(Deserialize, Debug, PartialEq)]
struct S { solution: String }
let yaml = "---
solution: |
for i in range(5,21):
print(i)
";
let result: S = from_yaml_str(yaml).unwrap();
assert_eq!(result.solution, "for i in range(5,21):\n print(i)\n");
}
#[test]
fn literal_block_keep_preserves_trailing_newlines() {
#[derive(Deserialize, Debug, PartialEq)]
struct S { value: String }
let yaml = "---
value: |+
hello
";
let result: S = from_yaml_str(yaml).unwrap();
assert_eq!(result.value, "hello\n\n");
}
#[test]
fn literal_block_keep_multiple_trailing_blank_lines() {
#[derive(Deserialize, Debug, PartialEq)]
struct S { value: String, next: String }
let yaml = "---
value: |+
hello
next: done
";
let result: S = from_yaml_str(yaml).unwrap();
assert_eq!(result.value, "hello\n\n\n");
assert_eq!(result.next, "done");
}
#[test]
fn key_with_trailing_space_is_trimmed() {
let yaml = "---
type : foo
x: 7
";
#[derive(Deserialize, Debug, PartialEq)]
struct S { r#type: String, x: u32 }
let result: Result<S, _> = from_yaml_str(yaml);
assert_eq!(result.unwrap(), S { r#type: "foo".to_string(), x: 7 });
}
#[test]
fn flatten_untagged_internally_tagged() {
let yaml = "---
metadata:
id: 42
type: foo
x: 7
";
let result: Result<FlatItem, _> = from_yaml_str(yaml);
assert_eq!(
result.unwrap(),
FlatItem { metadata: FlatMeta { id: 42 }, content: FlatOuter::A(FlatInner::Foo(FooData { x: 7 })) }
);
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct NestedFlatItem {
meta: u32,
#[serde(flatten)]
content: NestedContent,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
enum NestedContent {
Input(NestedInput),
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct NestedInput {
#[serde(flatten)]
kind: NestedKind,
points: i16,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum NestedKind {
Expr(NestedExpr),
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct NestedExpr {
solution: i64,
feedback: Option<String>,
}
#[test]
fn nested_flatten_with_extra_fields() {
let yaml = "---
meta: 1
type: input
kind: expr
solution: 42
points: 100
";
let result: Result<NestedFlatItem, _> = from_yaml_str(yaml);
assert_eq!(
result.unwrap(),
NestedFlatItem {
meta: 1,
content: NestedContent::Input(NestedInput {
kind: NestedKind::Expr(NestedExpr { solution: 42, feedback: None }),
points: 100,
})
}
);
}
#[test]
fn nested_flatten_with_explicit_none_and_extra_fields() {
let yaml = "---
meta: 1
type: input
kind: expr
solution: 42
feedback: ~
points: 100
";
let result: Result<NestedFlatItem, _> = from_yaml_str(yaml);
assert_eq!(
result.unwrap(),
NestedFlatItem {
meta: 1,
content: NestedContent::Input(NestedInput {
kind: NestedKind::Expr(NestedExpr { solution: 42, feedback: None }),
points: 100,
})
}
);
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct HasOutput {
expected_output: String,
points: i64,
}
#[test]
fn literal_block_with_leading_spaces_round_trips() {
let original = HasOutput {
expected_output: " September 2021\n Mo Tu We Th Fr Sa Su\n".to_string(),
points: 5,
};
let roundtripped: HasOutput = round_trip(&original);
assert_eq!(roundtripped, original, "leading spaces in literal block must survive a round-trip");
}
#[test]
fn literal_block_sibling_field_not_stolen_by_block_value() {
let yaml = "---
expected_output: |2
leading spaces here
points: 42
";
let result: Result<HasOutput, _> = from_yaml_str(yaml);
let h = result.expect("points must be parseable alongside a |2 block scalar");
assert_eq!(h.points, 42);
assert_eq!(h.expected_output, " leading spaces here\n");
}
#[derive(Deserialize, Debug, PartialEq)]
struct TupleWrapper {
tuple_items: Vec<TupleItemValue>,
}
#[derive(Deserialize, Debug, PartialEq)]
#[serde(untagged)]
enum TupleItemValue {
Nested(TupleWrapper),
Int(i64),
}
#[derive(Deserialize, Debug, PartialEq)]
struct OuterExpr {
solution: TupleWrapper,
points: i64,
}
#[test]
fn single_key_list_item_does_not_steal_outer_sibling() {
let yaml = "---
solution:
tuple_items:
- 10
- tuple_items:
- 8
- 5
points: 99
";
let result: Result<OuterExpr, _> = from_yaml_str(yaml);
let outer = result.expect("points must be parsed at the outer level, not nested inside tuple_items");
assert_eq!(outer.points, 99);
assert_eq!(outer.solution.tuple_items.len(), 2);
match &outer.solution.tuple_items[1] {
TupleItemValue::Nested(inner) => {
assert_eq!(inner.tuple_items.len(), 2);
}
TupleItemValue::Int(_) => panic!("expected nested TupleWrapper"),
}
}
#[test]
fn multi_key_list_item_still_works() {
#[derive(Deserialize, Debug, PartialEq)]
struct TwoKeys {
a: i64,
b: i64,
}
#[derive(Deserialize, Debug, PartialEq)]
struct Container {
items: Vec<TwoKeys>,
}
let yaml = "---
items:
- a: 1
b: 2
- a: 3
b: 4
";
let result: Result<Container, _> = from_yaml_str(yaml);
let c = result.expect("two-key list items must still parse correctly");
assert_eq!(c.items, vec![TwoKeys { a: 1, b: 2 }, TwoKeys { a: 3, b: 4 }]);
}
#[test]
fn visit_yaml_collects_anchors() {
use crate::yaml::{parse_yaml_file, DocumentData, VisitYaml};
let yaml_str = "---
options:
- &correct answer_a
- &wrong answer_b
solution: *correct
";
let doc = parse_yaml_file(yaml_str).unwrap();
let yaml = doc.items.into_iter().find_map(|i| match i {
DocumentData::Yaml(y) => Some(y),
_ => None,
}).unwrap();
let mut names: Vec<String> = Vec::new();
yaml.visit_yaml(&mut |a: &crate::yaml::AliasedYaml| {
if let Some(name) = &a.alias {
names.push(name.clone());
}
});
names.sort();
assert_eq!(names, vec!["correct", "wrong"]);
}
}