rbx_xml 2.0.1

Implementation of Roblox's XML file formats, rbxlx and rbxmx
Documentation
use std::io::{Read, Write};

use crate::{
    core::XmlType,
    deserializer_core::XmlEventReader,
    error::{DecodeError, EncodeError},
    serializer_core::XmlEventWriter,
};

macro_rules! float_type {
    ($rust_type: ident, $xml_name: expr) => {
        impl XmlType for $rust_type {
            const XML_TAG_NAME: &'static str = $xml_name;

            fn write_xml<W: Write>(
                &self,
                writer: &mut XmlEventWriter<W>,
            ) -> Result<(), EncodeError> {
                if *self == $rust_type::INFINITY {
                    writer.write_characters("INF")
                } else if *self == $rust_type::NEG_INFINITY {
                    writer.write_characters("-INF")
                } else if self.is_nan() {
                    writer.write_characters("NAN")
                } else {
                    writer.write_characters(self)
                }
            }

            fn read_xml<R: Read>(reader: &mut XmlEventReader<R>) -> Result<Self, DecodeError> {
                let contents = reader.read_characters()?;

                Ok(match contents.as_str() {
                    "INF" => $rust_type::INFINITY,
                    "-INF" => $rust_type::NEG_INFINITY,
                    "NAN" => $rust_type::NAN,
                    number => number.parse().map_err(|e| reader.error(e))?,
                })
            }
        }
    };
}

macro_rules! int_type {
    ($rust_type: ty, $xml_name: expr) => {
        impl XmlType for $rust_type {
            const XML_TAG_NAME: &'static str = $xml_name;

            fn write_xml<W: Write>(
                &self,
                writer: &mut XmlEventWriter<W>,
            ) -> Result<(), EncodeError> {
                writer.write_characters(*self)
            }

            fn read_xml<R: Read>(reader: &mut XmlEventReader<R>) -> Result<Self, DecodeError> {
                reader
                    .read_characters()?
                    .parse()
                    .map_err(|e| reader.error(e))
            }
        }
    };
}

float_type!(f32, "float");
float_type!(f64, "double");
int_type!(i32, "int");
int_type!(i64, "int64");

// Convenience implementations for other types.
// FIXME: This feels weird to bundle into the XmlType trait.
int_type!(i16, "<NOT A REAL TYPE>");
int_type!(u16, "<NOT A REAL TYPE>");

#[cfg(test)]
mod test {
    use crate::{core::XmlType, deserializer_core::XmlEventReader, test_util};

    #[test]
    fn round_trip_f32() {
        test_util::test_xml_round_trip(&123456.0f32);
    }

    #[test]
    fn round_trip_f64() {
        test_util::test_xml_round_trip(&123456.0f64);
    }

    #[test]
    fn round_trip_i32() {
        test_util::test_xml_round_trip(&-4654321i32);
    }

    #[test]
    fn round_trip_i64() {
        test_util::test_xml_round_trip(&281474976710656i64);
    }

    #[test]
    fn test_inf_and_nan_deserialize() {
        test_util::test_xml_deserialize(r#"<float name="foo">INF</float>"#, &f32::INFINITY);

        test_util::test_xml_deserialize(r#"<float name="foo">-INF</float>"#, &f32::NEG_INFINITY);

        // Can't just use test_util::test_xml_deserialize, because NaN != NaN!

        // Clippy's suggestion here doesn't work and is less clear than just
        // using as_bytes.
        #[allow(clippy::string_lit_as_bytes)]
        let mut reader = XmlEventReader::from_source(r#"<float name="foo">NAN</float>"#.as_bytes());
        reader.next().unwrap().unwrap(); // Eat StartDocument event
        let value = f32::read_outer_xml(&mut reader).unwrap();
        assert!(value.is_nan());
    }

    #[test]
    fn test_inf_and_nan_serialize() {
        test_util::test_xml_serialize(r#"<float name="foo">INF</float>"#, &f32::INFINITY);

        test_util::test_xml_serialize(r#"<float name="foo">-INF</float>"#, &f32::NEG_INFINITY);

        test_util::test_xml_serialize(r#"<float name="foo">NAN</float>"#, &f32::NAN);
    }
}