xml-stinks 0.1.0

Powerful & easy manual XML deserialization using quick-xml
Documentation
use std::convert::Infallible;
use std::error::Error;
use std::fmt::Debug;

use xml_stinks::deserializer::buffered::Buffered as BufferedDeserializer;
use xml_stinks::deserializer::{Deserializer, Error as DeserializerError, IgnoreEnd};
use xml_stinks::tagged::TagStart;
use xml_stinks::DeserializeTagged;

fn main() -> Result<(), Box<dyn Error>>
{
    let mut deserializer = BufferedDeserializer::new(
        concat!("<food><name>Banana</name><kind>Fruit</kind></food>",).as_bytes(),
    );

    let food = deserializer.de_tag::<Food>("food", IgnoreEnd::No)?;

    println!("Food {} with kind {:?}", food.name, food.kind);

    Ok(())
}

#[derive(Debug)]
struct Food
{
    name: String,
    kind: FoodKind,
}

impl DeserializeTagged for Food
{
    type Error = FoodError;

    fn deserialize<TDeserializer: Deserializer>(
        _start: &TagStart,
        deserializer: &mut TDeserializer,
    ) -> Result<Self, Self::Error>
    {
        let name =
            deserializer.de_tag_with("name", IgnoreEnd::No, |_, deserializer| {
                deserializer.de_text()
            })?;

        let kind =
            deserializer.de_tag_with("kind", IgnoreEnd::No, |_, deserializer| {
                match deserializer.de_text()?.as_str() {
                    "Fruit" => Ok(FoodKind::Fruit),
                    "Vegetable" => Ok(FoodKind::Vegetable),
                    unknown_kind => {
                        Err(FoodError::UnknownFoodKind(unknown_kind.to_string()))
                    }
                }
            })?;

        Ok(Self { name, kind })
    }
}

#[derive(Debug)]
enum FoodKind
{
    Fruit,
    Vegetable,
}

#[derive(Debug, thiserror::Error)]
enum FoodError
{
    #[error("Unknown food kind '{0}'")]
    UnknownFoodKind(String),

    #[error(transparent)]
    DeserializeFailed(#[from] DeserializerError<Infallible>),
}

impl<DeError: Into<Self>> From<DeserializerError<DeError>> for FoodError
{
    fn from(err: DeserializerError<DeError>) -> Self
    {
        if let DeserializerError::DeserializeFailed(de_err) = err {
            return de_err.into();
        }

        err.into_never_de_err().into()
    }
}

#[cfg(test)]
mod tests
{
    use mockall::mock;
    use mockall::predicate::{always, eq};
    use xml_stinks::deserializer::{Error, MaybeStatic};

    use super::*;

    mock! {
        pub Deserializer {}

        impl Deserializer for Deserializer
        {
            fn de_tag<De: DeserializeTagged>(
                &mut self,
                tag_name: &str,
                ignore_end: IgnoreEnd,
            ) -> Result<De, Error<De::Error>>;

            fn de_tag_with<TOutput, Err, Func>(
                &mut self,
                tag_name: &str,
                ignore_end: IgnoreEnd,
                deserialize: Func,
            ) -> Result<TOutput, Error<Err>>
            where
                TOutput: MaybeStatic,
                Err: std::error::Error + Send + Sync + 'static,
                Func: FnOnce(&TagStart, &mut MockDeserializer) -> Result<TOutput, Err> + MaybeStatic;

            fn de_tag_list<De, TagName>(
                &mut self,
                tag_name: Option<TagName>
            ) -> Result<Vec<De>, Error<De::Error>>
                where
                    De: DeserializeTagged,
                    TagName: AsRef<str> + MaybeStatic;

            fn de_text(&mut self) -> Result<String, Error<Infallible>>;

            fn skip_to_tag_start(&mut self, tag_name: &str) -> Result<(), Error<Infallible>>;

            fn skip_to_tag_end(&mut self, tag_name: &str) -> Result<(), Error<Infallible>>;
        }
    }

    #[test]
    fn deserialize_food_works()
    {
        let mut mock_deserializer = MockDeserializer::new();

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError<Infallible>>()
            .with(eq("name"), eq(IgnoreEnd::No), always())
            .returning(|tag_name, _, func| {
                let mut deserializer = MockDeserializer::new();

                deserializer
                    .expect_de_text()
                    .returning(|| Ok("Carrot".to_string()))
                    .once();

                Ok(func(&TagStart::new(tag_name), &mut deserializer)?)
            })
            .once();

        mock_deserializer
            .expect_de_tag_with::<FoodKind, FoodError>()
            .with(eq("kind"), eq(IgnoreEnd::No), always())
            .returning(|tag_name, _, func| {
                let mut deserializer = MockDeserializer::new();

                deserializer
                    .expect_de_text()
                    .returning(|| Ok("Vegetable".to_string()))
                    .once();

                Ok(func(&TagStart::new(tag_name), &mut deserializer)?)
            })
            .once();

        let food = Food::deserialize(&TagStart::new("food"), &mut mock_deserializer)
            .expect("Expected Ok");

        assert_eq!(food.name, "Carrot");
        assert!(matches!(food.kind, FoodKind::Vegetable));
    }
}