serde-xml-rs 0.8.2

xml-rs based deserializer for Serde (compatible with 1.0)
Documentation
use crate::{from_str, to_string};
use rstest::rstest;
use serde::{Deserialize, Serialize};

mod given_struct_with_optional_field {
    use super::*;

    #[derive(Debug, PartialEq, Serialize, Deserialize)]
    #[serde(rename = "document")]
    struct Document {
        content: Option<String>,
    }

    #[rstest]
    #[case::some(r#"<content>abc</content>"#, Some("abc".to_string()))]
    #[case::some_empty(r#"<content></content>"#, Some("".to_string()))]
    #[case::some_empty(r#"<content />"#, Some("".to_string()))]
    #[case::none(r#""#, None)]
    #[test_log::test]
    fn when_deserialize(#[case] content_text: &str, #[case] content_value: Option<String>) {
        let text =
            format!(r#"<?xml version="1.0" encoding="UTF-8"?><document>{content_text}</document>"#);
        let value = Document {
            content: content_value,
        };
        assert_eq!(from_str::<Document>(&text).unwrap(), value);
    }

    #[rstest]
    #[case::some(r#"<document><content>abc</content></document>"#, Some("abc".to_string()))]
    #[case::none(r#"<document />"#, None)]
    #[test_log::test]
    fn when_serialize(#[case] content_text: &str, #[case] content_value: Option<String>) {
        let text = format!(r#"<?xml version="1.0" encoding="UTF-8"?>{content_text}"#);
        let value = Document {
            content: content_value,
        };
        assert_eq!(to_string(&value).unwrap(), text);
    }
}

mod given_struct_with_optional_skipped_field {
    use super::*;

    #[derive(Debug, PartialEq, Serialize, Deserialize)]
    #[serde(rename = "document")]
    struct Document {
        #[serde(skip_serializing_if = "Option::is_none")]
        content: Option<String>,
    }

    #[rstest]
    #[case::some(r#"<document><content>abc</content></document>"#, Some("abc".to_string()))]
    #[case::none(r#"<document />"#, None)]
    #[test_log::test]
    fn when_serialize(#[case] content_text: &str, #[case] content_value: Option<String>) {
        let text = format!(r#"<?xml version="1.0" encoding="UTF-8"?>{content_text}"#);
        let value = Document {
            content: content_value,
        };
        assert_eq!(to_string(&value).unwrap(), text);
    }
}

mod given_struct_with_optional_attribute {
    use super::*;

    #[derive(Debug, PartialEq, Serialize, Deserialize)]
    #[serde(rename = "document")]
    struct Document {
        #[serde(rename = "@content")]
        content: Option<String>,
    }

    #[rstest]
    #[case::some(r#"<document content="abc" />"#, Some("abc".to_string()))]
    #[case::some(r#"<document content="" />"#, Some("".to_string()))]
    #[case::none(r#"<document />"#, None)]
    #[test_log::test]
    fn when_deserialize(#[case] content_text: &str, #[case] content_value: Option<String>) {
        let text = format!(r#"<?xml version="1.0" encoding="UTF-8"?>{content_text}"#);
        let value = Document {
            content: content_value,
        };
        assert_eq!(from_str::<Document>(&text).unwrap(), value);
    }

    #[rstest]
    #[case::some(r#"<document content="abc" />"#, Some("abc".to_string()))]
    #[case::none(r#"<document />"#, None)]
    #[test_log::test]
    fn when_serialize(#[case] content_text: &str, #[case] content_value: Option<String>) {
        let text = format!(r#"<?xml version="1.0" encoding="UTF-8"?>{content_text}"#);
        let value = Document {
            content: content_value,
        };
        assert_eq!(to_string(&value).unwrap(), text);
    }
}

mod given_nested_struct_with_optional_attribute {
    use super::*;

    #[derive(Debug, PartialEq, Serialize, Deserialize)]
    #[serde(rename = "document")]
    struct Document<T> {
        content: Content<T>,
    }

    #[derive(Debug, PartialEq, Serialize, Deserialize)]
    struct Content<T> {
        #[serde(rename = "@a")]
        a: Option<T>,
    }

    #[rstest]
    #[case::unit((), "")]
    #[case::string("abc".to_string(), "abc")]
    #[case::string("".to_string(), "")]
    #[case::u8(1u8, "1")]
    #[test_log::test]
    fn when_serialize_some_then_attribute<'de, T>(
        #[case] content_value: T,
        #[case] content_text: &str,
    ) where
        T: std::fmt::Debug + PartialEq + Serialize + Deserialize<'de>,
    {
        let text = format!(
            r#"<?xml version="1.0" encoding="UTF-8"?><document><content a="{}" /></document>"#,
            content_text
        );
        let value = Document {
            content: Content {
                a: Some(content_value),
            },
        };

        assert_eq!(to_string(&value).unwrap(), text);
    }

    #[rstest]
    #[case::unit(Option::<()>::None)]
    #[case::string(Option::<String>::None)]
    #[case::u8(Option::<u8>::None)]
    #[test_log::test]
    fn when_serialize_none_then_empty_no_attribute<'de, T>(#[case] content_value: Option<T>)
    where
        T: std::fmt::Debug + PartialEq + Serialize + Deserialize<'de>,
    {
        let text = r#"<?xml version="1.0" encoding="UTF-8"?><document><content /></document>"#;
        let value = Document {
            content: Content { a: content_value },
        };

        assert_eq!(to_string(&value).unwrap(), text);
    }

    #[rstest]
    #[case::unit((), "")]
    #[case::string("abc".to_string(), "abc")]
    #[case::string("".to_string(), "")]
    #[case::u8(1u8, "1")]
    #[test_log::test]
    fn when_deserialize_attribute_then_some<'de, T>(
        #[case] content_value: T,
        #[case] content_text: &str,
    ) where
        T: std::fmt::Debug + PartialEq + Serialize + Deserialize<'de>,
    {
        let text = format!(
            r#"<?xml version="1.0" encoding="UTF-8"?><document><content a="{}" /></document>"#,
            content_text
        );
        let value = Document {
            content: Content {
                a: Some(content_value),
            },
        };

        assert_eq!(from_str::<Document<T>>(&text).unwrap(), value);
    }

    #[rstest]
    #[case::unit(Option::<()>::None)]
    #[case::string(Option::<String>::None)]
    #[case::u8(Option::<u8>::None)]
    #[test_log::test]
    fn when_deserialize_absent_attribute_then_none<'de, T>(#[case] content_value: Option<T>)
    where
        T: std::fmt::Debug + PartialEq + Serialize + Deserialize<'de>,
    {
        let text = r#"<?xml version="1.0" encoding="UTF-8"?><document><content /></document>"#;
        let value = Document {
            content: Content { a: content_value },
        };

        assert_eq!(from_str::<Document<T>>(&text).unwrap(), value);
    }
}

mod given_option_unit_field {
    use super::*;

    #[derive(Debug, PartialEq, Serialize, Deserialize)]
    #[serde(rename = "document")]
    struct Document {
        content: Option<()>,
    }

    #[rstest]
    #[case::some(r#"<document><content/></document>"#, Some(()))]
    #[case::none(r#"<document />"#, None)]
    #[test_log::test]
    fn when_deserialize(#[case] content_text: &str, #[case] content_value: Option<()>) {
        let text = format!(r#"<?xml version="1.0" encoding="UTF-8"?>{content_text}"#);
        let value = Document {
            content: content_value,
        };
        assert_eq!(from_str::<Document>(&text).unwrap(), value);
    }

    #[rstest]
    #[case::some(r#"<document><content /></document>"#, Some(()))]
    #[case::none(r#"<document />"#, None)]
    #[test_log::test]
    fn when_serialize(#[case] content_text: &str, #[case] content_value: Option<()>) {
        let text = format!(r#"<?xml version="1.0" encoding="UTF-8"?>{content_text}"#);
        let value = Document {
            content: content_value,
        };
        assert_eq!(to_string(&value).unwrap(), text);
    }
}

mod given_option_unit_struct_field {
    use super::*;

    #[derive(Debug, PartialEq, Serialize, Deserialize)]
    #[serde(rename = "document")]
    struct Document {
        content: Option<Unit>,
    }

    #[derive(Debug, PartialEq, Serialize, Deserialize)]
    struct Unit;

    #[rstest]
    #[case::some(r#"<document><content/></document>"#, Some(Unit))]
    #[case::none(r#"<document />"#, None)]
    #[test_log::test]
    fn when_deserialize(#[case] content_text: &str, #[case] content_value: Option<Unit>) {
        let text = format!(r#"<?xml version="1.0" encoding="UTF-8"?>{content_text}"#);
        let value = Document {
            content: content_value,
        };
        assert_eq!(from_str::<Document>(&text).unwrap(), value);
    }

    #[rstest]
    #[case::some(r#"<document><content /></document>"#, Some(Unit))]
    #[case::none(r#"<document />"#, None)]
    #[test_log::test]
    fn when_serialize(#[case] content_text: &str, #[case] content_value: Option<Unit>) {
        let text = format!(r#"<?xml version="1.0" encoding="UTF-8"?>{content_text}"#);
        let value = Document {
            content: content_value,
        };
        assert_eq!(to_string(&value).unwrap(), text);
    }
}