From de374cf182d6f21f8b873ce90bab87d57ac73ce8 Mon Sep 17 00:00:00 2001 From: Lukas Friman Date: Mon, 5 May 2025 15:07:56 +0200 Subject: [PATCH 1/2] Started work on xml-rs (De)serializer. --- Cargo.lock | 17 + Cargo.toml | 2 +- xmlity-xml-rs/CHANGELOG.md | 1 + xmlity-xml-rs/Cargo.toml | 23 + xmlity-xml-rs/README.md | 27 + xmlity-xml-rs/src/de.rs | 774 +++++ xmlity-xml-rs/src/lib.rs | 123 + xmlity-xml-rs/src/ser.rs | 693 +++++ xmlity-xml-rs/tests/XMLSchema.xsd | 2534 +++++++++++++++++ xmlity-xml-rs/tests/elements/attribute.rs | 79 + xmlity-xml-rs/tests/elements/basic.rs | 83 + xmlity-xml-rs/tests/elements/default.rs | 129 + xmlity-xml-rs/tests/elements/extendable.rs | 65 + xmlity-xml-rs/tests/elements/generics.rs | 97 + .../elements/inline_attribute_declarations.rs | 59 + .../tests/elements/inline_declarations.rs | 171 ++ xmlity-xml-rs/tests/elements/mixed.rs | 82 + xmlity-xml-rs/tests/elements/mod.rs | 11 + .../tests/elements/namespace_expr.rs | 317 +++ .../tests/elements/single_namespace.rs | 314 ++ xmlity-xml-rs/tests/elements/strict_order.rs | 459 +++ xmlity-xml-rs/tests/features.rs | 5 + xmlity-xml-rs/tests/groups/basic.rs | 230 ++ xmlity-xml-rs/tests/groups/generics.rs | 41 + xmlity-xml-rs/tests/groups/mod.rs | 2 + xmlity-xml-rs/tests/other/mod.rs | 2 + xmlity-xml-rs/tests/other/variant.rs | 71 + xmlity-xml-rs/tests/other/xml_value.rs | 87 + xmlity-xml-rs/tests/text/enum_value.rs | 29 + .../tests/text/enum_value_rename_all.rs | 320 +++ xmlity-xml-rs/tests/text/extendable.rs | 22 + xmlity-xml-rs/tests/text/mixed.rs | 23 + xmlity-xml-rs/tests/text/mod.rs | 5 + xmlity-xml-rs/tests/text/strings.rs | 3 + xmlity-xml-rs/tests/utils.rs | 91 + xmlity-xml-rs/tests/xsd.rs | 772 +++++ 36 files changed, 7762 insertions(+), 1 deletion(-) create mode 100644 xmlity-xml-rs/CHANGELOG.md create mode 100644 xmlity-xml-rs/Cargo.toml create mode 100644 xmlity-xml-rs/README.md create mode 100644 xmlity-xml-rs/src/de.rs create mode 100644 xmlity-xml-rs/src/lib.rs create mode 100644 xmlity-xml-rs/src/ser.rs create mode 100644 xmlity-xml-rs/tests/XMLSchema.xsd create mode 100644 xmlity-xml-rs/tests/elements/attribute.rs create mode 100644 xmlity-xml-rs/tests/elements/basic.rs create mode 100644 xmlity-xml-rs/tests/elements/default.rs create mode 100644 xmlity-xml-rs/tests/elements/extendable.rs create mode 100644 xmlity-xml-rs/tests/elements/generics.rs create mode 100644 xmlity-xml-rs/tests/elements/inline_attribute_declarations.rs create mode 100644 xmlity-xml-rs/tests/elements/inline_declarations.rs create mode 100644 xmlity-xml-rs/tests/elements/mixed.rs create mode 100644 xmlity-xml-rs/tests/elements/mod.rs create mode 100644 xmlity-xml-rs/tests/elements/namespace_expr.rs create mode 100644 xmlity-xml-rs/tests/elements/single_namespace.rs create mode 100644 xmlity-xml-rs/tests/elements/strict_order.rs create mode 100644 xmlity-xml-rs/tests/features.rs create mode 100644 xmlity-xml-rs/tests/groups/basic.rs create mode 100644 xmlity-xml-rs/tests/groups/generics.rs create mode 100644 xmlity-xml-rs/tests/groups/mod.rs create mode 100644 xmlity-xml-rs/tests/other/mod.rs create mode 100644 xmlity-xml-rs/tests/other/variant.rs create mode 100644 xmlity-xml-rs/tests/other/xml_value.rs create mode 100644 xmlity-xml-rs/tests/text/enum_value.rs create mode 100644 xmlity-xml-rs/tests/text/enum_value_rename_all.rs create mode 100644 xmlity-xml-rs/tests/text/extendable.rs create mode 100644 xmlity-xml-rs/tests/text/mixed.rs create mode 100644 xmlity-xml-rs/tests/text/mod.rs create mode 100644 xmlity-xml-rs/tests/text/strings.rs create mode 100644 xmlity-xml-rs/tests/utils.rs create mode 100644 xmlity-xml-rs/tests/xsd.rs diff --git a/Cargo.lock b/Cargo.lock index e5fce9e..9a552a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,6 +369,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "xml-rs" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" + [[package]] name = "xmlity" version = "0.0.2" @@ -400,6 +406,17 @@ dependencies = [ "xmlity", ] +[[package]] +name = "xmlity-xml-rs" +version = "0.0.2" +dependencies = [ + "pretty_assertions", + "rstest", + "thiserror", + "xml-rs", + "xmlity", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 78ac29f..b342065 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["xmlity", "xmlity-derive", "xmlity-quick-xml"] +members = ["xmlity", "xmlity-derive", "xmlity-quick-xml", "xmlity-xml-rs"] [workspace.package] version = "0.0.2" diff --git a/xmlity-xml-rs/CHANGELOG.md b/xmlity-xml-rs/CHANGELOG.md new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/xmlity-xml-rs/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/xmlity-xml-rs/Cargo.toml b/xmlity-xml-rs/Cargo.toml new file mode 100644 index 0000000..b5dae4c --- /dev/null +++ b/xmlity-xml-rs/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "xmlity-xml-rs" +description = "XMLity implementation of xml-rs." +version = "0.0.2" +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +documentation.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +exclude.workspace = true + +[dependencies] +thiserror.workspace = true +xmlity.workspace = true +xml-rs = "0.8.26" + + +[dev-dependencies] +pretty_assertions.workspace = true +rstest.workspace = true +xmlity = { workspace = true, features = ["derive"] } diff --git a/xmlity-xml-rs/README.md b/xmlity-xml-rs/README.md new file mode 100644 index 0000000..0132df8 --- /dev/null +++ b/xmlity-xml-rs/README.md @@ -0,0 +1,27 @@ +# # XMLity xml-rs   [![Build Status]][actions] [![Latest Version]][crates.io] [![Latest Docs]][docs.rs] [![xmlity msrv]][Rust 1.82] + +[Build Status]: https://img.shields.io/github/actions/workflow/status/lukasfri/xmlity/rust.yaml?branch=main +[actions]: https://github.com/lukasfri/xmlity/actions?query=branch%3Amain +[Latest Version]: https://img.shields.io/crates/v/xmlity-quick-xml.svg +[crates.io]: https://crates.io/crates/xmlity-quick-xml +[Latest Docs]: https://img.shields.io/badge/docs.rs-Latest-bbbbbb.svg +[docs.rs]: https://docs.rs/xmlity-quick-xml/latest/xmlity_quick_xml +[xmlity msrv]: https://img.shields.io/badge/rustc-1.82.0+-ab6000.svg +[Rust 1.82]: https://blog.rust-lang.org/2023/06/01/Rust-1.82.0.html + +This crate contains the implementation of the [`xml-rs`] backend for XMLity. It is the intention to keep this crate up to date with the latest version of `xml-rs` and `xmlity`. + +## License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Serde by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + diff --git a/xmlity-xml-rs/src/de.rs b/xmlity-xml-rs/src/de.rs new file mode 100644 index 0000000..e3b9dad --- /dev/null +++ b/xmlity-xml-rs/src/de.rs @@ -0,0 +1,774 @@ +use std::{io::Read, ops::Deref}; + +use xml::{ + name::OwnedName, + reader::{EventReader, XmlEvent}, +}; + +use xmlity::{ + de::{self, Error as _, Unexpected, Visitor}, + Deserialize, ExpandedName, LocalName, QName, XmlNamespace, +}; + +use crate::HasXmlRsAlternative; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Quick XML error: {0}")] + XmlRs(#[from] xml::reader::Error), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Unexpected: {0}")] + Unexpected(xmlity::de::Unexpected), + #[error("Custom: {0}")] + Custom(String), + #[error("Wrong name: expected {expected:?}, got {actual:?}")] + WrongName { + actual: Box>, + expected: Box>, + }, + #[error("Unknown child")] + UnknownChild, + #[error("Invalid UTF-8: {0}")] + InvalidUtf8(#[from] std::string::FromUtf8Error), + #[error("Invalid string")] + InvalidString, + #[error("Missing field: {field}")] + MissingField { field: String }, + #[error("No possible variant: {ident}")] + NoPossibleVariant { ident: String }, + #[error("Missing data")] + MissingData, +} + +impl xmlity::de::Error for Error { + fn custom(msg: T) -> Self { + Error::Custom(msg.to_string()) + } + + fn wrong_name(actual: &ExpandedName<'_>, expected: &ExpandedName<'_>) -> Self { + Error::WrongName { + actual: Box::new(actual.clone().into_owned()), + expected: Box::new(expected.clone().into_owned()), + } + } + + fn unexpected_visit(unexpected: xmlity::de::Unexpected, _expected: &T) -> Self { + Error::Unexpected(unexpected) + } + + fn missing_field(field: &str) -> Self { + Error::MissingField { + field: field.to_string(), + } + } + + fn no_possible_variant(ident: &str) -> Self { + Error::NoPossibleVariant { + ident: ident.to_string(), + } + } + + fn missing_data() -> Self { + Error::MissingData + } + + fn unknown_child() -> Self { + Error::UnknownChild + } + + fn invalid_string() -> Self { + Error::InvalidString + } +} + +pub enum Peeked<'a> { + None, + Text, + CData, + Element { + name: QName<'a>, + namespace: Option>, + }, +} + +pub struct Deserializer { + reader: EventReader, + current_depth: i16, + peeked_event: Option, +} + +impl From> for Deserializer { + fn from(reader: EventReader) -> Self { + Self::new(reader) + } +} + +impl Deserializer { + pub fn new(reader: EventReader) -> Self { + Self { + reader, + current_depth: 0, + peeked_event: None, + } + } + + fn read_event(&mut self) -> Result, Error> { + while let Ok(event) = self.reader.read_event() { + match event { + XmlEvent::EndDocument => return Ok(None), + XmlEvent::Characters(text) if text.clone().into_inner().trim_ascii().is_empty() => { + continue; + } + event => return Ok(Some(event)), + } + } + + Ok(None) + } + + fn read_until_element_end(&mut self, name: &QuickName, depth: i16) -> Result<(), Error> { + while let Some(event) = self.peek_event() { + let correct_name = match event { + XmlEvent::EndElement { name: end_name } if end_name == *name => true, + XmlEvent::EndDocument => return Err(Error::Unexpected(Unexpected::Eof)), + _ => false, + }; + + if correct_name && self.current_depth == depth { + return Ok(()); + } + + self.next_event(); + } + + Err(Error::Unexpected(de::Unexpected::Eof)) + } + + pub fn peek_event(&mut self) -> Option<&XmlEvent> { + if self.peeked_event.is_some() { + return self.peeked_event.as_ref(); + } + + self.peeked_event = self.read_event().ok().flatten(); + self.peeked_event.as_ref() + } + + pub fn next_event(&mut self) -> Option { + let event = if self.peeked_event.is_some() { + self.peeked_event.take() + } else { + self.read_event().ok().flatten() + }; + + if matches!(event, Some(XmlEvent::EndElement { .. })) { + self.current_depth -= 1; + } + if matches!(event, Some(XmlEvent::StartElement { .. })) { + self.current_depth += 1; + } + + event + } + + pub fn create_sub_seq_access<'p>(&'p mut self) -> SubSeqAccess<'p, R> { + SubSeqAccess::Filled { + current: Some(self.clone()), + parent: self, + } + } + + pub fn try_deserialize( + &mut self, + closure: impl for<'a> FnOnce(&'a mut Deserializer) -> Result, + ) -> Result { + let mut sub_deserializer = self.clone(); + let res = closure(&mut sub_deserializer); + + if res.is_ok() { + *self = sub_deserializer; + } + res + } + + pub fn expand_name<'a>(&self, qname: QuickName<'a>) -> ExpandedName<'a> { + let (resolve_result, _) = self.reader.resolve(qname, false); + let namespace = xml_namespace_from_resolve_result(resolve_result).map(|ns| ns.into_owned()); + + ExpandedName::new(LocalName::from_quick_xml(qname.local_name()), namespace) + } + + pub fn resolve_bytes_start<'a>(&self, bytes_start: &'a BytesStart<'a>) -> ExpandedName<'a> { + self.expand_name(bytes_start.name()) + } + + pub fn resolve_attribute<'a>(&self, attribute: &'a Attribute<'a>) -> ExpandedName<'a> { + self.expand_name(attribute.key) + } +} + +pub struct ElementAccess<'a, R: Read> { + deserializer: Option<&'a mut Deserializer>, + attribute_index: usize, + start_name: OwnedName, + start_depth: i16, + empty: bool, +} + +impl Drop for ElementAccess<'_, R> { + fn drop(&mut self) { + self.try_end().ok(); + } +} + +impl ElementAccess<'_, R> { + fn deserializer(&self) -> &Deserializer { + self.deserializer + .as_ref() + .expect("Should not be called after ElementAccess has been consumed") + } + + fn try_end(&mut self) -> Result<(), Error> { + if self.empty { + return Ok(()); + } + + if let Some(deserializer) = self.deserializer.as_mut() { + deserializer + .read_until_element_end(&self.start_name.into_xmlity(), self.start_depth)?; + } + Ok(()) + } +} + +pub struct AttributeAccess<'a> { + name: ExpandedName<'a>, + value: String, +} + +impl<'a> de::AttributeAccess<'a> for AttributeAccess<'a> { + type Error = Error; + + fn name(&self) -> ExpandedName<'_> { + self.name.clone() + } + + fn value(&self) -> &str { + self.value.as_str() + } +} + +struct EmptySeqAccess; + +impl<'de> de::SeqAccess<'de> for EmptySeqAccess { + type Error = Error; + type SubAccess<'s> + = EmptySeqAccess + where + Self: 's; + + fn next_element_seq(&mut self) -> Result, Self::Error> + where + T: Deserialize<'de>, + { + Ok(None) + } + + fn next_element(&mut self) -> Result, Self::Error> + where + T: Deserialize<'de>, + { + Ok(None) + } + + fn sub_access(&mut self) -> Result, Self::Error> { + Ok(EmptySeqAccess) + } +} + +struct AttributeDeserializer<'a> { + name: ExpandedName<'a>, + value: String, +} + +impl<'a> xmlity::Deserializer<'a> for AttributeDeserializer<'a> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'a>, + { + visitor.visit_attribute(AttributeAccess { + name: self.name, + value: self.value, + }) + } + + fn deserialize_seq(self, _: V) -> Result + where + V: Visitor<'a>, + { + Err(Self::Error::Unexpected(de::Unexpected::Seq)) + } +} + +pub struct SubAttributesAccess<'a, 'r, R: Read + 'r> { + deserializer: &'a Deserializer, + bytes_start: &'a BytesStart<'r>, + attribute_index: usize, + write_attribute_to: &'a mut usize, +} + +impl Drop for SubAttributesAccess<'_, '_, R> { + fn drop(&mut self) { + *self.write_attribute_to = self.attribute_index; + } +} + +fn next_attribute<'a, 'de, T: Deserialize<'de>, R: Read>( + deserializer: &'a Deserializer, + bytes_start: &'a BytesStart<'_>, + attribute_index: &'a mut usize, +) -> Result, Error> { + let (attribute, key) = loop { + let Some(attribute) = bytes_start.attributes().nth(*attribute_index) else { + return Ok(None); + }; + + let attribute = attribute?; + + let key = deserializer.resolve_attribute(&attribute).into_owned(); + + const XMLNS_NAMESPACE: XmlNamespace<'static> = + XmlNamespace::new_dangerous("http://www.w3.org/2000/xmlns/"); + + if key.namespace() == Some(&XMLNS_NAMESPACE) { + *attribute_index += 1; + continue; + } + + break (attribute, key); + }; + + let value = String::from_utf8(attribute.value.into_owned()) + .expect("attribute value should be valid utf8"); + + let deserializer = AttributeDeserializer { name: key, value }; + + let res = T::deserialize(deserializer)?; + + // Only increment the index if the deserialization was successful + *attribute_index += 1; + + Ok(Some(res)) +} + +impl<'de, R: Read> de::AttributesAccess<'de> for SubAttributesAccess<'_, 'de, R> { + type Error = Error; + + type SubAccess<'a> + = SubAttributesAccess<'a, 'de, R> + where + Self: 'a; + + fn next_attribute(&mut self) -> Result, Self::Error> + where + T: Deserialize<'de>, + { + next_attribute( + self.deserializer, + self.bytes_start, + &mut self.attribute_index, + ) + } + + fn sub_access(&mut self) -> Result, Self::Error> { + Ok(Self::SubAccess { + deserializer: self.deserializer, + bytes_start: self.bytes_start, + attribute_index: self.attribute_index, + write_attribute_to: self.write_attribute_to, + }) + } +} + +impl<'de> de::AttributesAccess<'de> for ElementAccess<'_, 'de> { + type Error = Error; + + type SubAccess<'a> + = SubAttributesAccess<'a, 'de> + where + Self: 'a; + + fn next_attribute(&mut self) -> Result, Self::Error> + where + T: Deserialize<'de>, + { + next_attribute( + self.deserializer + .as_ref() + .expect("deserializer should be set"), + &self.bytes_start, + &mut self.attribute_index, + ) + } + + fn sub_access(&mut self) -> Result, Self::Error> { + Ok(Self::SubAccess { + bytes_start: &self.bytes_start, + attribute_index: self.attribute_index, + write_attribute_to: &mut self.attribute_index, + deserializer: self + .deserializer + .as_ref() + .expect("Should not be called after ElementAccess has been consumed"), + }) + } +} + +impl<'a, 'de, R: Read> de::ElementAccess<'de> for ElementAccess<'a, 'de, R> { + type ChildrenAccess = ChildrenAccess<'a, R>; + + fn name(&self) -> ExpandedName<'_> { + self.deserializer().resolve_bytes_start(&self.bytes_start) + } + + fn children(mut self) -> Result { + Ok(if self.empty { + ChildrenAccess::Empty + } else { + let deserializer = self + .deserializer + .take() + .expect("Should not be called after ElementAccess has been consumed"); + + ChildrenAccess::Filled { + expected_end: QName::from_quick_xml(self.bytes_start.name()).into_owned(), + start_depth: self.start_depth, + deserializer, + } + }) + } +} + +pub enum ChildrenAccess<'a, R: Read> { + Filled { + expected_end: QName<'static>, + deserializer: &'a mut Deserializer, + start_depth: i16, + }, + Empty, +} + +impl Drop for ChildrenAccess<'_, R> { + fn drop(&mut self) { + let ChildrenAccess::Filled { + expected_end, + deserializer, + start_depth, + } = self + else { + return; + }; + + deserializer + .read_until_element_end(&expected_end.into_xmlity(), *start_depth) + .unwrap(); + } +} + +impl<'r, R: Read + 'r> de::SeqAccess<'r> for ChildrenAccess<'_, R> { + type Error = Error; + + type SubAccess<'s> + = SubSeqAccess<'s, R> + where + Self: 's; + + fn next_element(&mut self) -> Result, Self::Error> + where + T: Deserialize<'r>, + { + let ChildrenAccess::Filled { + expected_end, + deserializer, + start_depth, + } = self + else { + return Ok(None); + }; + + if deserializer.peek_event().is_none() { + return Ok(None); + } + + let current_depth = deserializer.current_depth; + + if let Some(XmlEvent::EndElement { name: end_name }) = deserializer.peek_event() { + if end_name.into_xmlity() != *expected_end && current_depth == *start_depth { + return Err(Error::custom(format!( + "Expected end of element {}, found end of element {}", + expected_end, + end_name.into_xmlity() + ))); + } + + return Ok(None); + } + + deserializer + .try_deserialize(|deserializer| Deserialize::<'r>::deserialize(deserializer)) + .map(Some) + } + + fn next_element_seq(&mut self) -> Result, Self::Error> + where + T: Deserialize<'r>, + { + let ChildrenAccess::Filled { + expected_end, + deserializer, + start_depth, + } = self + else { + return Ok(None); + }; + + if deserializer.peek_event().is_none() { + return Ok(None); + } + + let current_depth = deserializer.current_depth; + + if let Some(XmlEvent::EndElement { name: end_name }) = deserializer.peek_event() { + if *expected_end != end_name.into_xmlity() && current_depth == *start_depth { + return Err(Error::custom(format!( + "Expected end of element {}, found end of element {}", + expected_end, + end_name.into_xmlity() + ))); + } + + return Ok(None); + } + + deserializer + .try_deserialize(|deserializer| Deserialize::<'r>::deserialize_seq(deserializer)) + .map(Some) + } + + fn sub_access(&mut self) -> Result, Self::Error> { + let ChildrenAccess::Filled { deserializer, .. } = self else { + return Ok(SubSeqAccess::Empty); + }; + + Ok(deserializer.create_sub_seq_access()) + } +} + +pub struct SeqAccess<'a, R: Read> { + deserializer: &'a mut Deserializer, +} + +#[allow(clippy::large_enum_variant)] +pub enum SubSeqAccess<'p, R: Read> { + Filled { + current: Option>, + parent: &'p mut Deserializer, + }, + Empty, +} + +impl Drop for SubSeqAccess<'_, R> { + fn drop(&mut self) { + if let SubSeqAccess::Filled { current, parent } = self { + **parent = current.take().expect("SubSeqAccess dropped twice"); + } + } +} + +impl<'r, R: Read + 'r> de::SeqAccess<'r> for SubSeqAccess<'_, R> { + type Error = Error; + + type SubAccess<'s> + = SubSeqAccess<'s, R> + where + Self: 's; + + fn next_element_seq(&mut self) -> Result, Self::Error> + where + T: Deserialize<'r>, + { + let Self::Filled { current, .. } = self else { + return Ok(None); + }; + + let deserializer = current.as_mut().expect("SubSeqAccess used after drop"); + + if deserializer.peek_event().is_none() { + return Ok(None); + } + + deserializer + .try_deserialize(|deserializer| Deserialize::<'r>::deserialize_seq(deserializer)) + .map(Some) + } + + fn next_element(&mut self) -> Result, Self::Error> + where + T: Deserialize<'r>, + { + let Self::Filled { current, .. } = self else { + return Ok(None); + }; + + let deserializer = current.as_mut().expect("SubSeqAccess used after drop"); + + if deserializer.peek_event().is_none() { + return Ok(None); + } + + deserializer + .try_deserialize(|deserializer| Deserialize::<'r>::deserialize(deserializer)) + .map(Some) + } + + fn sub_access(&mut self) -> Result, Self::Error> { + let Self::Filled { current, .. } = self else { + return Ok(SubSeqAccess::Empty); + }; + + Ok(current + .as_mut() + .expect("SubSeqAccess used after drop") + .create_sub_seq_access()) + } +} + +impl<'r, R: Read + 'r> de::SeqAccess<'r> for SeqAccess<'_, R> { + type Error = Error; + + type SubAccess<'s> + = SubSeqAccess<'s, R> + where + Self: 's; + + fn next_element_seq(&mut self) -> Result, Self::Error> + where + T: Deserialize<'r>, + { + if self.deserializer.peek_event().is_none() { + return Ok(None); + } + + self.deserializer + .try_deserialize(|deserializer| Deserialize::<'r>::deserialize_seq(deserializer)) + .map(Some) + } + + fn next_element(&mut self) -> Result, Self::Error> + where + T: Deserialize<'r>, + { + if self.deserializer.peek_event().is_none() { + return Ok(None); + } + + self.deserializer + .try_deserialize(|deserializer| Deserialize::<'r>::deserialize(deserializer)) + .map(Some) + } + + fn sub_access(&mut self) -> Result, Self::Error> { + Ok(SubSeqAccess::Filled { + current: Some(self.deserializer.clone()), + parent: self.deserializer, + }) + } +} + +impl<'r, R: Read + 'r> xmlity::Deserializer<'r> for &mut Deserializer { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: de::Visitor<'r>, + { + let event = self.next_event().ok_or_else(|| Error::custom("EOF"))?; + + match event { + XmlEvent::StartElement { + name, + namespace, + attributes, + } => { + let value = Visitor::visit_element( + visitor, + ElementAccess { + start_name: name, + start_depth: self.current_depth, + deserializer: Some(self), + empty: false, + attribute_index: 0, + }, + )?; + + let end_event = self.next_event().ok_or_else(|| Error::custom("EOF"))?; + + let success = if let XmlEvent::EndElement { name: end_name } = &end_event { + *end_name == name + } else { + false + }; + + if success { + Ok(value) + } else { + Err(Error::custom("No matching end element")) + } + } + XmlEvent::EndElement { .. } => Err(Error::custom("Unexpected end element")), + + XmlEvent::Characters(bytes_text) => visitor.visit_text(bytes_text.deref()), + XmlEvent::Whitespace(bytes_text) => visitor.visit_text(bytes_text.deref()), + XmlEvent::CData(bytes_cdata) => visitor.visit_cdata(bytes_cdata.deref()), + XmlEvent::Comment(bytes_text) => visitor.visit_comment(bytes_text.deref()), + XmlEvent::StartDocument { + encoding, + standalone, + version, + } => visitor.visit_decl( + version.to_string(), + Some(encoding.to_string()), + standalone.map(|standalone| standalone.to_string()), + ), + XmlEvent::ProcessingInstruction { data, name } => visitor.visit_pi(bytes_pi.deref()), + XmlEvent::EndDocument => Err(Error::custom("Unexpected EOF")), + } + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: de::Visitor<'r>, + { + visitor.visit_seq(SeqAccess { deserializer: self }) + } +} + +impl<'r, R: Read + 'r> xmlity::Deserializer<'r> for Deserializer { + type Error = Error; + + fn deserialize_any(mut self, visitor: V) -> Result + where + V: de::Visitor<'r>, + { + (&mut self).deserialize_any(visitor) + } + + fn deserialize_seq(mut self, visitor: V) -> Result + where + V: de::Visitor<'r>, + { + (&mut self).deserialize_seq(visitor) + } +} diff --git a/xmlity-xml-rs/src/lib.rs b/xmlity-xml-rs/src/lib.rs new file mode 100644 index 0000000..43d2044 --- /dev/null +++ b/xmlity-xml-rs/src/lib.rs @@ -0,0 +1,123 @@ +//! # XMLity Quick XML +//! +//! This crate contains a reference implementation of the `xmlity` crate using the `quick-xml` crate. It is the intention to keep this crate up to date with the latest version of `quick-xml` and `xmlity`. +#[cfg(doctest)] +#[doc = include_str!("../../README.md")] +struct _RootReadMeDocTests; + +#[cfg(doctest)] +#[doc = include_str!("../README.md")] +struct _ReadMeDocTests; + +use xmlity::{ser::IncludePrefix, ExpandedName, LocalName, Prefix, QName, XmlNamespace}; + +pub mod de; +pub mod ser; + +pub use de::Deserializer; +pub use ser::{to_string, Serializer}; +use xml::name::OwnedName; + +pub trait HasXmlRsAlternative { + type XmlityEquivalent; + + fn into_xmlity(self) -> Self::XmlityEquivalent; +} + +impl HasXmlRsAlternative for OwnedName { + type XmlityEquivalent = QName<'static>; + + fn into_xmlity(self) -> Self::XmlityEquivalent { + QName::new( + self.prefix + .map(|prefix| Prefix::new(prefix).expect("A xml-rs prefix should be valid")), + LocalName::new(self.local_name).expect("An xml-rs local name should be valid"), + ) + } +} + +impl<'a> HasXmlRsAlternative for &'a OwnedName { + type XmlityEquivalent = QName<'a>; + + fn into_xmlity(self) -> Self::XmlityEquivalent { + QName::new( + self.prefix + .as_deref() + .map(|prefix| Prefix::new(prefix).expect("A xml-rs prefix should be valid")), + LocalName::new(self.local_name.as_str()).expect("An xml-rs local name should be valid"), + ) + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct Attribute<'a> { + pub name: ExpandedName<'a>, + pub value: String, + pub enforce_prefix: IncludePrefix, + pub preferred_prefix: Option>, +} + +impl<'a> Attribute<'a> { + pub fn resolve(self, resolved_prefix: Option>) -> ResolvedAttribute<'a> { + ResolvedAttribute { + name: self.name.to_q_name(resolved_prefix), + value: self.value, + } + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct ResolvedAttribute<'a> { + pub name: QName<'a>, + pub value: String, +} + +fn declaration_into_attribute(xmlns: XmlnsDeclaration<'_>) -> ResolvedAttribute<'_> { + ResolvedAttribute { + name: XmlnsDeclaration::xmlns_qname(xmlns.prefix), + value: xmlns.namespace.as_str().to_owned(), + } +} + +/// An XML namespace declaration/singular mapping from a prefix to a namespace. +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct XmlnsDeclaration<'a> { + pub prefix: Prefix<'a>, + pub namespace: XmlNamespace<'a>, +} + +impl<'a> XmlnsDeclaration<'a> { + pub fn new(prefix: Prefix<'a>, namespace: XmlNamespace<'a>) -> Self { + Self { prefix, namespace } + } + + pub fn into_owned(self) -> XmlnsDeclaration<'static> { + XmlnsDeclaration { + prefix: self.prefix.into_owned(), + namespace: self.namespace.into_owned(), + } + } + + pub fn prefix(&self) -> &Prefix<'a> { + &self.prefix + } + + pub fn namespace(&self) -> &XmlNamespace<'a> { + &self.namespace + } + + /// Returns the QName for the XML namespace declaration. + pub fn xmlns_qname(prefix: Prefix<'_>) -> QName<'_> { + if prefix.is_default() { + QName::new( + None, + LocalName::new("xmlns").expect("xmlns is a valid local name"), + ) + } else { + QName::new( + Some(Prefix::new("xmlns").expect("xmlns is a valid prefix")), + LocalName::from(prefix), + ) + } + } +} diff --git a/xmlity-xml-rs/src/ser.rs b/xmlity-xml-rs/src/ser.rs new file mode 100644 index 0000000..eba6b38 --- /dev/null +++ b/xmlity-xml-rs/src/ser.rs @@ -0,0 +1,693 @@ +use core::str; +use std::collections::BTreeMap; +use std::io::Write; + +use xml::writer::{EventWriter, XmlEvent}; + +use xmlity::ser::IncludePrefix; +use xmlity::{ser, ExpandedName, Prefix, QName, Serialize, XmlNamespace}; + +use crate::{declaration_into_attribute, Attribute, XmlnsDeclaration}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Quick XML error: {0}")] + XmlRs(#[from] xml::writer::Error), + #[error("Unexpected: {0}")] + Unexpected(xmlity::de::Unexpected), + #[error("Custom: {0}")] + Custom(String), + #[error("Invalid UTF-8: {0}")] + InvalidUtf8(#[from] std::string::FromUtf8Error), +} + +impl xmlity::ser::Error for Error { + fn custom(msg: T) -> Self { + Error::Custom(msg.to_string()) + } +} + +fn serializer_to_string(serializer: EventWriter>, value: &T) -> Result +where + T: Serialize, +{ + let mut serializer = Serializer::from(serializer); + value.serialize(&mut serializer)?; + let bytes = serializer.into_inner(); + + String::from_utf8(bytes).map_err(Error::InvalidUtf8) +} + +pub fn to_string(value: &T) -> Result +where + T: Serialize, +{ + serializer_to_string(EventWriter::new(Vec::new()), value) +} + +pub fn to_string_pretty(value: &T, indentation: usize) -> Result +where + T: Serialize, +{ + todo!() +} + +struct NamespaceScope<'a> { + pub defined_namespaces: BTreeMap, XmlNamespace<'a>>, +} + +impl<'a> NamespaceScope<'a> { + pub fn new() -> Self { + Self { + defined_namespaces: BTreeMap::new(), + } + } + + const XML_PREFIX: Prefix<'static> = Prefix::new_dangerous("xml"); + const XML_NAMESPACE: XmlNamespace<'static> = + XmlNamespace::new_dangerous("http://www.w3.org/XML/1998/namespace"); + + pub fn top_scope() -> Self { + let mut scope = Self::new(); + scope + .defined_namespaces + .insert(Self::XML_PREFIX, Self::XML_NAMESPACE); + scope + } + + pub fn get_namespace<'b>(&'b self, prefix: &'b Prefix<'b>) -> Option<&'b XmlNamespace<'a>> { + self.defined_namespaces.get(prefix) + } +} + +struct NamespaceScopeContainer<'a> { + scopes: Vec>, + prefix_generator: PrefixGenerator, +} + +struct PrefixGenerator { + count: usize, +} + +impl PrefixGenerator { + pub fn index_to_name(index: usize) -> Prefix<'static> { + let letter = (index / 26) as u8 + b'a'; + let number = (index % 26) as u8 + b'0'; + let mut name = String::with_capacity(2); + name.push(letter as char); + name.push(number as char); + Prefix::new(name).expect("Invalid prefix generated") + } + + pub fn new() -> Self { + Self { count: 0 } + } + + pub fn new_prefix(&mut self) -> Prefix<'static> { + let name = Self::index_to_name(self.count); + self.count += 1; + name + } +} + +impl<'a> NamespaceScopeContainer<'a> { + pub fn new() -> Self { + Self { + scopes: vec![NamespaceScope::top_scope()], + prefix_generator: PrefixGenerator::new(), + } + } + + pub fn push_scope(&mut self) { + self.scopes.push(NamespaceScope::new()) + } + + pub fn pop_scope(&mut self) -> Option { + self.scopes.pop() + } + + pub fn get_namespace<'b>(&'b self, prefix: &'b Prefix<'b>) -> Option<&'b XmlNamespace<'a>> { + self.scopes + .iter() + .rev() + .find_map(|a| a.get_namespace(prefix)) + } + + /// Find matching prefix + pub fn find_matching_namespace<'b>( + &'b self, + namespace: &'b XmlNamespace<'b>, + ) -> Option<&'b Prefix<'a>> { + self.scopes.iter().rev().find_map(|a| { + a.defined_namespaces + .iter() + .find(|(_, found_namespace)| namespace == *found_namespace) + .map(|(prefix, _)| prefix) + }) + } + + /// This function takes in a namespace and tries to resolve it in different ways depending on the options provided. Unless `always_declare` is true, it will try to use an existing declaration. Otherwise, or if the namespace has not yet been declared, it will provide a declaration. + pub fn resolve_namespace<'b>( + &'b mut self, + namespace: &'b XmlNamespace<'b>, + preferred_prefix: Option<&'b Prefix<'b>>, + always_declare: IncludePrefix, + ) -> (Prefix<'b>, Option>) { + let existing_prefix = self + .find_matching_namespace(namespace) + // If we should always declare, we simply pretend it's not declared yet. + .filter(|_p| always_declare != IncludePrefix::Always); + + if let Some(existing_prefix) = existing_prefix { + return (existing_prefix.clone(), None); + } + + // If the namespace is not declared, use the specifically requested preferred prefix... + // ...if it is not already used and not the same as the existing prefix. + let prefix = preferred_prefix + .filter(|p| self.get_namespace(p).is_none_or(|n| n == namespace)) + // If the preferred prefix is not available, use the preferred namespace prefix from the serializer... + .or_else(|| { + preferred_prefix + // ...if it is not already used and not the same as the existing prefix. + .filter(|p| self.get_namespace(p).is_none_or(|n| n == namespace)) + }) + .cloned() + // If the preferred namespace prefix is not available, use a random prefix. + .unwrap_or_else(|| self.prefix_generator.new_prefix()); + + let xmlns = XmlnsDeclaration::new(prefix.clone(), namespace.clone()); + + self.scopes.last_mut().map(|a| { + a.defined_namespaces + .insert(prefix.clone().into_owned(), namespace.clone().into_owned()) + }); + + (prefix, Some(xmlns)) + } + + pub fn resolve_name<'b>( + &mut self, + name: ExpandedName<'b>, + preferred_prefix: Option<&Prefix<'b>>, + always_declare: IncludePrefix, + ) -> (QName<'b>, Option>) { + let (prefix, declaration) = name + .namespace() + .map(|namespace| self.resolve_namespace(namespace, preferred_prefix, always_declare)) + .unzip(); + + let declaration = declaration.flatten().map(|a| a.into_owned()); + let resolved_prefix = prefix.map(|a| a.into_owned()); + + let name = name.to_q_name(resolved_prefix); + (name.into_owned(), declaration) + } +} + +pub struct Serializer { + writer: EventWriter, + preferred_namespace_prefixes: BTreeMap, Prefix<'static>>, + namespace_scopes: NamespaceScopeContainer<'static>, +} + +impl Serializer { + pub fn new(writer: EventWriter) -> Self { + Self::new_with_namespaces(writer, BTreeMap::new()) + } + + pub fn new_with_namespaces( + writer: EventWriter, + preferred_namespace_prefixes: BTreeMap, Prefix<'static>>, + ) -> Self { + Self { + writer, + preferred_namespace_prefixes, + namespace_scopes: NamespaceScopeContainer::new(), + } + } + + pub fn into_inner(self) -> W { + self.writer.into_inner() + } + + pub fn push_namespace_scope(&mut self) { + self.namespace_scopes.push_scope() + } + + pub fn pop_namespace_scope(&mut self) { + self.namespace_scopes.pop_scope(); + } + + pub fn add_preferred_prefix( + &mut self, + namespace: XmlNamespace<'static>, + prefix: Prefix<'static>, + ) { + self.preferred_namespace_prefixes.insert(namespace, prefix); + } + + pub fn resolve_name<'b>( + &mut self, + name: ExpandedName<'b>, + preferred_prefix: Option<&Prefix<'b>>, + always_declare: IncludePrefix, + ) -> (QName<'b>, Option>) { + let name2 = name.clone(); + let preferred_prefix = preferred_prefix.or_else(|| { + name2 + .namespace() + .and_then(|a| self.preferred_namespace_prefixes.get(a)) + }); + + self.namespace_scopes + .resolve_name(name, preferred_prefix, always_declare) + } +} + +impl From> for Serializer { + fn from(writer: EventWriter) -> Self { + Self::new(writer) + } +} + +impl From for Serializer { + fn from(writer: W) -> Self { + Self::new(EventWriter::new(writer)) + } +} + +pub struct SerializeElement<'s, W: Write> { + serializer: &'s mut Serializer, + name: ExpandedName<'static>, + attributes: Vec>, + preferred_prefix: Option>, + enforce_prefix: IncludePrefix, +} + +pub struct AttributeSerializer<'t> { + name: ExpandedName<'static>, + on_end_add_to: &'t mut Vec>, + preferred_prefix: Option>, + enforce_prefix: IncludePrefix, +} + +impl ser::SerializeAttributeAccess for AttributeSerializer<'_> { + type Ok = (); + type Error = Error; + + fn include_prefix(&mut self, should_enforce: IncludePrefix) -> Result { + self.enforce_prefix = should_enforce; + Ok(()) + } + + fn preferred_prefix( + &mut self, + preferred_prefix: Option>, + ) -> Result { + self.preferred_prefix = preferred_prefix.map(Prefix::into_owned); + Ok(()) + } + + fn end>(self, value: S) -> Result { + self.on_end_add_to.push(Attribute { + name: self.name.into_owned(), + value: value.as_ref().to_owned(), + preferred_prefix: self.preferred_prefix, + enforce_prefix: self.enforce_prefix, + }); + + Ok(()) + } +} + +pub struct AttributeVecSerializer<'t> { + attributes: &'t mut Vec>, +} + +impl ser::AttributeSerializer for AttributeVecSerializer<'_> { + type Error = Error; + + type Ok = (); + type SerializeAttribute<'a> + = AttributeSerializer<'a> + where + Self: 'a; + + fn serialize_attribute( + &mut self, + name: &'_ ExpandedName<'_>, + ) -> Result, Self::Error> { + Ok(Self::SerializeAttribute { + name: name.clone().into_owned(), + on_end_add_to: &mut self.attributes, + preferred_prefix: None, + enforce_prefix: IncludePrefix::default(), + }) + } + + fn serialize_none(&mut self) -> Result { + Ok(()) + } +} + +impl<'s, W: Write> SerializeElement<'s, W> { + fn finish_start(self) -> (OwnedBytesStart, QName<'static>, &'s mut Serializer) { + let Self { + serializer, + name, + attributes, + enforce_prefix, + preferred_prefix, + } = self; + + let mut resolve_name_or_declare = + |name: &ExpandedName<'_>, + preferred_prefix: Option<&Prefix<'_>>, + enforce_prefix: IncludePrefix| + -> (QName<'static>, Option>) { + let (qname, decl) = + serializer.resolve_name(name.clone(), preferred_prefix, enforce_prefix); + + (qname.into_owned(), decl.map(|a| a.into_owned())) + }; + + let (elem_qname, elem_name_decl) = + resolve_name_or_declare(&name, preferred_prefix.as_ref(), enforce_prefix); + + let (attr_prefixes, attr_decls): (Vec<_>, Vec<_>) = attributes + .iter() + .map(|a| &a.name) + .map(|name| resolve_name_or_declare(name, None, IncludePrefix::default())) + .unzip(); + + let decls = elem_name_decl + .into_iter() + .chain(attr_decls.into_iter().flatten()) + .collect::>(); + + // Add declared namespaces first + let mut q_attributes = decls + .iter() + .map(|decl| declaration_into_attribute(decl.clone())) + .map(|attr| { + ( + OwnedQuickName::new(&attr.name), + attr.value.as_bytes().to_owned(), + ) + }) + .collect::>(); + + // Then add the attributes + q_attributes.extend( + attributes + .into_iter() + .zip(attr_prefixes) + .map(|(attr, qname)| attr.resolve(qname.prefix().cloned())) + .map(|attr| { + ( + OwnedQuickName::new(&attr.name), + attr.value.as_bytes().to_owned(), + ) + }), + ); + + let bytes_start = OwnedBytesStart { + name: OwnedQuickName::new(&elem_qname), + attributes: q_attributes, + }; + + (bytes_start, elem_qname, serializer) + } +} + +impl ser::SerializeAttributes for SerializeElement<'_, W> { + type Ok = (); + type Error = Error; + + fn serialize_attribute( + &mut self, + a: &A, + ) -> Result { + a.serialize_attribute(AttributeVecSerializer { + attributes: &mut self.attributes, + }) + } +} + +impl<'s, W: Write> ser::SerializeElement for SerializeElement<'s, W> { + type ChildrenSerializeSeq = ChildrenSerializeSeq<'s, W>; + + fn include_prefix(&mut self, should_enforce: IncludePrefix) -> Result { + self.enforce_prefix = should_enforce; + Ok(()) + } + fn preferred_prefix( + &mut self, + preferred_prefix: Option>, + ) -> Result { + self.preferred_prefix = preferred_prefix.map(Prefix::into_owned); + Ok(()) + } + + fn serialize_children(self) -> Result { + self.serializer.push_namespace_scope(); + let (bytes_start, end_name, serializer) = self.finish_start(); + + Ok(ChildrenSerializeSeq { + bytes_start: Some(bytes_start), + serializer, + end_name, + }) + } + + fn end(self) -> Result { + self.serializer.push_namespace_scope(); + let (bytes_start, _, serializer) = self.finish_start(); + + serializer + .writer + .write(XmlEvent::StartElement(bytes_start.as_quick_xml()))?; + + serializer.pop_namespace_scope(); + + Ok(()) + } +} + +pub struct ChildrenSerializeSeq<'s, W: Write> { + bytes_start: Option, + serializer: &'s mut Serializer, + end_name: QName<'static>, +} + +impl ser::SerializeSeq for ChildrenSerializeSeq<'_, W> { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, value: &V) -> Result { + value.serialize(SerializerWithPossibleBytesStart { + serializer: self.serializer, + possible_bytes_start: Some(&mut self.bytes_start), + }) + } + + fn end(self) -> Result { + // If we have a bytes_start, then we never wrote the start event, so we need to write an empty element instead. + if let Some(bytes_start) = self.bytes_start { + self.serializer + .writer + .write(XmlEvent::Empty(bytes_start.as_quick_xml())) + .map_err(Error::Io)?; + } else { + let end_name = OwnedQuickName::new(&self.end_name); + + let bytes_end = BytesEnd::from(end_name.as_ref()); + + self.serializer + .writer + .write(XmlEvent::End(bytes_end)) + .map_err(Error::Io)?; + } + + self.serializer.pop_namespace_scope(); + + Ok(()) + } +} + +pub struct SerializeSeq<'e, 'b, W: Write> { + serializer: &'e mut Serializer, + possible_bytes_start: Option<&'b mut Option>, +} + +impl ser::SerializeSeq for SerializeSeq<'_, '_, W> { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, v: &V) -> Result { + v.serialize(SerializerWithPossibleBytesStart { + serializer: self.serializer, + possible_bytes_start: self.possible_bytes_start.as_deref_mut(), + }) + } + + fn end(self) -> Result { + Ok(()) + } +} + +impl<'s, W: Write> xmlity::Serializer for &'s mut Serializer { + type Ok = (); + type Error = Error; + type SerializeElement = SerializeElement<'s, W>; + type SerializeSeq = SerializeSeq<'s, 'static, W>; + + fn serialize_cdata>(self, text: S) -> Result { + self.writer + .write(XmlEvent::CData(BytesCData::new(text.as_ref()))) + .map_err(Error::Io) + } + + fn serialize_text>(self, text: S) -> Result { + self.writer + .write(XmlEvent::Text(BytesText::from_escaped(text.as_ref()))) + .map_err(Error::Io) + } + + fn serialize_element<'a>( + self, + name: &'a ExpandedName<'a>, + ) -> Result { + Ok(SerializeElement { + serializer: self, + name: name.clone().into_owned(), + attributes: Vec::new(), + preferred_prefix: None, + enforce_prefix: IncludePrefix::default(), + }) + } + + fn serialize_seq(self) -> Result { + Ok(SerializeSeq { + serializer: self, + possible_bytes_start: None, + }) + } + + fn serialize_decl>( + self, + version: S, + encoding: Option, + standalone: Option, + ) -> Result { + self.writer + .write(XmlEvent::Decl(BytesDecl::new( + version.as_ref(), + encoding.as_ref().map(|s| s.as_ref()), + standalone.as_ref().map(|s| s.as_ref()), + ))) + .map_err(Error::Io) + } + + fn serialize_pi>(self, text: S) -> Result { + self.writer + .write(XmlEvent::ProcessingInstruction(BytesPI::new( + str::from_utf8(text.as_ref()).unwrap(), + ))) + .map_err(Error::Io) + } + + fn serialize_comment>(self, text: S) -> Result { + self.writer + .write(XmlEvent::Comment(str::from_utf8(text.as_ref()).unwrap())) + .map_err(Error::XmlRs) + } + + fn serialize_doctype>(self, text: S) -> Result { + todo!() + } + + fn serialize_none(self) -> Result { + Ok(()) + } +} + +pub struct SerializerWithPossibleBytesStart<'a, 'b, W: Write> { + serializer: &'a mut Serializer, + possible_bytes_start: Option<&'b mut Option>, +} + +impl SerializerWithPossibleBytesStart<'_, '_, W> { + pub fn try_start(&mut self) -> Result<(), Error> { + if let Some(bytes_start) = self.possible_bytes_start.take().and_then(Option::take) { + self.serializer + .writer + .write(XmlEvent::Start(bytes_start.as_quick_xml())) + .map_err(Error::Io)?; + } + Ok(()) + } +} + +impl<'s, 'b, W: Write> xmlity::Serializer for SerializerWithPossibleBytesStart<'s, 'b, W> { + type Ok = (); + type Error = Error; + type SerializeElement = SerializeElement<'s, W>; + type SerializeSeq = SerializeSeq<'s, 'b, W>; + + fn serialize_cdata>(mut self, text: S) -> Result { + self.try_start()?; + self.serializer.serialize_cdata(text) + } + + fn serialize_text>(mut self, text: S) -> Result { + self.try_start()?; + self.serializer.serialize_text(text) + } + + fn serialize_element<'a>( + mut self, + name: &'a ExpandedName<'a>, + ) -> Result { + self.try_start()?; + self.serializer.serialize_element(name) + } + + fn serialize_seq(self) -> Result { + Ok(SerializeSeq { + serializer: self.serializer, + possible_bytes_start: self.possible_bytes_start, + }) + } + + fn serialize_decl>( + mut self, + version: S, + encoding: Option, + standalone: Option, + ) -> Result { + self.try_start()?; + self.serializer + .serialize_decl(version, encoding, standalone) + } + + fn serialize_pi>(mut self, text: S) -> Result { + self.try_start()?; + self.serializer.serialize_pi(text) + } + + fn serialize_comment>(mut self, text: S) -> Result { + self.try_start()?; + self.serializer.serialize_comment(text) + } + + fn serialize_doctype>(mut self, text: S) -> Result { + self.try_start()?; + self.serializer.serialize_doctype(text) + } + + fn serialize_none(self) -> Result { + Ok(()) + } +} diff --git a/xmlity-xml-rs/tests/XMLSchema.xsd b/xmlity-xml-rs/tests/XMLSchema.xsd new file mode 100644 index 0000000..2e9a272 --- /dev/null +++ b/xmlity-xml-rs/tests/XMLSchema.xsd @@ -0,0 +1,2534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]> + + + + Part 1 version: Id: structures.xsd,v 1.2 2004/01/15 11:34:25 ht Exp + Part 2 version: Id: datatypes.xsd,v 1.3 2004/01/23 18:11:13 ht Exp + + + + + + The schema corresponding to this document is normative, + with respect to the syntactic constraints it expresses in the + XML Schema language. The documentation (within <documentation> elements) + below, is not normative, but rather highlights important aspects of + the W3C Recommendation of which this is a part + + + + + The simpleType element and all of its members are defined + towards the end of this schema document + + + + + + Get access to the xml: attribute groups for xml:lang + as declared on 'schema' and 'documentation' below + + + + + + + + This type is extended by almost all schema types + to allow attributes from other namespaces to be + added to user schemas. + + + + + + + + + + + + + This type is extended by all types which allow annotation + other than <schema> itself + + + + + + + + + + + + + + + + This group is for the + elements which occur freely at the top level of schemas. + All of their types are based on the "annotated" type by extension. + + + + + + + + + + + + + This group is for the + elements which can self-redefine (see <redefine> below). + + + + + + + + + + + + + A utility type, not for public use + + + + + + + + + + + A utility type, not for public use + + + + + + + + + + + A utility type, not for public use + + #all or (possibly empty) subset of {extension, restriction} + + + + + + + + + + + + + + + + + A utility type, not for public use + + + + + + + + + + + + + A utility type, not for public use + + #all or (possibly empty) subset of {extension, restriction, list, union} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + for maxOccurs + + + + + + + + + + + + for all particles + + + + + + + for element, group and attributeGroup, + which both define and reference + + + + + + + + 'complexType' uses this + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This branch is short for + <complexContent> + <restriction base="xs:anyType"> + ... + </restriction> + </complexContent> + + + + + + + + + + + + + + + Will be restricted to required or forbidden + + + + + + Not allowed if simpleContent child is chosen. + May be overriden by setting on complexContent child. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This choice is added simply to + make this a valid restriction per the REC + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Overrides any setting on complexType parent. + + + + + + + + + + + + + + + This choice is added simply to + make this a valid restriction per the REC + + + + + + + + + + + + + + + + + No typeDefParticle group reference + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A utility type, not for public use + + #all or (possibly empty) subset of {substitution, extension, + restriction} + + + + + + + + + + + + + + + + + + + + + + + + + The element element can be used either + at the top level to define an element-type binding globally, + or within a content model to either reference a globally-defined + element or type or declare an element-type binding locally. + The ref form is not allowed at the top level. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + group type for explicit groups, named top-level groups and + group references + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + group type for the three kinds of group + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This choice with min/max is here to + avoid a pblm with the Elt:All/Choice/Seq + Particle derivation constraint + + + + + + + + + + restricted max/min + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Only elements allowed inside + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + simple type for the value of the 'namespace' attr of + 'any' and 'anyAttribute' + + + + Value is + ##any - - any non-conflicting WFXML/attribute at all + + ##other - - any non-conflicting WFXML/attribute from + namespace other than targetNS + + ##local - - any unqualified non-conflicting WFXML/attribute + + one or - - any non-conflicting WFXML/attribute from + more URI the listed namespaces + references + (space separated) + + ##targetNamespace or ##local may appear in the above list, to + refer to the targetNamespace of the enclosing + schema or an absent targetNamespace respectively + + + + + + A utility type, not for public use + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A subset of XPath expressions for use +in selectors + A utility type, not for public +use + + + + The following pattern is intended to allow XPath + expressions per the following EBNF: + Selector ::= Path ( '|' Path )* + Path ::= ('.//')? Step ( '/' Step )* + Step ::= '.' | NameTest + NameTest ::= QName | '*' | NCName ':' '*' + child:: is also allowed + + + + + + + + + + + + + + + + + + + + + + + A subset of XPath expressions for use +in fields + A utility type, not for public +use + + + + The following pattern is intended to allow XPath + expressions per the same EBNF as for selector, + with the following change: + Path ::= ('.//')? ( Step '/' )* ( Step | '@' NameTest ) + + + + + + + + + + + + + + + + + + + + + + + + + + + The three kinds of identity constraints, all with + type of or derived from 'keybase'. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A utility type, not for public use + + A public identifier, per ISO 8879 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + notations for use within XML Schema schemas + + + + + + + + + Not the real urType, but as close an approximation as we can + get in the XML representation + + + + + + + + + + First the built-in primitive datatypes. These definitions are for + information only, the real built-in definitions are magic. + + + + For each built-in datatype in this schema (both primitive and + derived) can be uniquely addressed via a URI constructed + as follows: + 1) the base URI is the URI of the XML Schema namespace + 2) the fragment identifier is the name of the datatype + + For example, to address the int datatype, the URI is: + + http://www.w3.org/2001/XMLSchema#int + + Additionally, each facet definition element can be uniquely + addressed via a URI constructed as follows: + 1) the base URI is the URI of the XML Schema namespace + 2) the fragment identifier is the name of the facet + + For example, to address the maxInclusive facet, the URI is: + + http://www.w3.org/2001/XMLSchema#maxInclusive + + Additionally, each facet usage in a built-in datatype definition + can be uniquely addressed via a URI constructed as follows: + 1) the base URI is the URI of the XML Schema namespace + 2) the fragment identifier is the name of the datatype, followed + by a period (".") followed by the name of the facet + + For example, to address the usage of the maxInclusive facet in + the definition of int, the URI is: + + http://www.w3.org/2001/XMLSchema#int.maxInclusive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NOTATION cannot be used directly in a schema; rather a type + must be derived from it by specifying at least one enumeration + facet whose value is the name of a NOTATION declared in the + schema. + + + + + + + + + + Now the derived primitive types + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pattern specifies the content of section 2.12 of XML 1.0e2 + and RFC 3066 (Revised version of RFC 1766). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pattern matches production 7 from the XML spec + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pattern matches production 5 from the XML spec + + + + + + + + + + + + + + + pattern matches production 4 from the Namespaces in XML spec + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A utility type, not for public use + + + + + + + + + + + + + + + + + + + + + + #all or (possibly empty) subset of {restriction, union, list} + + + A utility type, not for public use + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Can be restricted to required or forbidden + + + + + + + + + + + + + + + + + + Required at the top level + + + + + + + + + + + + + + + + + + + Forbidden when nested + + + + + + + + + + + + + + + + + + + We should use a substitution group for facets, but + that's ruled out because it would allow users to + add their own, which we're not ready for yet. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + base attribute and simpleType child are mutually + exclusive, but one or other is required + + + + + + + + + + + + + + + + itemType attribute and simpleType child are mutually + exclusive, but one or other is required + + + + + + + + + + + + + + + + + + memberTypes attribute must be non-empty or there must be + at least one simpleType child + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xmlity-xml-rs/tests/elements/attribute.rs b/xmlity-xml-rs/tests/elements/attribute.rs new file mode 100644 index 0000000..02503d1 --- /dev/null +++ b/xmlity-xml-rs/tests/elements/attribute.rs @@ -0,0 +1,79 @@ +use crate::define_test; + +use xmlity::{Deserialize, Serialize, SerializeAttribute}; + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "b")] +pub struct B(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "C")] +pub struct C { + #[xattribute(deferred = true)] + pub b: B, +} + +define_test!( + one_attribute, + [( + C { + b: B("A".to_string()) + }, + r#""# + )] +); + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "D")] +pub struct D(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "E")] +pub struct E { + #[xattribute(deferred = true)] + pub b: B, + #[xattribute(deferred = true)] + pub d: D, +} + +define_test!( + two_attributes, + [( + E { + b: B("A".to_string()), + d: D("B".to_string()) + }, + r#""# + )] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "F")] +pub struct F { + #[xattribute(deferred = true)] + pub b: B, + pub c: Vec, + pub e: E, +} + +define_test!( + element_with_children_and_attributes, + [( + F { + b: B("A".to_string()), + c: vec![ + C { + b: B("B".to_string()) + }, + C { + b: B("B".to_string()) + } + ], + e: E { + b: B("A".to_string()), + d: D("B".to_string()) + } + }, + r#""# + )] +); diff --git a/xmlity-xml-rs/tests/elements/basic.rs b/xmlity-xml-rs/tests/elements/basic.rs new file mode 100644 index 0000000..6b780d9 --- /dev/null +++ b/xmlity-xml-rs/tests/elements/basic.rs @@ -0,0 +1,83 @@ +use crate::define_test; + +use xmlity::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "b")] +pub struct B(String); + +define_test!(element_with_text, [(B("A".to_string()), "A")]); + +#[rstest::rstest] +#[case("")] +#[case("")] +fn wrong_deserialize(#[case] xml: &str) { + let actual: Result = crate::utils::quick_xml_deserialize_test(xml); + + assert!(actual.is_err()); +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "c")] +pub struct C { + pub c: B, +} + +define_test!( + element_with_single_child, + [( + C { + c: B("A".to_string()) + }, + "A" + )] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "d")] +pub struct D { + pub b: B, + pub c: C, +} + +define_test!( + element_with_multiple_children, + [( + D { + b: B("A".to_string()), + c: C { + c: B("B".to_string()) + } + }, + "AB" + )] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "e")] +pub struct E { + pub d: Vec, +} + +define_test!( + element_with_vector_of_children, + [( + E { + d: vec![ + D { + b: B("A".to_string()), + c: C { + c: B("B".to_string()) + } + }, + D { + b: B("C".to_string()), + c: C { + c: B("D".to_string()) + } + } + ] + }, + r#"ABCD"# + )] +); diff --git a/xmlity-xml-rs/tests/elements/default.rs b/xmlity-xml-rs/tests/elements/default.rs new file mode 100644 index 0000000..dd00f9e --- /dev/null +++ b/xmlity-xml-rs/tests/elements/default.rs @@ -0,0 +1,129 @@ +use crate::define_test; + +use xmlity::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize, Default)] +#[xelement(name = "b")] +pub struct B(#[xvalue(default)] String); + +define_test!( + element_with_text_default_option, + [ + (B("A".to_string()), "A"), + (B("".to_string()), ""), + (B("".to_string()), "", "") + ] +); + +#[rstest::rstest] +#[case("")] +fn wrong_deserialize(#[case] xml: &str) { + let actual: Result = crate::utils::quick_xml_deserialize_test(xml); + + assert!(actual.is_err()); +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Default)] +#[xelement(name = "c")] +pub struct C { + #[xvalue(default)] + pub b: B, +} + +define_test!( + element_with_single_child, + [ + ( + C { + b: B("A".to_string()) + }, + "A" + ), + ( + C { + b: B("".to_string()) + }, + "", + "" + ), + ( + C { + b: B("".to_string()) + }, + "" + ) + ] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "d")] +pub struct D { + pub b: B, + #[xvalue(default)] + pub c: C, +} + +define_test!( + element_with_multiple_children, + [( + D { + b: B("A".to_string()), + c: C { + b: B("B".to_string()) + } + }, + "AB" + )] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "e")] +pub struct E { + #[xvalue(default)] + pub d: Vec, +} + +define_test!( + element_with_vector_of_children, + [ + ( + E { + d: vec![ + D { + b: B("A".to_string()), + c: C { + b: B("B".to_string()) + } + }, + D { + b: B("C".to_string()), + c: C { + b: B("D".to_string()) + } + } + ] + }, + r#"ABCD"# + ), + ( + E { + d: vec![ + D { + b: B("A".to_string()), + c: C { + b: B("B".to_string()) + } + }, + D { + b: B("C".to_string()), + c: C { + b: B("D".to_string()) + } + } + ] + }, + r#"ABCD"# + ), + (E { d: vec![] }, "") + ] +); diff --git a/xmlity-xml-rs/tests/elements/extendable.rs b/xmlity-xml-rs/tests/elements/extendable.rs new file mode 100644 index 0000000..800aa2a --- /dev/null +++ b/xmlity-xml-rs/tests/elements/extendable.rs @@ -0,0 +1,65 @@ +use crate::define_test; + +use xmlity::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "a")] +pub struct A(#[xvalue(extendable = true)] String); + +impl Extend for A { + fn extend>(&mut self, iter: T) { + self.0.extend(iter.into_iter().map(|a| a.0)); + } +} + +fn extendable_struct() -> ExtendableA { + ExtendableA(A("AsdrebootMoreText".to_string())) +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct ExtendableA(#[xvalue(extendable = true)] A); + +define_test!( + extendable_struct, + [ + ( + extendable_struct(), + "AsdrebootMoreText", + "AsdrebootText" + ), + (extendable_struct(), "AsdrebootMoreText"), + ( + extendable_struct(), + "AsdrebootMoreText", + "AsdrebootMoreText" + ) + ] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "b")] +pub struct B(#[xvalue(extendable = "iterator")] Vec); + +fn extendable_vec1() -> B { + B(vec![ + "Asdreboot".to_string(), + "More".to_string(), + "Text".to_string(), + ]) +} + +fn extendable_vec2() -> B { + B(vec!["Asd".to_string()]) +} + +define_test!( + extendable_vec, + [ + ( + extendable_vec1(), + "AsdrebootMoreText", + "AsdrebootText" + ), + (extendable_vec2(), "Asd") + ] +); diff --git a/xmlity-xml-rs/tests/elements/generics.rs b/xmlity-xml-rs/tests/elements/generics.rs new file mode 100644 index 0000000..a551f57 --- /dev/null +++ b/xmlity-xml-rs/tests/elements/generics.rs @@ -0,0 +1,97 @@ +use crate::define_test; + +use xmlity::{ + DeserializationGroup, Deserialize, DeserializeOwned, SerializationGroup, Serialize, + SerializeAttribute, +}; + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "a")] +pub struct A(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "b")] +pub struct B { + #[xattribute(deferred = true)] + pub a: T, +} + +define_test!( + generic_element, + [( + B { + a: A("A".to_string()), + }, + r#""# + )] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum C { + B(B), +} + +define_test!( + generic_enum, + [( + C::B(B { + a: A("A".to_string()), + }), + r#""# + )] +); + +#[derive(Debug, PartialEq, SerializationGroup, DeserializationGroup)] +pub struct D { + #[xattribute(deferred = true)] + pub c: T, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "d")] +pub struct E { + #[xgroup] + pub c: D, +} + +define_test!( + generic_group, + [( + E { + c: D { + c: A("A".to_string()), + }, + }, + r#""# + )] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum F { + T(T), + U(U), +} + +define_test!( + two_armed_generic_enum, + [ + (F::::T("A".to_string()), r#"A"#), + (F::::U(0.5), r#"0.5"#) + ] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum G { + #[xelement(name = "t")] + T(T), + #[xelement(name = "u")] + U(U), +} + +define_test!( + two_armed_element_generic_enum, + [ + (G::::T("A".to_string()), r#"A"#), + (G::::U(0.5), r#"0.5"#) + ] +); diff --git a/xmlity-xml-rs/tests/elements/inline_attribute_declarations.rs b/xmlity-xml-rs/tests/elements/inline_attribute_declarations.rs new file mode 100644 index 0000000..48abec9 --- /dev/null +++ b/xmlity-xml-rs/tests/elements/inline_attribute_declarations.rs @@ -0,0 +1,59 @@ +use crate::define_test; + +use xmlity::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "c")] +pub struct C { + #[xattribute(name = "b")] + pub c: String, +} + +define_test!( + element_with_single_child, + [(C { c: "A".to_string() }, r#""#)] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "d")] +pub struct D { + #[xattribute(name = "b")] + pub b: String, + pub c: C, +} + +define_test!( + element_with_multiple_children, + [( + D { + b: "A".to_string(), + c: C { c: "B".to_string() } + }, + r#""# + )] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "e")] +pub struct E { + pub d: Vec, +} + +define_test!( + element_with_vector_of_children, + [( + E { + d: vec![ + D { + b: "A".to_string(), + c: C { c: "B".to_string() } + }, + D { + b: "C".to_string(), + c: C { c: "D".to_string() } + } + ] + }, + r#""# + )] +); diff --git a/xmlity-xml-rs/tests/elements/inline_declarations.rs b/xmlity-xml-rs/tests/elements/inline_declarations.rs new file mode 100644 index 0000000..dba52d8 --- /dev/null +++ b/xmlity-xml-rs/tests/elements/inline_declarations.rs @@ -0,0 +1,171 @@ +use crate::define_test; + +use xmlity::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "c")] +pub struct C { + #[xelement(name = "b")] + pub c: String, +} + +define_test!( + element_with_single_child, + [(C { c: "A".to_string() }, "A")] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "d")] +pub struct D { + #[xelement(name = "b")] + pub b: String, + pub c: C, +} + +define_test!( + element_with_multiple_children, + [ + ( + D { + b: "A".to_string(), + c: C { c: "B".to_string() } + }, + "AB" + ), + ( + vec![ + D { + b: "A".to_string(), + c: C { c: "B".to_string() } + }, + D { + b: "C".to_string(), + c: C { c: "D".to_string() } + } + ], + "ABCD" + ) + ] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "e")] +pub struct E { + pub d: Vec, +} + +define_test!( + element_with_vector_of_children, + [( + E { + d: vec![ + D { + b: "A".to_string(), + c: C { c: "B".to_string() } + }, + D { + b: "C".to_string(), + c: C { c: "D".to_string() } + } + ] + }, + r#"ABCD"# + )] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct F { + #[xvalue(extendable = "iterator")] + pub g: Vec, +} + +define_test!( + element_with_extendable, + [( + F { + g: vec![ + D { + b: "A".to_string(), + c: C { c: "B".to_string() } + }, + D { + b: "C".to_string(), + c: C { c: "D".to_string() } + } + ] + }, + r#"ABCD"# + )] +); + +#[rstest::rstest] +#[case("")] +#[case(r#"ABCD"#)] +fn element_with_extendable_wrong_deserialize(#[case] xml: &str) { + let actual: Result = crate::utils::quick_xml_deserialize_test(xml); + + assert!(actual.is_err()); +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct H { + #[xelement(name = "g")] + pub g: Vec, +} + +define_test!( + multiple_elements, + [( + H { + g: vec![ + D { + b: "A".to_string(), + c: C { c: "B".to_string() } + }, + D { + b: "C".to_string(), + c: C { c: "D".to_string() } + } + ] + }, + r#"ABCD"# + )] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum I { + #[xelement(name = "g")] + G(D), +} + +define_test!( + enum_with_inline_element, + [( + I::G(D { + b: "A".to_string(), + c: C { c: "B".to_string() } + }), + r#"AB"# + )] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum J { + #[xelement(name = "g")] + G(D), + U(String), +} + +define_test!( + enum_with_inline_element_or_text, + [ + ( + I::G(D { + b: "A".to_string(), + c: C { c: "B".to_string() } + }), + r#"AB"# + ), + (J::U("A".to_string()), r#"A"#) + ] +); diff --git a/xmlity-xml-rs/tests/elements/mixed.rs b/xmlity-xml-rs/tests/elements/mixed.rs new file mode 100644 index 0000000..fb35f5f --- /dev/null +++ b/xmlity-xml-rs/tests/elements/mixed.rs @@ -0,0 +1,82 @@ +use crate::define_test; + +use xmlity::types::utils::CData; +use xmlity::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "elem")] +pub struct Elem(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "mixed")] +pub struct Mixed { + pub text1: String, + pub elem: Elem, + pub text2: String, +} + +fn simple_mixed_struct() -> Mixed { + Mixed { + text1: "Text".to_string(), + elem: Elem("Content".to_string()), + text2: "Text2".to_string(), + } +} + +define_test!( + mixed_text_and_element, + [( + simple_mixed_struct(), + "TextContentText2" + )] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "mixed")] +pub struct MixedCData { + pub text1: String, + pub cdata: CData, + pub elem: Elem, + pub text2: String, +} + +fn mixed_cdata_struct() -> MixedCData { + MixedCData { + text1: "Text".to_string(), + cdata: CData("More".to_string()), + elem: Elem("Content".to_string()), + text2: "Text2".to_string(), + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "mixed")] +pub struct MixedCDataConcat { + pub text1: String, + pub text2: CData, + pub elem: Elem, + pub text4: String, +} + +fn mixed_cdata_concat_struct() -> MixedCDataConcat { + MixedCDataConcat { + text1: "Text1".to_string(), + text2: CData("Text2".to_string()), + elem: Elem("Text3".to_string()), + text4: "Text4".to_string(), + } +} + +define_test!( + mixed_text_cdata_and_element, + [ + ( + mixed_cdata_struct(), + "TextContentText2" + ), + ( + mixed_cdata_concat_struct(), + "Text1Text3Text4" + ) + ] +); diff --git a/xmlity-xml-rs/tests/elements/mod.rs b/xmlity-xml-rs/tests/elements/mod.rs new file mode 100644 index 0000000..c0813ec --- /dev/null +++ b/xmlity-xml-rs/tests/elements/mod.rs @@ -0,0 +1,11 @@ +pub mod attribute; +pub mod basic; +pub mod default; +pub mod extendable; +pub mod generics; +pub mod inline_attribute_declarations; +pub mod inline_declarations; +pub mod mixed; +pub mod namespace_expr; +pub mod single_namespace; +pub mod strict_order; diff --git a/xmlity-xml-rs/tests/elements/namespace_expr.rs b/xmlity-xml-rs/tests/elements/namespace_expr.rs new file mode 100644 index 0000000..9f8854e --- /dev/null +++ b/xmlity-xml-rs/tests/elements/namespace_expr.rs @@ -0,0 +1,317 @@ +use std::str::FromStr; + +use pretty_assertions::assert_eq; + +use crate::utils::{ + clean_string, quick_xml_deserialize_test, quick_xml_serialize_test_with_default, +}; +use rstest::rstest; +use xmlity::{types::string::Trim, ExpandedName, XmlNamespace}; +use xmlity::{Deserialize, Serialize}; + +const NAMESPACE: XmlNamespace = + XmlNamespace::new_dangerous("http://my.namespace.example.com/this/is/a/namespace"); + +const SIMPLE_DEFAULT_NS_1D_STRUCT_TEST_XML: &str = r###" + Tove +"###; + +const SIMPLE_DEFAULT_WRONG_NS_1D_STRUCT_TEST_XML: &str = r###" + Tove +"###; + +const SIMPLE_NS_1D_STRUCT_TEST_XML: &str = r###" + Tove +"###; + +const SIMPLE_WRONG_NS_1D_STRUCT_TEST_XML: &str = r###" + Tove +"###; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "to", + namespace_expr = NAMESPACE +)] +pub struct To(String); + +fn simple_ns_1d_struct() -> To { + To("Tove".to_string()) +} + +#[rstest] +#[case::default_ns( + SIMPLE_DEFAULT_NS_1D_STRUCT_TEST_XML, + Some("http://my.namespace.example.com/this/is/a/namespace") +)] +#[case::ns(SIMPLE_NS_1D_STRUCT_TEST_XML, None)] +fn simple_ns_1d_struct_serialize( + #[case] test_xml: &str, + #[case] default_namespace: Option<&'static str>, +) { + let actual = quick_xml_serialize_test_with_default( + simple_ns_1d_struct(), + default_namespace.map(XmlNamespace::new).map(Result::unwrap), + ) + .unwrap(); + + assert_eq!(actual, clean_string(test_xml)); +} + +#[rstest] +#[case::default_ns(SIMPLE_DEFAULT_NS_1D_STRUCT_TEST_XML)] +#[case::ns(SIMPLE_NS_1D_STRUCT_TEST_XML)] +fn simple_ns_1d_struct_deserialize(#[case] test_xml: &str) { + let actual: To = quick_xml_deserialize_test(&clean_string(test_xml)).unwrap(); + + let expected = simple_ns_1d_struct(); + + assert_eq!(actual, expected); +} + +#[rstest] +#[case::default_ns(SIMPLE_DEFAULT_WRONG_NS_1D_STRUCT_TEST_XML)] +#[case::ns(SIMPLE_WRONG_NS_1D_STRUCT_TEST_XML)] +fn simple_ns_1d_struct_wrong_ns_deserialize(#[case] test_xml: &str) { + let err = quick_xml_deserialize_test::(&clean_string(test_xml)) + .expect_err("deserialization should fail"); + + let xmlity_quick_xml::Error::WrongName { actual, expected } = err else { + panic!("unexpected error: {err:?}"); + }; + + assert_eq!( + actual, + Box::new(ExpandedName::new( + "to".parse().unwrap(), + Some( + XmlNamespace::from_str("http://not.my.namespace.example.org/this/should/not/match") + .expect("Valid namespace") + ) + )) + ); + + assert_eq!( + expected, + Box::new(ExpandedName::new( + "to".parse().unwrap(), + Some( + XmlNamespace::from_str("http://my.namespace.example.com/this/is/a/namespace") + .expect("Valid namespace") + ) + )) + ); +} + +const SIMPLE_3D_DEFAULT_NS_LIST_TEST_XML: &str = r###" + + + Belgian Waffles + $5.95 + + Two of our famous Belgian Waffles with plenty of real maple syrup + + 650 + + + Strawberry Belgian Waffles + $7.95 + + Light Belgian waffles covered with strawberries and whipped cream + + 900 + + + Berry-Berry Belgian Waffles + $8.95 + + Belgian waffles covered with assorted fresh berries and whipped cream + + 900 + + + French Toast + $4.50 + + Thick slices made from our homemade sourdough bread + + 600 + + + Homestyle Breakfast + $6.95 + + Two eggs, bacon or sausage, toast, and our ever-popular hash browns + + 950 + + +"###; + +const SIMPLE_3D_NS_LIST_TEST_XML: &str = r###" + + + Belgian Waffles + $5.95 + + Two of our famous Belgian Waffles with plenty of real maple syrup + + 650 + + + Strawberry Belgian Waffles + $7.95 + + Light Belgian waffles covered with strawberries and whipped cream + + 900 + + + Berry-Berry Belgian Waffles + $8.95 + + Belgian waffles covered with assorted fresh berries and whipped cream + + 900 + + + French Toast + $4.50 + + Thick slices made from our homemade sourdough bread + + 600 + + + Homestyle Breakfast + $6.95 + + Two eggs, bacon or sausage, toast, and our ever-popular hash browns + + 950 + + +"###; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "name", + namespace_expr = NAMESPACE +)] +pub struct Name(pub String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "price", + namespace_expr = NAMESPACE +)] +pub struct Price(pub String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "description", + namespace_expr = NAMESPACE +)] +pub struct Description(pub Trim); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "calories", + namespace_expr = NAMESPACE +)] +pub struct Calories(pub u16); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "food", + namespace_expr = NAMESPACE +)] +struct Food { + name: Name, + price: Price, + description: Description, + calories: Calories, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "breakfast_menu", + namespace_expr = NAMESPACE +)] +struct BreakfastMenu { + food: Vec, +} + +fn simple_3d_list_test_value() -> BreakfastMenu { + BreakfastMenu { + food: vec![ + Food { + name: Name("Belgian Waffles".to_string()), + price: Price("$5.95".to_string()), + description: Description(Trim( + "Two of our famous Belgian Waffles with plenty of real maple syrup".to_string(), + )), + calories: Calories(650), + }, + Food { + name: Name("Strawberry Belgian Waffles".to_string()), + price: Price("$7.95".to_string()), + description: Description(Trim( + "Light Belgian waffles covered with strawberries and whipped cream".to_string(), + )), + calories: Calories(900), + }, + Food { + name: Name("Berry-Berry Belgian Waffles".to_string()), + price: Price("$8.95".to_string()), + description: Description(Trim( + "Belgian waffles covered with assorted fresh berries and whipped cream" + .to_string(), + )), + calories: Calories(900), + }, + Food { + name: Name("French Toast".to_string()), + price: Price("$4.50".to_string()), + description: Description(Trim( + "Thick slices made from our homemade sourdough bread".to_string(), + )), + calories: Calories(600), + }, + Food { + name: Name("Homestyle Breakfast".to_string()), + price: Price("$6.95".to_string()), + description: Description(Trim( + "Two eggs, bacon or sausage, toast, and our ever-popular hash browns" + .to_string(), + )), + calories: Calories(950), + }, + ], + } +} + +#[rstest] +#[case::default_ns( + SIMPLE_3D_DEFAULT_NS_LIST_TEST_XML, + Some("http://my.namespace.example.com/this/is/a/namespace") +)] +#[case::no_default_ns(SIMPLE_3D_NS_LIST_TEST_XML, None)] +fn simple_3d_struct_serialize(#[case] xml: &str, #[case] default_ns: Option<&'static str>) { + let actual = quick_xml_serialize_test_with_default( + simple_3d_list_test_value(), + default_ns.map(XmlNamespace::new).map(Result::unwrap), + ) + .unwrap(); + let expected = clean_string(xml); + assert_eq!(actual, expected); +} + +#[rstest] +#[case::default_ns(SIMPLE_3D_DEFAULT_NS_LIST_TEST_XML)] +#[case::no_default_ns(SIMPLE_3D_NS_LIST_TEST_XML)] +fn simple_3d_struct_deserialize(#[case] xml: &str) { + let actual: BreakfastMenu = quick_xml_deserialize_test(xml).unwrap(); + let expected = simple_3d_list_test_value(); + assert_eq!(actual, expected); +} diff --git a/xmlity-xml-rs/tests/elements/single_namespace.rs b/xmlity-xml-rs/tests/elements/single_namespace.rs new file mode 100644 index 0000000..8cdae34 --- /dev/null +++ b/xmlity-xml-rs/tests/elements/single_namespace.rs @@ -0,0 +1,314 @@ +use std::str::FromStr; + +use pretty_assertions::assert_eq; + +use crate::utils::{ + clean_string, quick_xml_deserialize_test, quick_xml_serialize_test_with_default, +}; +use rstest::rstest; +use xmlity::{types::string::Trim, ExpandedName, XmlNamespace}; +use xmlity::{Deserialize, Serialize}; + +const SIMPLE_DEFAULT_NS_1D_STRUCT_TEST_XML: &str = r###" + Tove +"###; + +const SIMPLE_DEFAULT_WRONG_NS_1D_STRUCT_TEST_XML: &str = r###" + Tove +"###; + +const SIMPLE_NS_1D_STRUCT_TEST_XML: &str = r###" + Tove +"###; + +const SIMPLE_WRONG_NS_1D_STRUCT_TEST_XML: &str = r###" + Tove +"###; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "to", + namespace = "http://my.namespace.example.com/this/is/a/namespace" +)] +pub struct To(String); + +fn simple_ns_1d_struct() -> To { + To("Tove".to_string()) +} + +#[rstest] +#[case::default_ns( + SIMPLE_DEFAULT_NS_1D_STRUCT_TEST_XML, + Some("http://my.namespace.example.com/this/is/a/namespace") +)] +#[case::ns(SIMPLE_NS_1D_STRUCT_TEST_XML, None)] +fn simple_ns_1d_struct_serialize( + #[case] test_xml: &str, + #[case] default_namespace: Option<&'static str>, +) { + let actual = quick_xml_serialize_test_with_default( + simple_ns_1d_struct(), + default_namespace.map(XmlNamespace::new).map(Result::unwrap), + ) + .unwrap(); + + assert_eq!(actual, clean_string(test_xml)); +} + +#[rstest] +#[case::default_ns(SIMPLE_DEFAULT_NS_1D_STRUCT_TEST_XML)] +#[case::ns(SIMPLE_NS_1D_STRUCT_TEST_XML)] +fn simple_ns_1d_struct_deserialize(#[case] test_xml: &str) { + let actual: To = quick_xml_deserialize_test(&clean_string(test_xml)).unwrap(); + + let expected = simple_ns_1d_struct(); + + assert_eq!(actual, expected); +} + +#[rstest] +#[case::default_ns(SIMPLE_DEFAULT_WRONG_NS_1D_STRUCT_TEST_XML)] +#[case::ns(SIMPLE_WRONG_NS_1D_STRUCT_TEST_XML)] +fn simple_ns_1d_struct_wrong_ns_deserialize(#[case] test_xml: &str) { + let err = quick_xml_deserialize_test::(&clean_string(test_xml)) + .expect_err("deserialization should fail"); + + let xmlity_quick_xml::Error::WrongName { actual, expected } = err else { + panic!("unexpected error: {err:?}"); + }; + + assert_eq!( + actual, + Box::new(ExpandedName::new( + "to".parse().unwrap(), + Some( + XmlNamespace::from_str("http://not.my.namespace.example.org/this/should/not/match") + .expect("Valid namespace") + ) + )) + ); + + assert_eq!( + expected, + Box::new(ExpandedName::new( + "to".parse().unwrap(), + Some( + XmlNamespace::from_str("http://my.namespace.example.com/this/is/a/namespace") + .expect("Valid namespace") + ) + )) + ); +} + +const SIMPLE_3D_DEFAULT_NS_LIST_TEST_XML: &str = r###" + + + Belgian Waffles + $5.95 + + Two of our famous Belgian Waffles with plenty of real maple syrup + + 650 + + + Strawberry Belgian Waffles + $7.95 + + Light Belgian waffles covered with strawberries and whipped cream + + 900 + + + Berry-Berry Belgian Waffles + $8.95 + + Belgian waffles covered with assorted fresh berries and whipped cream + + 900 + + + French Toast + $4.50 + + Thick slices made from our homemade sourdough bread + + 600 + + + Homestyle Breakfast + $6.95 + + Two eggs, bacon or sausage, toast, and our ever-popular hash browns + + 950 + + +"###; + +const SIMPLE_3D_NS_LIST_TEST_XML: &str = r###" + + + Belgian Waffles + $5.95 + + Two of our famous Belgian Waffles with plenty of real maple syrup + + 650 + + + Strawberry Belgian Waffles + $7.95 + + Light Belgian waffles covered with strawberries and whipped cream + + 900 + + + Berry-Berry Belgian Waffles + $8.95 + + Belgian waffles covered with assorted fresh berries and whipped cream + + 900 + + + French Toast + $4.50 + + Thick slices made from our homemade sourdough bread + + 600 + + + Homestyle Breakfast + $6.95 + + Two eggs, bacon or sausage, toast, and our ever-popular hash browns + + 950 + + +"###; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "name", + namespace = "http://my.namespace.example.com/this/is/a/namespace" +)] +pub struct Name(pub String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "price", + namespace = "http://my.namespace.example.com/this/is/a/namespace" +)] +pub struct Price(pub String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "description", + namespace = "http://my.namespace.example.com/this/is/a/namespace" +)] +pub struct Description(pub Trim); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "calories", + namespace = "http://my.namespace.example.com/this/is/a/namespace" +)] +pub struct Calories(pub u16); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "food", + namespace = "http://my.namespace.example.com/this/is/a/namespace" +)] +struct Food { + name: Name, + price: Price, + description: Description, + calories: Calories, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement( + name = "breakfast_menu", + namespace = "http://my.namespace.example.com/this/is/a/namespace" +)] +struct BreakfastMenu { + food: Vec, +} + +fn simple_3d_list_test_value() -> BreakfastMenu { + BreakfastMenu { + food: vec![ + Food { + name: Name("Belgian Waffles".to_string()), + price: Price("$5.95".to_string()), + description: Description(Trim( + "Two of our famous Belgian Waffles with plenty of real maple syrup".to_string(), + )), + calories: Calories(650), + }, + Food { + name: Name("Strawberry Belgian Waffles".to_string()), + price: Price("$7.95".to_string()), + description: Description(Trim( + "Light Belgian waffles covered with strawberries and whipped cream".to_string(), + )), + calories: Calories(900), + }, + Food { + name: Name("Berry-Berry Belgian Waffles".to_string()), + price: Price("$8.95".to_string()), + description: Description(Trim( + "Belgian waffles covered with assorted fresh berries and whipped cream" + .to_string(), + )), + calories: Calories(900), + }, + Food { + name: Name("French Toast".to_string()), + price: Price("$4.50".to_string()), + description: Description(Trim( + "Thick slices made from our homemade sourdough bread".to_string(), + )), + calories: Calories(600), + }, + Food { + name: Name("Homestyle Breakfast".to_string()), + price: Price("$6.95".to_string()), + description: Description(Trim( + "Two eggs, bacon or sausage, toast, and our ever-popular hash browns" + .to_string(), + )), + calories: Calories(950), + }, + ], + } +} + +#[rstest] +#[case::default_ns( + SIMPLE_3D_DEFAULT_NS_LIST_TEST_XML, + Some("http://my.namespace.example.com/this/is/a/namespace") +)] +#[case::no_default_ns(SIMPLE_3D_NS_LIST_TEST_XML, None)] +fn simple_3d_struct_serialize(#[case] xml: &str, #[case] default_ns: Option<&'static str>) { + let actual = quick_xml_serialize_test_with_default( + simple_3d_list_test_value(), + default_ns.map(XmlNamespace::new).map(Result::unwrap), + ) + .unwrap(); + let expected = clean_string(xml); + assert_eq!(actual, expected); +} + +#[rstest] +#[case::default_ns(SIMPLE_3D_DEFAULT_NS_LIST_TEST_XML)] +#[case::no_default_ns(SIMPLE_3D_NS_LIST_TEST_XML)] +fn simple_3d_struct_deserialize(#[case] xml: &str) { + let actual: BreakfastMenu = quick_xml_deserialize_test(xml).unwrap(); + let expected = simple_3d_list_test_value(); + assert_eq!(actual, expected); +} diff --git a/xmlity-xml-rs/tests/elements/strict_order.rs b/xmlity-xml-rs/tests/elements/strict_order.rs new file mode 100644 index 0000000..d4490a2 --- /dev/null +++ b/xmlity-xml-rs/tests/elements/strict_order.rs @@ -0,0 +1,459 @@ +use pretty_assertions::assert_eq; + +use crate::define_test; +use crate::utils::{clean_string, quick_xml_deserialize_test, quick_xml_serialize_test}; + +use rstest::rstest; +use xmlity::{ + DeserializationGroup, Deserialize, SerializationGroup, Serialize, SerializeAttribute, +}; +use xmlity::{ExpandedName, LocalName}; +use xmlity_quick_xml::Error; + +const SIMPLE_2D_STRUCT_TEST_XML: &str = r###" + + Reminder + Don't forget me this weekend! + +"###; + +const SIMPLE_2D_STRUCT_TEST_XML_WRONG_ORDER: &str = r###" + + Don't forget me this weekend! + Reminder + +"###; + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "to")] +pub struct To(String); + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "from")] +pub struct From(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "heading")] +pub struct Heading(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "body")] +pub struct Body(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "note", attribute_order = "loose", children_order = "loose")] +pub struct Note { + #[xattribute(deferred = true)] + pub to: To, + #[xattribute(deferred = true)] + pub from: From, + pub heading: Heading, + pub body: Body, +} + +fn simple_2d_struct_result() -> Note { + Note { + to: To("Tove".to_string()), + from: From("Jani".to_string()), + heading: Heading("Reminder".to_string()), + body: Body("Don't forget me this weekend!".to_string()), + } +} + +define_test!( + struct_2d_with_attributes, + [( + simple_2d_struct_result(), + clean_string(SIMPLE_2D_STRUCT_TEST_XML) + )] +); + +#[test] +fn struct_2d_with_attributes_deserialize_fail() { + let actual: Result = + quick_xml_deserialize_test(clean_string(SIMPLE_2D_STRUCT_TEST_XML_WRONG_ORDER).as_str()); + + assert!(actual.is_err()); + let Error::WrongName { actual, expected } = actual.unwrap_err() else { + panic!("Wrong error type"); + }; + assert_eq!( + *actual, + ExpandedName::new(LocalName::new("body").unwrap(), None) + ); + assert_eq!( + *expected, + ExpandedName::new(LocalName::new("heading").unwrap(), None) + ); +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "type")] +pub struct HammerType(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "name")] +pub struct ToolName(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "weight")] +pub struct Weight(u32); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "shape")] +pub struct HammerShape(String); + +#[derive(Debug, PartialEq, SerializationGroup, DeserializationGroup)] +#[xgroup] +pub struct Tool { + #[xvalue] + pub name: ToolName, + #[xvalue] + pub weight: Weight, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "hammer", attribute_order = "loose", children_order = "loose")] +pub struct Hammer { + pub hammer_type: HammerType, + #[xgroup] + pub tool: Tool, + pub shape: HammerShape, +} + +const STRUCT_WITH_GROUP_ORDER_EXACT_ORDER: &str = r#" + + Hammer + Hammer + 10 + Square + +"#; + +const STRUCT_WITH_GROUP_ORDER_OK_REORDER: &str = r#" + + Hammer + 10 + Hammer + Square + +"#; + +const STRUCT_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER1: &str = r#" + + Hammer + 10 + Square + Hammer + +"#; + +const STRUCT_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER2: &str = r#" + + 10 + Hammer + Hammer + Square + +"#; + +const STRUCT_WITH_GROUP_ORDER_TEST_XML_WRONG_INCOMPLETE_GROUP: &str = r#" + + 10 + Hammer + Square + +"#; + +fn hammer_struct_result() -> Hammer { + Hammer { + hammer_type: HammerType("Hammer".to_string()), + tool: Tool { + name: ToolName("Hammer".to_string()), + weight: Weight(10), + }, + shape: HammerShape("Square".to_string()), + } +} + +define_test!( + struct_with_group_order, + [ + ( + hammer_struct_result(), + clean_string(STRUCT_WITH_GROUP_ORDER_EXACT_ORDER) + ), + ( + hammer_struct_result(), + clean_string(STRUCT_WITH_GROUP_ORDER_EXACT_ORDER), + clean_string(STRUCT_WITH_GROUP_ORDER_OK_REORDER) + ) + ] +); + +#[rstest] +#[case(STRUCT_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER1)] +#[case(STRUCT_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER2)] +#[case(STRUCT_WITH_GROUP_ORDER_TEST_XML_WRONG_INCOMPLETE_GROUP)] +fn struct_with_group_order_deserialize_fail(#[case] xml: &str) { + let actual: Result = quick_xml_deserialize_test(clean_string(xml).as_str()); + + assert!(actual.is_err()); + //TODO: assert error type +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "type")] +pub struct CarType(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "name")] +pub struct VehicleName(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "shape")] +pub struct CarShape(String); + +#[derive(Debug, PartialEq, SerializationGroup, DeserializationGroup)] +#[xgroup(attribute_order = "loose", children_order = "loose")] +pub struct Vehicle { + #[xvalue] + pub name: VehicleName, + #[xvalue] + pub weight: Weight, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "car", attribute_order = "loose", children_order = "loose")] +pub struct Car { + pub car_type: CarType, + #[xgroup] + pub vehicle: Vehicle, + pub shape: CarShape, +} + +const CAR_WITH_GROUP_ORDER_EXACT_ORDER: &str = r#" + + Car + Car + 10 + Square + +"#; + +const CAR_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER1: &str = r#" + + Car + 10 + Square + Car + +"#; + +const CAR_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER2: &str = r#" + + 10 + Car + Car + Square + +"#; + +const CAR_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER3: &str = r#" + + Car + 10 + Car + Square + +"#; + +const CAR_WITH_GROUP_ORDER_TEST_XML_WRONG_INCOMPLETE_GROUP: &str = r#" + + 10 + Car + Square + +"#; + +fn car_struct_result() -> Car { + Car { + car_type: CarType("Car".to_string()), + vehicle: Vehicle { + name: VehicleName("Car".to_string()), + weight: Weight(10), + }, + shape: CarShape("Square".to_string()), + } +} + +#[test] +fn car_with_group_order_serialize() { + let car = car_struct_result(); + + let actual = quick_xml_serialize_test(car).unwrap(); + + let expected = clean_string(CAR_WITH_GROUP_ORDER_EXACT_ORDER); + + assert_eq!(actual, expected); +} + +#[rstest] +#[case(CAR_WITH_GROUP_ORDER_EXACT_ORDER)] +fn car_with_group_order_deserialize(#[case] xml: &str) { + let actual: Car = quick_xml_deserialize_test(clean_string(xml).as_str()).unwrap(); + + let expected = car_struct_result(); + + assert_eq!(actual, expected); +} + +#[rstest] +#[case(CAR_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER1)] +#[case(CAR_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER2)] +#[case(CAR_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER3)] +#[case(CAR_WITH_GROUP_ORDER_TEST_XML_WRONG_INCOMPLETE_GROUP)] +fn car_with_group_order_deserialize_fail(#[case] xml: &str) { + let actual: Result = quick_xml_deserialize_test(clean_string(xml).as_str()); + + assert!(actual.is_err()); + //TODO: assert error type +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "type")] +pub struct ClothingType(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "name")] +pub struct ClothingName(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "shape")] +pub struct ClothingShape(String); + +#[derive(Debug, PartialEq, SerializationGroup, DeserializationGroup)] +#[xgroup(attribute_order = "strict", children_order = "strict")] +pub struct Clothing { + #[xvalue] + pub name: ClothingName, + #[xvalue] + pub weight: Weight, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "shirt")] +pub struct Shirt { + pub clothing_type: ClothingType, + #[xgroup] + pub clothing: Clothing, + pub shape: ClothingShape, +} + +const SHIRT_WITH_GROUP_ORDER_EXACT_ORDER: &str = r#" + + Shirt + Shirt + 10 + Square + +"#; + +const SHIRT_WITH_GROUP_ORDER_OK_REORDER1: &str = r#" + + Shirt + 10 + Shirt + Square + +"#; + +const SHIRT_WITH_GROUP_ORDER_OK_REORDER2: &str = r#" + + Square + Shirt + Shirt + 10 + +"#; + +const SHIRT_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER1: &str = r#" + + Shirt + 10 + Square + Shirt + +"#; + +const SHIRT_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER2: &str = r#" + + 10 + Shirt + Shirt + Square + +"#; + +const SHIRT_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER3: &str = r#" + + Shirt + 10 + Shirt + Square + +"#; + +const SHIRT_WITH_GROUP_ORDER_TEST_XML_WRONG_INCOMPLETE_GROUP: &str = r#" + + 10 + Shirt + Square + +"#; + +fn shirt_struct_result() -> Shirt { + Shirt { + clothing_type: ClothingType("Shirt".to_string()), + clothing: Clothing { + name: ClothingName("Shirt".to_string()), + weight: Weight(10), + }, + shape: ClothingShape("Square".to_string()), + } +} + +#[test] +fn shirt_with_group_order_serialize() { + let shirt = shirt_struct_result(); + + let actual = quick_xml_serialize_test(shirt).unwrap(); + + let expected = clean_string(SHIRT_WITH_GROUP_ORDER_EXACT_ORDER); + + assert_eq!(actual, expected); +} + +#[rstest] +#[case(SHIRT_WITH_GROUP_ORDER_EXACT_ORDER)] +#[case(SHIRT_WITH_GROUP_ORDER_OK_REORDER1)] +#[case(SHIRT_WITH_GROUP_ORDER_OK_REORDER2)] +fn shirt_with_group_order_deserialize(#[case] xml: &str) { + let actual: Shirt = quick_xml_deserialize_test(clean_string(xml).as_str()).unwrap(); + + let expected = shirt_struct_result(); + + assert_eq!(actual, expected); +} + +#[rstest] +#[case(SHIRT_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER1)] +#[case(SHIRT_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER2)] +#[case(SHIRT_WITH_GROUP_ORDER_TEST_XML_WRONG_ORDER3)] +#[case(SHIRT_WITH_GROUP_ORDER_TEST_XML_WRONG_INCOMPLETE_GROUP)] +fn shirt_with_group_order_deserialize_fail(#[case] xml: &str) { + let actual: Result = quick_xml_deserialize_test(clean_string(xml).as_str()); + + assert!(actual.is_err()); + //TODO: assert error type +} diff --git a/xmlity-xml-rs/tests/features.rs b/xmlity-xml-rs/tests/features.rs new file mode 100644 index 0000000..2d7da85 --- /dev/null +++ b/xmlity-xml-rs/tests/features.rs @@ -0,0 +1,5 @@ +pub mod elements; +pub mod groups; +pub mod other; +pub mod text; +pub mod utils; diff --git a/xmlity-xml-rs/tests/groups/basic.rs b/xmlity-xml-rs/tests/groups/basic.rs new file mode 100644 index 0000000..14f16be --- /dev/null +++ b/xmlity-xml-rs/tests/groups/basic.rs @@ -0,0 +1,230 @@ +use crate::{define_test, utils::clean_string}; + +use xmlity::{ + DeserializationGroup, Deserialize, SerializationGroup, Serialize, SerializeAttribute, +}; + +const SIMPLE_2D_STRUCT_TEST_XML: &str = r###" + + Don't forget me this weekend! + +"###; + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "to")] +pub struct To(String); + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "from")] +pub struct From(String); + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "heading")] +pub struct Heading(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "body")] +pub struct Body(String); + +#[derive(Debug, PartialEq, SerializationGroup, DeserializationGroup)] +pub struct NoteGroup { + #[xattribute(deferred = true)] + pub to: To, + #[xattribute(deferred = true)] + pub from: From, + #[xattribute(deferred = true)] + pub heading: Heading, + pub body: Body, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "note")] +pub struct Note { + #[xgroup] + pub group: NoteGroup, +} + +fn simple_2d_struct_using_group_result() -> Note { + Note { + group: NoteGroup { + to: To("Tove".to_string()), + from: From("Jani".to_string()), + heading: Heading("Reminder".to_string()), + body: Body("Don't forget me this weekend!".to_string()), + }, + } +} + +define_test!( + simple_2d_struct_using_group, + [( + simple_2d_struct_using_group_result(), + clean_string(SIMPLE_2D_STRUCT_TEST_XML) + )] +); + +const SIMPLE_3D_LIST_TEST_XML: &str = r###" + + + + Two of our famous Belgian Waffles with plenty of real maple syrup + + + + + Light Belgian waffles covered with strawberries and whipped cream + + + + + Belgian waffles covered with assorted fresh berries and whipped cream + + + + + Thick slices made from our homemade sourdough bread + + + + + Two eggs, bacon or sausage, toast, and our ever-popular hash browns + + + +"###; + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "name")] +pub struct Name(pub String); + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "price")] +pub struct Price(pub String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "description")] +pub struct Description(pub String); + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "calories")] +pub struct Calories(pub u16); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "food")] +struct Food { + #[xattribute(deferred = true)] + name: Name, + #[xattribute(deferred = true)] + price: Price, + description: Description, + #[xattribute(deferred = true)] + calories: Calories, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "breakfast_menu")] +struct BreakfastMenu { + food: Vec, +} + +fn simple_3d_list_test_value() -> BreakfastMenu { + BreakfastMenu { + food: vec![ + Food { + name: Name("Belgian Waffles".to_string()), + price: Price("$5.95".to_string()), + description: Description( + "Two of our famous Belgian Waffles with plenty of real maple syrup".to_string(), + ), + calories: Calories(650), + }, + Food { + name: Name("Strawberry Belgian Waffles".to_string()), + price: Price("$7.95".to_string()), + description: Description( + "Light Belgian waffles covered with strawberries and whipped cream".to_string(), + ), + calories: Calories(900), + }, + Food { + name: Name("Berry-Berry Belgian Waffles".to_string()), + price: Price("$8.95".to_string()), + description: Description( + "Belgian waffles covered with assorted fresh berries and whipped cream" + .to_string(), + ), + calories: Calories(900), + }, + Food { + name: Name("French Toast".to_string()), + price: Price("$4.50".to_string()), + description: Description( + "Thick slices made from our homemade sourdough bread".to_string(), + ), + calories: Calories(600), + }, + Food { + name: Name("Homestyle Breakfast".to_string()), + price: Price("$6.95".to_string()), + description: Description( + "Two eggs, bacon or sausage, toast, and our ever-popular hash browns" + .to_string(), + ), + calories: Calories(950), + }, + ], + } +} + +define_test!( + struct_3d_using_group, + [( + simple_3d_list_test_value(), + clean_string(SIMPLE_3D_LIST_TEST_XML) + )] +); + +#[derive(Debug, PartialEq, SerializationGroup, DeserializationGroup)] +pub struct NoteGroup2 { + #[xattribute(deferred = true)] + pub heading: Heading, + pub body: Body, +} + +#[derive(Debug, PartialEq, SerializationGroup, DeserializationGroup)] +pub struct NoteGroup1 { + #[xattribute(deferred = true)] + pub to: To, + #[xattribute(deferred = true)] + pub from: From, + #[xgroup] + pub group: NoteGroup2, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "note")] +pub struct Note2 { + #[xgroup] + pub group: NoteGroup1, +} + +fn multi_level_group_2d_struct_using_group_result() -> Note2 { + Note2 { + group: NoteGroup1 { + to: To("Tove".to_string()), + from: From("Jani".to_string()), + group: NoteGroup2 { + heading: Heading("Reminder".to_string()), + body: Body("Don't forget me this weekend!".to_string()), + }, + }, + } +} + +define_test!( + multi_level_group_struct_2d_using_group, + [( + multi_level_group_2d_struct_using_group_result(), + clean_string(SIMPLE_2D_STRUCT_TEST_XML) + )] +); diff --git a/xmlity-xml-rs/tests/groups/generics.rs b/xmlity-xml-rs/tests/groups/generics.rs new file mode 100644 index 0000000..037d2ba --- /dev/null +++ b/xmlity-xml-rs/tests/groups/generics.rs @@ -0,0 +1,41 @@ +use crate::define_test; + +use xmlity::{ + DeserializationGroup, Deserialize, DeserializeOwned, SerializationGroup, Serialize, + SerializeAttribute, +}; + +#[derive(Debug, PartialEq, SerializeAttribute, Deserialize)] +#[xattribute(name = "to")] +pub struct To(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "note")] +pub struct Note { + #[xgroup] + pub to: NoteGroup, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum NoteEnum { + Note(Note), +} + +#[derive(Debug, PartialEq, SerializationGroup, DeserializationGroup)] +pub struct NoteGroup { + #[xattribute(deferred = true)] + pub to: T, +} + +fn simple_2d_struct_result() -> Note { + Note { + to: NoteGroup { + to: To("Tove".to_string()), + }, + } +} + +define_test!( + generic_group, + [(simple_2d_struct_result(), r#""#)] +); diff --git a/xmlity-xml-rs/tests/groups/mod.rs b/xmlity-xml-rs/tests/groups/mod.rs new file mode 100644 index 0000000..bfe3d89 --- /dev/null +++ b/xmlity-xml-rs/tests/groups/mod.rs @@ -0,0 +1,2 @@ +pub mod basic; +pub mod generics; diff --git a/xmlity-xml-rs/tests/other/mod.rs b/xmlity-xml-rs/tests/other/mod.rs new file mode 100644 index 0000000..88b47d0 --- /dev/null +++ b/xmlity-xml-rs/tests/other/mod.rs @@ -0,0 +1,2 @@ +pub mod variant; +pub mod xml_value; diff --git a/xmlity-xml-rs/tests/other/variant.rs b/xmlity-xml-rs/tests/other/variant.rs new file mode 100644 index 0000000..2ea8f3d --- /dev/null +++ b/xmlity-xml-rs/tests/other/variant.rs @@ -0,0 +1,71 @@ +use crate::define_test; +pub use xmlity::types::utils::CData; +pub use xmlity::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum CDataOrText { + CData(CData), + String(String), +} + +define_test!( + cdata_or_text_enum, + [ + (CDataOrText::String("Text".to_owned()), "Text"), + ( + CDataOrText::CData(CData("CData".to_owned())), + "" + ) + ] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "list")] +pub struct VariantList { + pub text1: Vec, +} + +fn mixed_cdata_list() -> VariantList { + VariantList { + text1: vec![ + CDataOrText::String("Text1".to_string()), + CDataOrText::CData(CData("Text2".to_string())), + ], + } +} + +define_test!( + mixed_cdata_list_struct, + [(mixed_cdata_list(), "Text1")] +); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "elem")] +pub struct Elem(String); + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xelement(name = "mixed")] +pub struct MixedCDataSeparated { + pub text1: Vec, + pub elem: Elem, + pub text2: String, +} + +fn mixed_cdata_separated_1() -> MixedCDataSeparated { + MixedCDataSeparated { + text1: vec![ + CDataOrText::String("Text1".to_string()), + CDataOrText::CData(CData("Text2".to_string())), + ], + elem: Elem("Text3".to_string()), + text2: "Text4".to_string(), + } +} + +define_test!( + mixed_cdata_separated, + [( + mixed_cdata_separated_1(), + "Text1Text3Text4" + )] +); diff --git a/xmlity-xml-rs/tests/other/xml_value.rs b/xmlity-xml-rs/tests/other/xml_value.rs new file mode 100644 index 0000000..bb106c8 --- /dev/null +++ b/xmlity-xml-rs/tests/other/xml_value.rs @@ -0,0 +1,87 @@ +use crate::{define_test, utils::clean_string}; + +use xmlity::{ + types::value::{XmlAttribute, XmlChild, XmlDecl, XmlElement, XmlText, XmlValue}, + ExpandedName, LocalName, XmlNamespace, +}; + +const SIMPLE_1D_STRUCT_TEST_XML: &str = r###" + Tove +"###; + +fn xml_value() -> XmlValue { + XmlValue::Element( + XmlElement::new(ExpandedName::new(LocalName::new("to").unwrap(), None)) + .with_children(vec![XmlChild::Text(XmlText::new("Tove"))]), + ) +} + +define_test!( + xml_value_1d_element, + [(xml_value(), clean_string(SIMPLE_1D_STRUCT_TEST_XML))] +); + +const COMPLEX_XML_EXAMPLE: &str = r###" + + Tove + Jani + Reminder + Don't forget me this weekend! + + Test + + +"###; + +fn complex_xml_value() -> XmlValue { + XmlValue::Element( + XmlElement::new(ExpandedName::new(LocalName::new("note").unwrap(), None)).with_children( + vec![ + XmlChild::Element( + XmlElement::new(ExpandedName::new(LocalName::new("to").unwrap(), None)) + .with_children(vec![XmlChild::Text(XmlText::new("Tove"))]), + ), + XmlChild::Element( + XmlElement::new(ExpandedName::new(LocalName::new("from").unwrap(), None)) + .with_children(vec![XmlChild::Text(XmlText::new("Jani"))]), + ), + XmlChild::Element( + XmlElement::new(ExpandedName::new(LocalName::new("heading").unwrap(), None)) + .with_children(vec![XmlChild::Text(XmlText::new("Reminder"))]), + ), + XmlChild::Element( + XmlElement::new(ExpandedName::new(LocalName::new("body").unwrap(), None)) + .with_attributes(vec![XmlAttribute::new( + ExpandedName::new(LocalName::new("attribute").unwrap(), None), + "value".to_string(), + )]) + .with_child(XmlText::new("Don't forget me this weekend!")), + ), + XmlChild::Element( + XmlElement::new(ExpandedName::new( + LocalName::new("test").unwrap(), + Some(XmlNamespace::new("http://testns.com").unwrap()), + )) + .with_children(vec![XmlChild::Text(XmlText::new("Test"))]), + ), + ], + ), + ) +} + +define_test!( + complex_xml_value, + [(complex_xml_value(), clean_string(COMPLEX_XML_EXAMPLE))] +); + +fn decl_xml_value() -> XmlDecl { + XmlDecl::new("1.0", Some("UTF-8"), None) +} + +define_test!( + complex_xml_decl, + [( + decl_xml_value(), + r#""# + )] +); diff --git a/xmlity-xml-rs/tests/text/enum_value.rs b/xmlity-xml-rs/tests/text/enum_value.rs new file mode 100644 index 0000000..0148890 --- /dev/null +++ b/xmlity-xml-rs/tests/text/enum_value.rs @@ -0,0 +1,29 @@ +use crate::{define_test, utils::quick_xml_deserialize_test}; + +use rstest::rstest; +use xmlity::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum Union { + #[xvalue(value = "restriction")] + Restriction, + #[xvalue(value = "extension")] + Extension, +} + +define_test!( + union_test, + [ + (Union::Restriction, "restriction"), + (Union::Extension, "extension") + ] +); + +#[rstest] +#[case::restriction("Restriction")] +#[case::extension("Extension")] +fn wrong_union_test(#[case] xml: &str) { + let union: Result = quick_xml_deserialize_test(xml); + + assert!(union.is_err()); +} diff --git a/xmlity-xml-rs/tests/text/enum_value_rename_all.rs b/xmlity-xml-rs/tests/text/enum_value_rename_all.rs new file mode 100644 index 0000000..4c1248a --- /dev/null +++ b/xmlity-xml-rs/tests/text/enum_value_rename_all.rs @@ -0,0 +1,320 @@ +use std::any::type_name; + +use pretty_assertions::assert_eq; + +use crate::utils::{quick_xml_deserialize_test, quick_xml_serialize_test}; + +use rstest::rstest; +use xmlity::{Deserialize, DeserializeOwned, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xvalue] +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +enum EnumValue { + Alpha, + Beta, + Gamma, + ZuluZuluZulu, + Iota_Iota_Iota, + KAPPAKAPPAKAPPA, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xvalue(rename_all = "lowercase")] +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +enum EnumValuelower { + Alpha, + Beta, + Gamma, + ZuluZuluZulu, + Iota_Iota_Iota, + KAPPAKAPPAKAPPA, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xvalue(rename_all = "PascalCase")] +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +enum EnumValuePascalCase { + Alpha, + Beta, + Gamma, + ZuluZuluZulu, + Iota_Iota_Iota, + KAPPAKAPPAKAPPA, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xvalue(rename_all = "camelCase")] +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +enum EnumValueCamelCase { + Alpha, + Beta, + Gamma, + ZuluZuluZulu, + Iota_Iota_Iota, + KAPPAKAPPAKAPPA, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xvalue(rename_all = "snake_case")] +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +enum EnumValueSnakeCase { + Alpha, + Beta, + Gamma, + ZuluZuluZulu, + Iota_Iota_Iota, + KAPPAKAPPAKAPPA, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xvalue(rename_all = "SCREAMING_SNAKE_CASE")] +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +enum EnumValueScreamingSnakeCase { + Alpha, + Beta, + Gamma, + ZuluZuluZulu, + Iota_Iota_Iota, + KAPPAKAPPAKAPPA, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xvalue(rename_all = "kebab-case")] +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +enum EnumValueKebabCase { + Alpha, + Beta, + Gamma, + ZuluZuluZulu, + Iota_Iota_Iota, + KAPPAKAPPAKAPPA, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[xvalue(rename_all = "SCREAMING-KEBAB-CASE")] +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +enum EnumValueScreamingKebabCase { + Alpha, + Beta, + Gamma, + ZuluZuluZulu, + Iota_Iota_Iota, + KAPPAKAPPAKAPPA, +} +#[rstest] +#[case::default_alpha(EnumValue::Alpha, "Alpha")] +#[case::default_beta(EnumValue::Beta, "Beta")] +#[case::default_gamma(EnumValue::Gamma, "Gamma")] +#[case::default_zulu_zulu_zulu(EnumValue::ZuluZuluZulu, "ZuluZuluZulu")] +#[case::default_iota_iota_iota(EnumValue::Iota_Iota_Iota, "Iota_Iota_Iota")] +#[case::default_kappa_kappa_kappa(EnumValue::KAPPAKAPPAKAPPA, "KAPPAKAPPAKAPPA")] +#[case::lower_alpha(EnumValuelower::Alpha, "alpha")] +#[case::lower_beta(EnumValuelower::Beta, "beta")] +#[case::lower_gamma(EnumValuelower::Gamma, "gamma")] +#[case::lower_zulu_zulu_zulu(EnumValuelower::ZuluZuluZulu, "zuluzuluzulu")] +#[case::lower_iota_iota_iota(EnumValuelower::Iota_Iota_Iota, "iota_iota_iota")] +#[case::lower_kappa_kappa_kappa(EnumValuelower::KAPPAKAPPAKAPPA, "kappakappakappa")] +#[case::pascal_alpha(EnumValuePascalCase::Alpha, "Alpha")] +#[case::pascal_beta(EnumValuePascalCase::Beta, "Beta")] +#[case::pascal_gamma(EnumValuePascalCase::Gamma, "Gamma")] +#[case::pascal_zulu_zulu_zulu(EnumValuePascalCase::ZuluZuluZulu, "ZuluZuluZulu")] +#[case::pascal_iota_iota_iota(EnumValuePascalCase::Iota_Iota_Iota, "Iota_Iota_Iota")] +#[case::pascal_kappa_kappa_kappa(EnumValuePascalCase::KAPPAKAPPAKAPPA, "KAPPAKAPPAKAPPA")] +#[case::camel_alpha(EnumValueCamelCase::Alpha, "alpha")] +#[case::camel_beta(EnumValueCamelCase::Beta, "beta")] +#[case::camel_gamma(EnumValueCamelCase::Gamma, "gamma")] +#[case::camel_zulu_zulu_zulu(EnumValueCamelCase::ZuluZuluZulu, "zuluZuluZulu")] +#[case::camel_iota_iota_iota(EnumValueCamelCase::Iota_Iota_Iota, "iota_Iota_Iota")] +#[case::camel_kappa_kappa_kappa(EnumValueCamelCase::KAPPAKAPPAKAPPA, "kAPPAKAPPAKAPPA")] +#[case::snake_alpha(EnumValueSnakeCase::Alpha, "alpha")] +#[case::snake_beta(EnumValueSnakeCase::Beta, "beta")] +#[case::snake_gamma(EnumValueSnakeCase::Gamma, "gamma")] +#[case::snake_zulu_zulu_zulu(EnumValueSnakeCase::ZuluZuluZulu, "zulu_zulu_zulu")] +#[case::snake_iota_iota_iota(EnumValueSnakeCase::Iota_Iota_Iota, "iota__iota__iota")] +#[case::snake_kappa_kappa_kappa( + EnumValueSnakeCase::KAPPAKAPPAKAPPA, + "k_a_p_p_a_k_a_p_p_a_k_a_p_p_a" +)] +#[case::screaming_snake_alpha(EnumValueScreamingSnakeCase::Alpha, "ALPHA")] +#[case::screaming_snake_beta(EnumValueScreamingSnakeCase::Beta, "BETA")] +#[case::screaming_snake_gamma(EnumValueScreamingSnakeCase::Gamma, "GAMMA")] +#[case::screaming_snake_zulu_zulu_zulu(EnumValueScreamingSnakeCase::ZuluZuluZulu, "ZULU_ZULU_ZULU")] +#[case::screaming_snake_iota_iota_iota( + EnumValueScreamingSnakeCase::Iota_Iota_Iota, + "IOTA__IOTA__IOTA" +)] +#[case::screaming_snake_kappa_kappa_kappa( + EnumValueScreamingSnakeCase::KAPPAKAPPAKAPPA, + "K_A_P_P_A_K_A_P_P_A_K_A_P_P_A" +)] +#[case::kebab_case_alpha(EnumValueKebabCase::Alpha, "alpha")] +#[case::kebab_case_beta(EnumValueKebabCase::Beta, "beta")] +#[case::kebab_case_gamma(EnumValueKebabCase::Gamma, "gamma")] +#[case::kebab_case_zulu_zulu_zulu(EnumValueKebabCase::ZuluZuluZulu, "zulu-zulu-zulu")] +#[case::kebab_case_iota_iota_iota(EnumValueKebabCase::Iota_Iota_Iota, "iota--iota--iota")] +#[case::kebab_case_kappa_kappa_kappa( + EnumValueKebabCase::KAPPAKAPPAKAPPA, + "k-a-p-p-a-k-a-p-p-a-k-a-p-p-a" +)] +#[case::screaming_kebab_case_alpha(EnumValueScreamingKebabCase::Alpha, "ALPHA")] +#[case::screaming_kebab_case_beta(EnumValueScreamingKebabCase::Beta, "BETA")] +#[case::screaming_kebab_case_gamma(EnumValueScreamingKebabCase::Gamma, "GAMMA")] +#[case::screaming_kebab_case_zulu_zulu_zulu( + EnumValueScreamingKebabCase::ZuluZuluZulu, + "ZULU-ZULU-ZULU" +)] +#[case::screaming_kebab_case_iota_iota_iota( + EnumValueScreamingKebabCase::Iota_Iota_Iota, + "IOTA--IOTA--IOTA" +)] +#[case::screaming_kebab_case_kappa_kappa_kappa( + EnumValueScreamingKebabCase::KAPPAKAPPAKAPPA, + "K-A-P-P-A-K-A-P-P-A-K-A-P-P-A" +)] +fn serialize(#[case] value: T, #[case] expected: &str) { + let actual = quick_xml_serialize_test(value).unwrap(); + + assert_eq!(actual, expected); +} +#[rstest] +#[case::default_alpha(EnumValue::Alpha, "Alpha")] +#[case::default_beta(EnumValue::Beta, "Beta")] +#[case::default_gamma(EnumValue::Gamma, "Gamma")] +#[case::default_zulu_zulu_zulu(EnumValue::ZuluZuluZulu, "ZuluZuluZulu")] +#[case::default_iota_iota_iota(EnumValue::Iota_Iota_Iota, "Iota_Iota_Iota")] +#[case::default_kappa_kappa_kappa(EnumValue::KAPPAKAPPAKAPPA, "KAPPAKAPPAKAPPA")] +#[case::lower_alpha(EnumValuelower::Alpha, "alpha")] +#[case::lower_beta(EnumValuelower::Beta, "beta")] +#[case::lower_gamma(EnumValuelower::Gamma, "gamma")] +#[case::lower_zulu_zulu_zulu(EnumValuelower::ZuluZuluZulu, "zuluzuluzulu")] +#[case::lower_iota_iota_iota(EnumValuelower::Iota_Iota_Iota, "iota_iota_iota")] +#[case::lower_kappa_kappa_kappa(EnumValuelower::KAPPAKAPPAKAPPA, "kappakappakappa")] +#[case::pascal_alpha(EnumValuePascalCase::Alpha, "Alpha")] +#[case::pascal_beta(EnumValuePascalCase::Beta, "Beta")] +#[case::pascal_gamma(EnumValuePascalCase::Gamma, "Gamma")] +#[case::pascal_zulu_zulu_zulu(EnumValuePascalCase::ZuluZuluZulu, "ZuluZuluZulu")] +#[case::pascal_iota_iota_iota(EnumValuePascalCase::Iota_Iota_Iota, "Iota_Iota_Iota")] +#[case::pascal_kappa_kappa_kappa(EnumValuePascalCase::KAPPAKAPPAKAPPA, "KAPPAKAPPAKAPPA")] +#[case::camel_alpha(EnumValueCamelCase::Alpha, "alpha")] +#[case::camel_beta(EnumValueCamelCase::Beta, "beta")] +#[case::camel_gamma(EnumValueCamelCase::Gamma, "gamma")] +#[case::camel_zulu_zulu_zulu(EnumValueCamelCase::ZuluZuluZulu, "zuluZuluZulu")] +#[case::camel_iota_iota_iota(EnumValueCamelCase::Iota_Iota_Iota, "iota_Iota_Iota")] +#[case::camel_kappa_kappa_kappa(EnumValueCamelCase::KAPPAKAPPAKAPPA, "kAPPAKAPPAKAPPA")] +#[case::snake_alpha(EnumValueSnakeCase::Alpha, "alpha")] +#[case::snake_beta(EnumValueSnakeCase::Beta, "beta")] +#[case::snake_gamma(EnumValueSnakeCase::Gamma, "gamma")] +#[case::snake_zulu_zulu_zulu(EnumValueSnakeCase::ZuluZuluZulu, "zulu_zulu_zulu")] +#[case::snake_iota_iota_iota(EnumValueSnakeCase::Iota_Iota_Iota, "iota__iota__iota")] +#[case::snake_kappa_kappa_kappa( + EnumValueSnakeCase::KAPPAKAPPAKAPPA, + "k_a_p_p_a_k_a_p_p_a_k_a_p_p_a" +)] +#[case::screaming_snake_alpha(EnumValueScreamingSnakeCase::Alpha, "ALPHA")] +#[case::screaming_snake_beta(EnumValueScreamingSnakeCase::Beta, "BETA")] +#[case::screaming_snake_gamma(EnumValueScreamingSnakeCase::Gamma, "GAMMA")] +#[case::screaming_snake_zulu_zulu_zulu(EnumValueScreamingSnakeCase::ZuluZuluZulu, "ZULU_ZULU_ZULU")] +#[case::screaming_snake_iota_iota_iota( + EnumValueScreamingSnakeCase::Iota_Iota_Iota, + "IOTA__IOTA__IOTA" +)] +#[case::screaming_snake_kappa_kappa_kappa( + EnumValueScreamingSnakeCase::KAPPAKAPPAKAPPA, + "K_A_P_P_A_K_A_P_P_A_K_A_P_P_A" +)] +#[case::kebab_case_alpha(EnumValueKebabCase::Alpha, "alpha")] +#[case::kebab_case_beta(EnumValueKebabCase::Beta, "beta")] +#[case::kebab_case_gamma(EnumValueKebabCase::Gamma, "gamma")] +#[case::kebab_case_zulu_zulu_zulu(EnumValueKebabCase::ZuluZuluZulu, "zulu-zulu-zulu")] +#[case::kebab_case_iota_iota_iota(EnumValueKebabCase::Iota_Iota_Iota, "iota--iota--iota")] +#[case::kebab_case_kappa_kappa_kappa( + EnumValueKebabCase::KAPPAKAPPAKAPPA, + "k-a-p-p-a-k-a-p-p-a-k-a-p-p-a" +)] +#[case::screaming_kebab_case_alpha(EnumValueScreamingKebabCase::Alpha, "ALPHA")] +#[case::screaming_kebab_case_beta(EnumValueScreamingKebabCase::Beta, "BETA")] +#[case::screaming_kebab_case_gamma(EnumValueScreamingKebabCase::Gamma, "GAMMA")] +#[case::screaming_kebab_case_zulu_zulu_zulu( + EnumValueScreamingKebabCase::ZuluZuluZulu, + "ZULU-ZULU-ZULU" +)] +#[case::screaming_kebab_case_iota_iota_iota( + EnumValueScreamingKebabCase::Iota_Iota_Iota, + "IOTA--IOTA--IOTA" +)] +#[case::screaming_kebab_case_kappa_kappa_kappa( + EnumValueScreamingKebabCase::KAPPAKAPPAKAPPA, + "K-A-P-P-A-K-A-P-P-A-K-A-P-P-A" +)] +fn deserialize( + #[case] expected: T, + #[case] text: &str, +) { + let actual: T = quick_xml_deserialize_test(text).unwrap(); + + assert_eq!(actual, expected); +} + +#[rstest] +#[case::default_0("Lmao", EnumValue::Alpha)] +#[case::default_1("lmao", EnumValue::Alpha)] +#[case::default_2("BETA", EnumValue::Alpha)] +#[case::default_3("gamma", EnumValue::Alpha)] +#[case::default_4("zuluZuluZulu", EnumValue::Alpha)] +#[case::default_5("IOTA_IOTA_IOTA", EnumValue::Alpha)] +#[case::default_6("kappakappakappa", EnumValue::Alpha)] +#[case::lower_0("ALPHA", EnumValuelower::Alpha)] +#[case::lower_1("Beta", EnumValuelower::Alpha)] +#[case::lower_2("GAMMA", EnumValuelower::Alpha)] +#[case::lower_3("ZuluZuluZulu", EnumValuelower::Alpha)] +#[case::lower_4("iotaiotaiota", EnumValuelower::Alpha)] +#[case::lower_5("KAPPAKAPPAKAPPA", EnumValuelower::Alpha)] +#[case::pascal_0("alpha", EnumValuePascalCase::Alpha)] +#[case::pascal_1("BETA", EnumValuePascalCase::Alpha)] +#[case::pascal_2("gamma", EnumValuePascalCase::Alpha)] +#[case::pascal_3("zuluzuluzulu", EnumValuePascalCase::Alpha)] +#[case::pascal_4("IotaIotaIota", EnumValuePascalCase::Alpha)] +#[case::pascal_5("KappaKappaKappa", EnumValuePascalCase::Alpha)] +#[case::camel_0("ALPHA", EnumValueCamelCase::Alpha)] +#[case::camel_1("geta", EnumValueCamelCase::Alpha)] +#[case::camel_2("GAMMA", EnumValueCamelCase::Alpha)] +#[case::camel_3("ZuluZuluZulu", EnumValueCamelCase::Alpha)] +#[case::camel_4("iotaIotaIota", EnumValueCamelCase::Alpha)] +#[case::camel_5("kappaKappaKappa", EnumValueCamelCase::Alpha)] +#[case::snake_0("Alpha", EnumValueSnakeCase::Alpha)] +#[case::snake_1("BETA", EnumValueSnakeCase::Alpha)] +#[case::snake_2("GAMMA", EnumValueSnakeCase::Alpha)] +#[case::snake_3("zuluZuluZulu", EnumValueSnakeCase::Alpha)] +#[case::snake_4("IOTA_IOTA_IOTA", EnumValueSnakeCase::Alpha)] +#[case::snake_5("kappa_kappa_kappa", EnumValueSnakeCase::Alpha)] +#[case::screaming_snake_0("alpha", EnumValueScreamingSnakeCase::Alpha)] +#[case::screaming_snake_1("beta", EnumValueScreamingSnakeCase::Alpha)] +#[case::screaming_snake_2("Gamma", EnumValueScreamingSnakeCase::Alpha)] +#[case::screaming_snake_3("ZULU-ZULU-ZULU", EnumValueScreamingSnakeCase::Alpha)] +#[case::screaming_snake_4("iota_iota_iota", EnumValueScreamingSnakeCase::Alpha)] +#[case::screaming_snake_5("KAPPA_KAPPA_KAPPA", EnumValueScreamingSnakeCase::Alpha)] +#[case::kebab_0("Alpha", EnumValueKebabCase::Alpha)] +#[case::kebab_1("BETA", EnumValueKebabCase::Alpha)] +#[case::kebab_2("Gamma", EnumValueKebabCase::Alpha)] +#[case::kebab_3("zulu_zulu_zulu", EnumValueKebabCase::Alpha)] +#[case::kebab_4("IOTA-IOTA-IOTA", EnumValueKebabCase::Alpha)] +#[case::kebab_5("kappa-kappa-kappa", EnumValueKebabCase::Alpha)] +#[case::screaming_kebab_0("alpha", EnumValueScreamingKebabCase::Alpha)] +#[case::screaming_kebab_1("beta", EnumValueScreamingKebabCase::Alpha)] +#[case::screaming_kebab_2("Gamma", EnumValueScreamingKebabCase::Alpha)] +#[case::screaming_kebab_3("ZULU_ZULU_ZULU", EnumValueScreamingKebabCase::Alpha)] +#[case::screaming_kebab_4("IOTA-IOTA-IOTA", EnumValueScreamingKebabCase::Alpha)] +#[case::screaming_kebab_5("KAPPA-KAPPA-KAPPA", EnumValueScreamingKebabCase::Alpha)] +fn wrong_deserialize( + #[case] invalid: &str, + #[case] _type_marker: T, +) { + let actual: Result = quick_xml_deserialize_test(invalid); + assert!(actual.is_err()); + if let xmlity_quick_xml::Error::NoPossibleVariant { ident } = actual.unwrap_err() { + assert_eq!(ident, type_name::().split("::").last().unwrap()); + } else { + panic!("Unexpected error type"); + } +} diff --git a/xmlity-xml-rs/tests/text/extendable.rs b/xmlity-xml-rs/tests/text/extendable.rs new file mode 100644 index 0000000..3d14a60 --- /dev/null +++ b/xmlity-xml-rs/tests/text/extendable.rs @@ -0,0 +1,22 @@ +use crate::define_test; + +use xmlity::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct ExtendableText(#[xvalue(extendable = true)] String); + +fn extendable_struct() -> ExtendableText { + ExtendableText("AsdrebootMoreText".to_string()) +} + +define_test!( + extendable_struct, + [ + ( + extendable_struct(), + "AsdrebootMoreText", + "AsdrebootText" + ), + (extendable_struct(), "AsdrebootMoreText") + ] +); diff --git a/xmlity-xml-rs/tests/text/mixed.rs b/xmlity-xml-rs/tests/text/mixed.rs new file mode 100644 index 0000000..0bd8af3 --- /dev/null +++ b/xmlity-xml-rs/tests/text/mixed.rs @@ -0,0 +1,23 @@ +use crate::define_test; + +use xmlity::types::utils::CData; +use xmlity::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct MixedCData { + pub text1: String, + pub cdata: CData, + pub text2: String, +} + +define_test!( + mixed_cdata_struct, + [( + MixedCData { + text1: "Text".to_string(), + cdata: CData("More".to_string()), + text2: "Text2".to_string(), + }, + "TextText2" + )] +); diff --git a/xmlity-xml-rs/tests/text/mod.rs b/xmlity-xml-rs/tests/text/mod.rs new file mode 100644 index 0000000..22eeca1 --- /dev/null +++ b/xmlity-xml-rs/tests/text/mod.rs @@ -0,0 +1,5 @@ +pub mod enum_value; +pub mod enum_value_rename_all; +pub mod extendable; +pub mod mixed; +pub mod strings; diff --git a/xmlity-xml-rs/tests/text/strings.rs b/xmlity-xml-rs/tests/text/strings.rs new file mode 100644 index 0000000..f77884e --- /dev/null +++ b/xmlity-xml-rs/tests/text/strings.rs @@ -0,0 +1,3 @@ +use crate::define_test; + +define_test!(single_string, [("Alpha".to_owned(), "Alpha")]); diff --git a/xmlity-xml-rs/tests/utils.rs b/xmlity-xml-rs/tests/utils.rs new file mode 100644 index 0000000..787859f --- /dev/null +++ b/xmlity-xml-rs/tests/utils.rs @@ -0,0 +1,91 @@ +#![allow(dead_code)] + +use xmlity::{Prefix, XmlNamespace}; + +pub fn quick_xml_serialize_test( + input: T, +) -> Result { + quick_xml_serialize_test_with_default(input, None) +} + +pub fn quick_xml_serialize_test_with_default( + input: T, + default_namespace: Option>, +) -> Result { + let serializer = quick_xml::Writer::new(Vec::new()); + let mut serializer = xmlity_quick_xml::Serializer::from(serializer); + serializer.add_preferred_prefix( + XmlNamespace::new("http://my.namespace.example.com/this/is/a/namespace").unwrap(), + Prefix::new("testns").expect("testns is a valid prefix"), + ); + if let Some(default_namespace) = default_namespace { + serializer.add_preferred_prefix(default_namespace, Prefix::default()); + } + + input.serialize(&mut serializer)?; + let actual_xml = String::from_utf8(serializer.into_inner()).unwrap(); + + Ok(actual_xml) +} + +pub fn quick_xml_deserialize_test( + input: &str, +) -> Result { + let reader = quick_xml::NsReader::from_reader(input.as_bytes()); + + let mut deserializer = xmlity_quick_xml::de::Deserializer::from(reader); + + T::deserialize_seq(&mut deserializer) +} + +pub fn clean_string(input: &str) -> String { + input.trim().lines().map(str::trim).collect::() +} + +#[macro_export] +macro_rules! define_test { + // Main implementation of the macro + (@impl $name: ident, [$(,)*$(($value:expr, $serialize_xml:expr, $deserialize_xml:expr)),*]) => { + mod $name { + #[allow(unused_imports)] + use super::*; + #[rstest::rstest] + $( + #[case($value, $serialize_xml)] + )* + fn serialize>(#[case] to: T, #[case] expected: U) { + let actual = $crate::utils::quick_xml_serialize_test(to).unwrap(); + + pretty_assertions::assert_eq!(actual, expected.as_ref()); + } + + #[rstest::rstest] + $( + #[case($value, $deserialize_xml)] + )* + fn deserialize>(#[case] expected: T, #[case] xml: U) { + let actual: T = $crate::utils::quick_xml_deserialize_test(xml.as_ref()).unwrap(); + + pretty_assertions::assert_eq!(actual, expected); + } + } + }; + (@internal $name: ident, [$($existing:tt)*], [($value2:expr, $serialize_xml2:expr, $deserialize_xml2:expr)]) => { + $crate::define_test!(@impl $name, [$($existing)*, ($value2, $serialize_xml2, $deserialize_xml2)]); + }; + (@internal $name: ident, [$($existing:tt)*], [($value2:expr, $xml:expr)]) => { + $crate::define_test!(@impl $name, [$($existing)*, ($value2, $xml, $xml)]); + }; + (@internal $name: ident, [$($existing:tt)*], [($value2:expr, $serialize_xml2:expr, $deserialize_xml2:expr), $($tail:tt)*]) => { + $crate::define_test!(@internal $name, [$($existing)*, ($value2, $serialize_xml2, $deserialize_xml2)], [$($tail)*]); + }; + (@internal $name: ident, [$($existing:tt)*], [($value2:expr, $xml:expr), $($tail:tt)*]) => { + $crate::define_test!(@internal $name, [$($existing)*, ($value2, $xml, $xml)], [$($tail)*]); + }; + ($name: ident, [$($tail:tt)*]) => { + $crate::define_test!(@internal $name, [], [$($tail)*]); + }; + // ($name: ident, []) => { + // $crate::define_test!($name, [], []); + // }; +} diff --git a/xmlity-xml-rs/tests/xsd.rs b/xmlity-xml-rs/tests/xsd.rs new file mode 100644 index 0000000..6d0901a --- /dev/null +++ b/xmlity-xml-rs/tests/xsd.rs @@ -0,0 +1,772 @@ +//! This uses parts of the XSD specification file to test the derive macros. +//! +//! This has nothing to do with the actual XSD crate/generator, it's just some tests that happen to use the same thing. +use pretty_assertions::assert_eq; + +pub mod utils; +use utils::{clean_string, quick_xml_deserialize_test}; + +use std::fs; + +use xmlity::types::{ + utils::{IgnoredAny, XmlRoot}, + value::{XmlComment, XmlDecl, XmlDoctype}, +}; +use xmlity::{Deserialize, Serialize, SerializeAttribute}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[xelement(name = "sequence", namespace = "http://www.w3.org/2001/XMLSchema")] +pub struct Sequence {} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[xelement(name = "annotation", namespace = "http://www.w3.org/2001/XMLSchema")] +pub struct Annotation {} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[xelement(name = "complexType", namespace = "http://www.w3.org/2001/XMLSchema")] +pub struct ComplexType { + pub complex_content: ComplexContent, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ExtensionEntry { + Sequence(Sequence), + Attribute(Attribute), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[xelement( + name = "complexContent", + namespace = "http://www.w3.org/2001/XMLSchema" +)] +pub struct ComplexContent { + content: Extension, +} + +#[derive(Debug, Clone, SerializeAttribute, Deserialize, PartialEq)] +#[xattribute(name = "base")] +pub struct Base(String); + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[xelement( + name = "extension", + namespace = "http://www.w3.org/2001/XMLSchema", + allow_unknown_children, + allow_unknown_attributes +)] +pub struct Extension { + #[xattribute(deferred = true)] + base: Base, + entries: Vec, +} + +#[derive(Debug, Clone, SerializeAttribute, Deserialize, PartialEq)] +#[xattribute(name = "name")] +pub struct Name(String); + +#[derive(Debug, Clone, SerializeAttribute, Deserialize, PartialEq)] +#[xattribute(name = "id")] +pub struct Id(String); + +#[derive(Debug, Clone, SerializeAttribute, Deserialize, PartialEq)] +#[xattribute(name = "type")] +pub struct Type(String); + +#[derive(Debug, Clone, SerializeAttribute, Deserialize, PartialEq)] +#[xattribute(name = "use")] +pub struct Use(String); + +#[derive(Debug, Clone, SerializeAttribute, Deserialize, PartialEq)] +#[xattribute(name = "default")] +pub struct Default(String); + +#[derive(Debug, Clone, SerializeAttribute, Deserialize, PartialEq)] +#[xattribute(name = "ref")] +pub struct Ref(String); + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[xelement( + name = "attribute", + namespace = "http://www.w3.org/2001/XMLSchema", + allow_unknown_children, + allow_unknown_attributes +)] +pub struct Attribute { + #[xattribute(default, deferred = true)] + name: Option, + #[xattribute(default, deferred)] + attr_type: Option, + #[xattribute(default, deferred)] + attr_use: Option, + #[xattribute(default, deferred)] + attr_default: Option, + #[xattribute(default, deferred)] + attr_ref: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[xelement( + name = "element", + namespace = "http://www.w3.org/2001/XMLSchema", + allow_unknown_children, + allow_unknown_attributes +)] +pub struct Element { + #[xattribute(deferred = true)] + name: Name, + #[xattribute(deferred = true)] + id: Id, + annotation: Annotation, + complex_type: ComplexType, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum SchemaEntry { + Annotation(Annotation), + Element(Element), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[xelement(name = "schema", namespace = "http://www.w3.org/2001/XMLSchema")] +pub struct Schema { + #[xvalue(default)] + pub sequence: Vec, +} + +const XSD_XML_DOCTYPE: &str = r###"xs:schema PUBLIC "-//W3C//DTD XMLSCHEMA 200102//EN" "XMLSchema.dtd" [ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]"###; + +fn xsd_struct() -> XmlRoot { + XmlRoot::new() + .with_decl(XmlDecl::new("1.0", Some("UTF-8"), None)) + .with_comments([ + XmlComment::new( + " XML Schema schema for XML Schemas: Part 1: Structures " + .as_bytes() + .to_owned(), + ), + XmlComment::new( + " Note this schema is NOT the normative structures schema. " + .as_bytes() + .to_owned(), + ), + XmlComment::new( + " The prose copy in the structures REC is the normative " + .as_bytes() + .to_owned(), + ), + XmlComment::new( + " version (which shouldn't differ from this one except for " + .as_bytes() + .to_owned(), + ), + XmlComment::new( + " this comment and entity expansions, but just in case " + .as_bytes() + .to_owned(), + ), + ]) + .with_doctype(XmlDoctype::new(XSD_XML_DOCTYPE.as_bytes())) + .with_element(Schema { + sequence: vec![ + SchemaEntry::Annotation(Annotation {}), + SchemaEntry::Annotation(Annotation {}), + SchemaEntry::Annotation(Annotation {}), + ], + }) +} + +#[test] +fn xsd_struct_deserialize() { + let input_xml = fs::read_to_string("./tests/XMLSchema.xsd").unwrap(); + + // Windows uses CRLF line endings, but the tests assume LF line endings. This is a hack to make the tests pass on Windows. + #[cfg(windows)] + let input_xml = input_xml.replace("\r\n", "\n"); + + let actual: XmlRoot = quick_xml_deserialize_test(&input_xml).unwrap(); + + let expected = xsd_struct(); + + assert_eq!(actual, expected); +} + +const EMPTY_SCHEMA: &str = r####" + + + +"####; + +fn empty_schema() -> XmlRoot { + XmlRoot::new() + .with_decl(XmlDecl::new("1.0", Some("UTF-8"), None)) + .with_element(Schema { sequence: vec![] }) +} + +#[test] +fn empty_schema_deserialize() { + let actual: XmlRoot = quick_xml_deserialize_test(&clean_string(EMPTY_SCHEMA)).unwrap(); + let expected = empty_schema(); + assert_eq!(actual, expected); +} + +const SCHEMA_WITH_SINGLE_ANNOTATION: &str = r####" + + + + Schema for XML Schema. + + +"####; + +fn schema_with_single_annotation() -> XmlRoot { + XmlRoot::new() + .with_decl(XmlDecl::new("1.0", Some("UTF-8"), None)) + .with_element(Schema { + sequence: vec![SchemaEntry::Annotation(Annotation {})], + }) +} + +#[test] +fn schema_with_single_annotation_deserialize() { + let actual: XmlRoot = + quick_xml_deserialize_test(SCHEMA_WITH_SINGLE_ANNOTATION).unwrap(); + let expected = schema_with_single_annotation(); + assert_eq!(actual, expected); +} + +const SCHEMA_WITH_SINGLE_ANNOTATION_WITHOUT_DECL: &str = r####" + + + Schema for XML Schema. + + +"####; + +fn schema_with_single_annotation_no_decl() -> Schema { + Schema { + sequence: vec![SchemaEntry::Annotation(Annotation {})], + } +} + +#[test] +fn schema_with_single_annotation_no_decl_deserialize() { + let actual: Schema = + quick_xml_deserialize_test(SCHEMA_WITH_SINGLE_ANNOTATION_WITHOUT_DECL).unwrap(); + let expected = schema_with_single_annotation_no_decl(); + assert_eq!(actual, expected); +} + +#[test] +fn ignored_any_deserialize() { + let actual: IgnoredAny = + quick_xml_deserialize_test(SCHEMA_WITH_SINGLE_ANNOTATION_WITHOUT_DECL).unwrap(); + assert_eq!(actual, IgnoredAny); +} + +const SINGLE_ATTRIBUTE: &str = r####" + +"####; + +fn single_attribute() -> Attribute { + Attribute { + name: Some(Name("targetNamespace".to_string())), + attr_type: Some(Type("xs:anyURI".to_string())), + attr_use: None, + attr_default: None, + attr_ref: None, + } +} + +#[test] +fn single_attribute_deserialize() { + let actual: Attribute = quick_xml_deserialize_test(SINGLE_ATTRIBUTE).unwrap(); + let expected = single_attribute(); + assert_eq!(actual, expected); +} + +const MULTIPLE_ATTRIBUTES: &str = r####" + + + + + + + + +"####; + +fn multiple_attribute() -> Vec { + vec![ + Attribute { + name: Some(Name("targetNamespace".to_string())), + attr_type: Some(Type("xs:anyURI".to_string())), + attr_use: None, + attr_default: None, + attr_ref: None, + }, + Attribute { + name: Some(Name("version".to_string())), + attr_type: Some(Type("xs:token".to_string())), + attr_use: None, + attr_default: None, + attr_ref: None, + }, + Attribute { + name: Some(Name("finalDefault".to_string())), + attr_type: Some(Type("xs:fullDerivationSet".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("".to_string())), + attr_ref: None, + }, + Attribute { + name: Some(Name("blockDefault".to_string())), + attr_type: Some(Type("xs:blockSet".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("".to_string())), + attr_ref: None, + }, + Attribute { + name: Some(Name("attributeFormDefault".to_string())), + attr_type: Some(Type("xs:formChoice".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("unqualified".to_string())), + attr_ref: None, + }, + Attribute { + name: Some(Name("elementFormDefault".to_string())), + attr_type: Some(Type("xs:formChoice".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("unqualified".to_string())), + attr_ref: None, + }, + Attribute { + name: Some(Name("id".to_string())), + attr_type: Some(Type("xs:ID".to_string())), + attr_use: None, + attr_default: None, + attr_ref: None, + }, + Attribute { + name: None, + attr_type: None, + attr_use: None, + attr_default: None, + attr_ref: Some(Ref("xml:lang".to_string())), + }, + ] +} + +#[test] +fn multiple_attribute_deserialize() { + let actual: Vec = quick_xml_deserialize_test(MULTIPLE_ATTRIBUTES).unwrap(); + let expected = multiple_attribute(); + assert_eq!(actual, expected); +} + +#[test] +fn multiple_attribute_wrapped_deserialize() { + let actual: Vec = quick_xml_deserialize_test(MULTIPLE_ATTRIBUTES).unwrap(); + let expected: Vec = multiple_attribute() + .into_iter() + .map(ExtensionEntry::Attribute) + .collect(); + assert_eq!(actual, expected); +} + +const SINGLE_CONTENT: &str = r####" + + + + + + + + + + + + + + + + + + + + + + + + +"####; + +fn single_content() -> ComplexContent { + ComplexContent { + content: Extension { + base: Base("xs:openAttrs".to_string()), + entries: vec![ + ExtensionEntry::Sequence(Sequence {}), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("targetNamespace".to_string())), + attr_type: Some(Type("xs:anyURI".to_string())), + attr_use: None, + attr_default: None, + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("version".to_string())), + attr_type: Some(Type("xs:token".to_string())), + attr_use: None, + attr_default: None, + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("finalDefault".to_string())), + attr_type: Some(Type("xs:fullDerivationSet".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("".to_string())), + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("blockDefault".to_string())), + attr_type: Some(Type("xs:blockSet".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("".to_string())), + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("attributeFormDefault".to_string())), + attr_type: Some(Type("xs:formChoice".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("unqualified".to_string())), + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("elementFormDefault".to_string())), + attr_type: Some(Type("xs:formChoice".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("unqualified".to_string())), + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("id".to_string())), + attr_type: Some(Type("xs:ID".to_string())), + attr_use: None, + attr_default: None, + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: None, + attr_type: None, + attr_use: None, + attr_default: None, + attr_ref: Some(Ref("xml:lang".to_string())), + }), + ], + }, + } +} + +#[test] +fn single_content_deserialize() { + let actual: ComplexContent = quick_xml_deserialize_test(SINGLE_CONTENT).unwrap(); + let expected = single_content(); + assert_eq!(actual, expected); +} + +const SINGLE_ELEMENT: &str = r####" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"####; + +fn single_element() -> Element { + Element { + name: Name("schema".to_string()), + id: Id("schema".to_string()), + annotation: Annotation {}, + complex_type: ComplexType { + complex_content: ComplexContent { + content: Extension { + base: Base("xs:openAttrs".to_string()), + entries: vec![ + ExtensionEntry::Sequence(Sequence {}), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("targetNamespace".to_string())), + attr_type: Some(Type("xs:anyURI".to_string())), + attr_use: None, + attr_default: None, + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("version".to_string())), + attr_type: Some(Type("xs:token".to_string())), + attr_use: None, + attr_default: None, + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("finalDefault".to_string())), + attr_type: Some(Type("xs:fullDerivationSet".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("".to_string())), + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("blockDefault".to_string())), + attr_type: Some(Type("xs:blockSet".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("".to_string())), + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("attributeFormDefault".to_string())), + attr_type: Some(Type("xs:formChoice".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("unqualified".to_string())), + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("elementFormDefault".to_string())), + attr_type: Some(Type("xs:formChoice".to_string())), + attr_use: Some(Use("optional".to_string())), + attr_default: Some(Default("unqualified".to_string())), + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: Some(Name("id".to_string())), + attr_type: Some(Type("xs:ID".to_string())), + attr_use: None, + attr_default: None, + attr_ref: None, + }), + ExtensionEntry::Attribute(Attribute { + name: None, + attr_type: None, + attr_use: None, + attr_default: None, + attr_ref: Some(Ref("xml:lang".to_string())), + }), + ], + }, + }, + }, + } +} + +#[test] +fn single_element_deserialize() { + let actual: Element = quick_xml_deserialize_test(SINGLE_ELEMENT).unwrap(); + let expected = single_element(); + assert_eq!(actual, expected); +} + +const SCHEMA_WITH_SINGLE_ELEMENT: &str = r####" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"####; + +fn schema_with_single_element() -> XmlRoot { + XmlRoot::new() + .with_decl(XmlDecl::new("1.0", Some("UTF-8"), None)) + .with_element(Schema { + sequence: vec![SchemaEntry::Element(single_element())], + }) +} + +#[test] +fn schema_with_single_element_deserialize() { + let actual: XmlRoot = quick_xml_deserialize_test(SCHEMA_WITH_SINGLE_ELEMENT).unwrap(); + let expected = schema_with_single_element(); + assert_eq!(actual, expected); +} From aee7a5bbfd4197b76faa913e2f682177617b3caf Mon Sep 17 00:00:00 2001 From: Lukas Friman Date: Wed, 7 May 2025 10:21:35 +0200 Subject: [PATCH 2/2] Continued rework on both Deserializer and Serializer. --- xmlity-xml-rs/src/de.rs | 116 ++++---- xmlity-xml-rs/src/lib.rs | 44 +++- xmlity-xml-rs/src/ser.rs | 555 +++++++++++++++++++++------------------ 3 files changed, 378 insertions(+), 337 deletions(-) diff --git a/xmlity-xml-rs/src/de.rs b/xmlity-xml-rs/src/de.rs index e3b9dad..c0fc65b 100644 --- a/xmlity-xml-rs/src/de.rs +++ b/xmlity-xml-rs/src/de.rs @@ -1,16 +1,17 @@ use std::{io::Read, ops::Deref}; use xml::{ + attribute::OwnedAttribute, name::OwnedName, reader::{EventReader, XmlEvent}, }; use xmlity::{ de::{self, Error as _, Unexpected, Visitor}, - Deserialize, ExpandedName, LocalName, QName, XmlNamespace, + Deserialize, ExpandedName, QName, XmlNamespace, }; -use crate::HasXmlRsAlternative; +use crate::{IsExpandedName, IsQName}; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -114,10 +115,10 @@ impl Deserializer { } fn read_event(&mut self) -> Result, Error> { - while let Ok(event) = self.reader.read_event() { + while let Ok(event) = self.reader.next() { match event { XmlEvent::EndDocument => return Ok(None), - XmlEvent::Characters(text) if text.clone().into_inner().trim_ascii().is_empty() => { + XmlEvent::Characters(text) if text.trim_ascii().is_empty() => { continue; } event => return Ok(Some(event)), @@ -127,10 +128,10 @@ impl Deserializer { Ok(None) } - fn read_until_element_end(&mut self, name: &QuickName, depth: i16) -> Result<(), Error> { + fn read_until_element_end(&mut self, name: &OwnedName, depth: i16) -> Result<(), Error> { while let Some(event) = self.peek_event() { let correct_name = match event { - XmlEvent::EndElement { name: end_name } if end_name == *name => true, + XmlEvent::EndElement { name: end_name } if *end_name == *name => true, XmlEvent::EndDocument => return Err(Error::Unexpected(Unexpected::Eof)), _ => false, }; @@ -145,7 +146,7 @@ impl Deserializer { Err(Error::Unexpected(de::Unexpected::Eof)) } - pub fn peek_event(&mut self) -> Option<&XmlEvent> { + fn peek_event(&mut self) -> Option<&XmlEvent> { if self.peeked_event.is_some() { return self.peeked_event.as_ref(); } @@ -154,7 +155,7 @@ impl Deserializer { self.peeked_event.as_ref() } - pub fn next_event(&mut self) -> Option { + fn next_event(&mut self) -> Option { let event = if self.peeked_event.is_some() { self.peeked_event.take() } else { @@ -171,14 +172,14 @@ impl Deserializer { event } - pub fn create_sub_seq_access<'p>(&'p mut self) -> SubSeqAccess<'p, R> { + fn create_sub_seq_access<'p>(&'p mut self) -> SubSeqAccess<'p, R> { SubSeqAccess::Filled { current: Some(self.clone()), parent: self, } } - pub fn try_deserialize( + fn try_deserialize( &mut self, closure: impl for<'a> FnOnce(&'a mut Deserializer) -> Result, ) -> Result { @@ -190,26 +191,12 @@ impl Deserializer { } res } - - pub fn expand_name<'a>(&self, qname: QuickName<'a>) -> ExpandedName<'a> { - let (resolve_result, _) = self.reader.resolve(qname, false); - let namespace = xml_namespace_from_resolve_result(resolve_result).map(|ns| ns.into_owned()); - - ExpandedName::new(LocalName::from_quick_xml(qname.local_name()), namespace) - } - - pub fn resolve_bytes_start<'a>(&self, bytes_start: &'a BytesStart<'a>) -> ExpandedName<'a> { - self.expand_name(bytes_start.name()) - } - - pub fn resolve_attribute<'a>(&self, attribute: &'a Attribute<'a>) -> ExpandedName<'a> { - self.expand_name(attribute.key) - } } pub struct ElementAccess<'a, R: Read> { deserializer: Option<&'a mut Deserializer>, attribute_index: usize, + attributes: Vec, start_name: OwnedName, start_depth: i16, empty: bool, @@ -234,8 +221,7 @@ impl ElementAccess<'_, R> { } if let Some(deserializer) = self.deserializer.as_mut() { - deserializer - .read_until_element_end(&self.start_name.into_xmlity(), self.start_depth)?; + deserializer.read_until_element_end(&self.start_name, self.start_depth)?; } Ok(()) } @@ -246,7 +232,7 @@ pub struct AttributeAccess<'a> { value: String, } -impl<'a> de::AttributeAccess<'a> for AttributeAccess<'a> { +impl<'de> de::AttributeAccess<'de> for AttributeAccess<'_> { type Error = Error; fn name(&self) -> ExpandedName<'_> { @@ -291,12 +277,12 @@ struct AttributeDeserializer<'a> { value: String, } -impl<'a> xmlity::Deserializer<'a> for AttributeDeserializer<'a> { +impl<'de> xmlity::Deserializer<'de> for AttributeDeserializer<'_> { type Error = Error; fn deserialize_any(self, visitor: V) -> Result where - V: Visitor<'a>, + V: Visitor<'de>, { visitor.visit_attribute(AttributeAccess { name: self.name, @@ -306,38 +292,35 @@ impl<'a> xmlity::Deserializer<'a> for AttributeDeserializer<'a> { fn deserialize_seq(self, _: V) -> Result where - V: Visitor<'a>, + V: Visitor<'de>, { Err(Self::Error::Unexpected(de::Unexpected::Seq)) } } -pub struct SubAttributesAccess<'a, 'r, R: Read + 'r> { +pub struct SubAttributesAccess<'a, R: Read> { deserializer: &'a Deserializer, - bytes_start: &'a BytesStart<'r>, + attributes: &'a [OwnedAttribute], attribute_index: usize, write_attribute_to: &'a mut usize, } -impl Drop for SubAttributesAccess<'_, '_, R> { +impl Drop for SubAttributesAccess<'_, R> { fn drop(&mut self) { *self.write_attribute_to = self.attribute_index; } } -fn next_attribute<'a, 'de, T: Deserialize<'de>, R: Read>( - deserializer: &'a Deserializer, - bytes_start: &'a BytesStart<'_>, +fn next_attribute<'a, 'de, T: Deserialize<'de>>( + attributes: &[OwnedAttribute], attribute_index: &'a mut usize, ) -> Result, Error> { let (attribute, key) = loop { - let Some(attribute) = bytes_start.attributes().nth(*attribute_index) else { + let Some(attribute) = attributes.get(*attribute_index) else { return Ok(None); }; - let attribute = attribute?; - - let key = deserializer.resolve_attribute(&attribute).into_owned(); + let key = (&attribute.name).into_expanded_name(); const XMLNS_NAMESPACE: XmlNamespace<'static> = XmlNamespace::new_dangerous("http://www.w3.org/2000/xmlns/"); @@ -350,10 +333,10 @@ fn next_attribute<'a, 'de, T: Deserialize<'de>, R: Read>( break (attribute, key); }; - let value = String::from_utf8(attribute.value.into_owned()) - .expect("attribute value should be valid utf8"); - - let deserializer = AttributeDeserializer { name: key, value }; + let deserializer = AttributeDeserializer { + name: key, + value: attribute.value.clone(), + }; let res = T::deserialize(deserializer)?; @@ -363,11 +346,11 @@ fn next_attribute<'a, 'de, T: Deserialize<'de>, R: Read>( Ok(Some(res)) } -impl<'de, R: Read> de::AttributesAccess<'de> for SubAttributesAccess<'_, 'de, R> { +impl<'de, R: Read + 'de> de::AttributesAccess<'de> for SubAttributesAccess<'_, R> { type Error = Error; type SubAccess<'a> - = SubAttributesAccess<'a, 'de, R> + = SubAttributesAccess<'a, R> where Self: 'a; @@ -376,8 +359,8 @@ impl<'de, R: Read> de::AttributesAccess<'de> for SubAttributesAccess<'_, 'de, R> T: Deserialize<'de>, { next_attribute( - self.deserializer, - self.bytes_start, + // self.deserializer, + self.attributes, &mut self.attribute_index, ) } @@ -385,18 +368,18 @@ impl<'de, R: Read> de::AttributesAccess<'de> for SubAttributesAccess<'_, 'de, R> fn sub_access(&mut self) -> Result, Self::Error> { Ok(Self::SubAccess { deserializer: self.deserializer, - bytes_start: self.bytes_start, + attributes: self.attributes, attribute_index: self.attribute_index, write_attribute_to: self.write_attribute_to, }) } } -impl<'de> de::AttributesAccess<'de> for ElementAccess<'_, 'de> { +impl<'de, R: Read + 'de> de::AttributesAccess<'de> for ElementAccess<'_, R> { type Error = Error; type SubAccess<'a> - = SubAttributesAccess<'a, 'de> + = SubAttributesAccess<'a, R> where Self: 'a; @@ -404,18 +387,12 @@ impl<'de> de::AttributesAccess<'de> for ElementAccess<'_, 'de> { where T: Deserialize<'de>, { - next_attribute( - self.deserializer - .as_ref() - .expect("deserializer should be set"), - &self.bytes_start, - &mut self.attribute_index, - ) + next_attribute(&self.attributes, &mut self.attribute_index) } fn sub_access(&mut self) -> Result, Self::Error> { Ok(Self::SubAccess { - bytes_start: &self.bytes_start, + attributes: &self.attributes, attribute_index: self.attribute_index, write_attribute_to: &mut self.attribute_index, deserializer: self @@ -426,11 +403,11 @@ impl<'de> de::AttributesAccess<'de> for ElementAccess<'_, 'de> { } } -impl<'a, 'de, R: Read> de::ElementAccess<'de> for ElementAccess<'a, 'de, R> { +impl<'a, 'de, R: Read + 'de> de::ElementAccess<'de> for ElementAccess<'a, R> { type ChildrenAccess = ChildrenAccess<'a, R>; fn name(&self) -> ExpandedName<'_> { - self.deserializer().resolve_bytes_start(&self.bytes_start) + (&self.start_name).into_expanded_name() } fn children(mut self) -> Result { @@ -443,7 +420,7 @@ impl<'a, 'de, R: Read> de::ElementAccess<'de> for ElementAccess<'a, 'de, R> { .expect("Should not be called after ElementAccess has been consumed"); ChildrenAccess::Filled { - expected_end: QName::from_quick_xml(self.bytes_start.name()).into_owned(), + expected_end: self.start_name.clone(), start_depth: self.start_depth, deserializer, } @@ -453,7 +430,7 @@ impl<'a, 'de, R: Read> de::ElementAccess<'de> for ElementAccess<'a, 'de, R> { pub enum ChildrenAccess<'a, R: Read> { Filled { - expected_end: QName<'static>, + expected_end: OwnedName, deserializer: &'a mut Deserializer, start_depth: i16, }, @@ -472,7 +449,7 @@ impl Drop for ChildrenAccess<'_, R> { }; deserializer - .read_until_element_end(&expected_end.into_xmlity(), *start_depth) + .read_until_element_end(&expected_end, *start_depth) .unwrap(); } } @@ -505,11 +482,11 @@ impl<'r, R: Read + 'r> de::SeqAccess<'r> for ChildrenAccess<'_, R> { let current_depth = deserializer.current_depth; if let Some(XmlEvent::EndElement { name: end_name }) = deserializer.peek_event() { - if end_name.into_xmlity() != *expected_end && current_depth == *start_depth { + if end_name != expected_end && current_depth == *start_depth { return Err(Error::custom(format!( "Expected end of element {}, found end of element {}", expected_end, - end_name.into_xmlity() + end_name.into_qname() ))); } @@ -541,11 +518,11 @@ impl<'r, R: Read + 'r> de::SeqAccess<'r> for ChildrenAccess<'_, R> { let current_depth = deserializer.current_depth; if let Some(XmlEvent::EndElement { name: end_name }) = deserializer.peek_event() { - if *expected_end != end_name.into_xmlity() && current_depth == *start_depth { + if *expected_end != *end_name && current_depth == *start_depth { return Err(Error::custom(format!( "Expected end of element {}, found end of element {}", expected_end, - end_name.into_xmlity() + end_name.into_qname() ))); } @@ -706,6 +683,7 @@ impl<'r, R: Read + 'r> xmlity::Deserializer<'r> for &mut Deserializer { visitor, ElementAccess { start_name: name, + attributes, start_depth: self.current_depth, deserializer: Some(self), empty: false, diff --git a/xmlity-xml-rs/src/lib.rs b/xmlity-xml-rs/src/lib.rs index 43d2044..9b452ae 100644 --- a/xmlity-xml-rs/src/lib.rs +++ b/xmlity-xml-rs/src/lib.rs @@ -18,16 +18,16 @@ pub use de::Deserializer; pub use ser::{to_string, Serializer}; use xml::name::OwnedName; -pub trait HasXmlRsAlternative { +pub trait IsQName { type XmlityEquivalent; - fn into_xmlity(self) -> Self::XmlityEquivalent; + fn into_qname(self) -> Self::XmlityEquivalent; } -impl HasXmlRsAlternative for OwnedName { +impl IsQName for OwnedName { type XmlityEquivalent = QName<'static>; - fn into_xmlity(self) -> Self::XmlityEquivalent { + fn into_qname(self) -> Self::XmlityEquivalent { QName::new( self.prefix .map(|prefix| Prefix::new(prefix).expect("A xml-rs prefix should be valid")), @@ -36,10 +36,10 @@ impl HasXmlRsAlternative for OwnedName { } } -impl<'a> HasXmlRsAlternative for &'a OwnedName { +impl<'a> IsQName for &'a OwnedName { type XmlityEquivalent = QName<'a>; - fn into_xmlity(self) -> Self::XmlityEquivalent { + fn into_qname(self) -> Self::XmlityEquivalent { QName::new( self.prefix .as_deref() @@ -49,6 +49,38 @@ impl<'a> HasXmlRsAlternative for &'a OwnedName { } } +pub trait IsExpandedName { + type XmlityEquivalent; + + fn into_expanded_name(self) -> Self::XmlityEquivalent; +} + +impl IsExpandedName for OwnedName { + type XmlityEquivalent = ExpandedName<'static>; + + fn into_expanded_name(self) -> Self::XmlityEquivalent { + ExpandedName::new( + LocalName::new(self.local_name).expect("An xml-rs local name should be valid"), + self.namespace.map(|namespace| { + XmlNamespace::new(namespace).expect("A xml-rs namespace should be valid") + }), + ) + } +} + +impl<'a> IsExpandedName for &'a OwnedName { + type XmlityEquivalent = ExpandedName<'a>; + + fn into_expanded_name(self) -> Self::XmlityEquivalent { + ExpandedName::new( + LocalName::new(self.local_name.as_str()).expect("An xml-rs local name should be valid"), + self.namespace.as_deref().map(|namespace| { + XmlNamespace::new(namespace).expect("A xml-rs namespace should be valid") + }), + ) + } +} + #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Attribute<'a> { pub name: ExpandedName<'a>, diff --git a/xmlity-xml-rs/src/ser.rs b/xmlity-xml-rs/src/ser.rs index eba6b38..601f6e4 100644 --- a/xmlity-xml-rs/src/ser.rs +++ b/xmlity-xml-rs/src/ser.rs @@ -1,22 +1,33 @@ use core::str; +use std::borrow::Cow; use std::collections::BTreeMap; use std::io::Write; +use std::ops::DerefMut; +use xml::attribute::{Attribute, OwnedAttribute}; +use xml::common::XmlVersion; +use xml::name::OwnedName; use xml::writer::{EventWriter, XmlEvent}; +use xml::EmitterConfig; use xmlity::ser::IncludePrefix; -use xmlity::{ser, ExpandedName, Prefix, QName, Serialize, XmlNamespace}; +use xmlity::{ser, ExpandedName, LocalName, Prefix, QName, Serialize, XmlNamespace}; -use crate::{declaration_into_attribute, Attribute, XmlnsDeclaration}; +use crate::XmlnsDeclaration; +/// Errors that can occur when using this crate. #[derive(Debug, thiserror::Error)] pub enum Error { + /// Error from the `xml` crate. #[error("Quick XML error: {0}")] XmlRs(#[from] xml::writer::Error), - #[error("Unexpected: {0}")] - Unexpected(xmlity::de::Unexpected), + /// IO errors. + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + /// Custom errors from [`Serialize`] implementations. #[error("Custom: {0}")] Custom(String), + /// Invalid UTF-8 when serializing. #[error("Invalid UTF-8: {0}")] InvalidUtf8(#[from] std::string::FromUtf8Error), } @@ -38,6 +49,7 @@ where String::from_utf8(bytes).map_err(Error::InvalidUtf8) } +/// Serialize a value into a string. pub fn to_string(value: &T) -> Result where T: Serialize, @@ -45,11 +57,21 @@ where serializer_to_string(EventWriter::new(Vec::new()), value) } +/// Serialize a value into a string with pretty printing. pub fn to_string_pretty(value: &T, indentation: usize) -> Result where T: Serialize, { - todo!() + serializer_to_string( + EventWriter::new_with_config( + Vec::new(), + EmitterConfig { + indent_string: Cow::Owned((0..indentation).map(|_| ' ').collect::()), + ..Default::default() + }, + ), + value, + ) } struct NamespaceScope<'a> { @@ -91,6 +113,14 @@ struct PrefixGenerator { impl PrefixGenerator { pub fn index_to_name(index: usize) -> Prefix<'static> { + // 0 = a0 + // 1 = a1 + // 26 = b0 + // 27 = b1 + // 52 = c0 + // 53 = c1 + // ... + let letter = (index / 26) as u8 + b'a'; let number = (index % 26) as u8 + b'0'; let mut name = String::with_capacity(2); @@ -136,7 +166,7 @@ impl<'a> NamespaceScopeContainer<'a> { /// Find matching prefix pub fn find_matching_namespace<'b>( &'b self, - namespace: &'b XmlNamespace<'b>, + namespace: &'_ XmlNamespace<'_>, ) -> Option<&'b Prefix<'a>> { self.scopes.iter().rev().find_map(|a| { a.defined_namespaces @@ -149,17 +179,16 @@ impl<'a> NamespaceScopeContainer<'a> { /// This function takes in a namespace and tries to resolve it in different ways depending on the options provided. Unless `always_declare` is true, it will try to use an existing declaration. Otherwise, or if the namespace has not yet been declared, it will provide a declaration. pub fn resolve_namespace<'b>( &'b mut self, - namespace: &'b XmlNamespace<'b>, + namespace: &'_ XmlNamespace<'b>, preferred_prefix: Option<&'b Prefix<'b>>, always_declare: IncludePrefix, - ) -> (Prefix<'b>, Option>) { - let existing_prefix = self - .find_matching_namespace(namespace) - // If we should always declare, we simply pretend it's not declared yet. - .filter(|_p| always_declare != IncludePrefix::Always); - - if let Some(existing_prefix) = existing_prefix { - return (existing_prefix.clone(), None); + ) -> (Prefix<'a>, Option>) { + if always_declare != IncludePrefix::Always { + let existing_prefix = self.find_matching_namespace(namespace); + + if let Some(existing_prefix) = existing_prefix { + return (existing_prefix.clone(), None); + } } // If the namespace is not declared, use the specifically requested preferred prefix... @@ -174,48 +203,68 @@ impl<'a> NamespaceScopeContainer<'a> { }) .cloned() // If the preferred namespace prefix is not available, use a random prefix. - .unwrap_or_else(|| self.prefix_generator.new_prefix()); + .unwrap_or_else(|| self.prefix_generator.new_prefix()) + .into_owned(); - let xmlns = XmlnsDeclaration::new(prefix.clone(), namespace.clone()); + let scope = self + .scopes + .last_mut() + .expect("There should be at least one scope"); - self.scopes.last_mut().map(|a| { - a.defined_namespaces - .insert(prefix.clone().into_owned(), namespace.clone().into_owned()) - }); + scope + .defined_namespaces + .insert(prefix.clone(), namespace.clone().into_owned()); + + let (prefix, namespace) = scope + .defined_namespaces + .get_key_value(&prefix) + .expect("The namespace should be defined as it was just added"); + + let xmlns = XmlnsDeclaration::new(prefix.clone(), namespace.clone()); - (prefix, Some(xmlns)) + (prefix.clone(), Some(xmlns)) } - pub fn resolve_name<'b>( - &mut self, - name: ExpandedName<'b>, - preferred_prefix: Option<&Prefix<'b>>, + pub fn resolve_name<'c>( + &'c mut self, + local_name: LocalName<'c>, + namespace: &Option>, + preferred_prefix: Option<&'c Prefix<'c>>, always_declare: IncludePrefix, - ) -> (QName<'b>, Option>) { - let (prefix, declaration) = name - .namespace() + ) -> (QName<'a>, Option>) { + let (prefix, declaration) = namespace + .as_ref() .map(|namespace| self.resolve_namespace(namespace, preferred_prefix, always_declare)) .unzip(); - let declaration = declaration.flatten().map(|a| a.into_owned()); - let resolved_prefix = prefix.map(|a| a.into_owned()); + let declaration = declaration.flatten(); - let name = name.to_q_name(resolved_prefix); - (name.into_owned(), declaration) + let name = QName::new(prefix, local_name.into_owned()); + (name, declaration) } } +struct Element { + name: OwnedName, + attributes: Vec, +} + +/// The [`xmlity::Deserializer`] for the `quick-xml` crate. pub struct Serializer { writer: EventWriter, preferred_namespace_prefixes: BTreeMap, Prefix<'static>>, namespace_scopes: NamespaceScopeContainer<'static>, + buffered_bytes_start: Element, + buffered_bytes_start_empty: bool, } impl Serializer { + /// Create a new serializer. pub fn new(writer: EventWriter) -> Self { Self::new_with_namespaces(writer, BTreeMap::new()) } + /// Create a new serializer with preferred namespace prefixes. pub fn new_with_namespaces( writer: EventWriter, preferred_namespace_prefixes: BTreeMap, Prefix<'static>>, @@ -224,44 +273,42 @@ impl Serializer { writer, preferred_namespace_prefixes, namespace_scopes: NamespaceScopeContainer::new(), + buffered_bytes_start: Element { + name: OwnedName::local(""), + attributes: Vec::new(), + }, + buffered_bytes_start_empty: true, } } + /// Consume the serializer and return the underlying writer. pub fn into_inner(self) -> W { self.writer.into_inner() } - pub fn push_namespace_scope(&mut self) { + fn push_namespace_scope(&mut self) { self.namespace_scopes.push_scope() } - pub fn pop_namespace_scope(&mut self) { + fn pop_namespace_scope(&mut self) { self.namespace_scopes.pop_scope(); } - pub fn add_preferred_prefix( - &mut self, - namespace: XmlNamespace<'static>, - prefix: Prefix<'static>, - ) { - self.preferred_namespace_prefixes.insert(namespace, prefix); - } - - pub fn resolve_name<'b>( + fn resolve_name<'b>( &mut self, name: ExpandedName<'b>, preferred_prefix: Option<&Prefix<'b>>, always_declare: IncludePrefix, - ) -> (QName<'b>, Option>) { - let name2 = name.clone(); - let preferred_prefix = preferred_prefix.or_else(|| { - name2 - .namespace() - .and_then(|a| self.preferred_namespace_prefixes.get(a)) - }); + ) -> (QName<'static>, Option>) { + let (local_name, namespace) = name.into_parts(); + + let namespace_ref = namespace.as_ref(); + + let preferred_prefix = preferred_prefix + .or_else(|| namespace_ref.and_then(|a| self.preferred_namespace_prefixes.get(a))); self.namespace_scopes - .resolve_name(name, preferred_prefix, always_declare) + .resolve_name(local_name, &namespace, preferred_prefix, always_declare) } } @@ -277,22 +324,36 @@ impl From for Serializer { } } +/// The main element serializer for the `quick-xml` crate. pub struct SerializeElement<'s, W: Write> { serializer: &'s mut Serializer, name: ExpandedName<'static>, - attributes: Vec>, + include_prefix: IncludePrefix, preferred_prefix: Option>, - enforce_prefix: IncludePrefix, } -pub struct AttributeSerializer<'t> { +impl<'s, W: Write> SerializeElement<'s, W> { + fn resolve_name_or_declare<'a>( + name: ExpandedName<'a>, + preferred_prefix: Option<&Prefix<'a>>, + enforce_prefix: IncludePrefix, + serializer: &mut Serializer, + ) -> (QName<'a>, Option>) { + let (qname, decl) = serializer.resolve_name(name, preferred_prefix, enforce_prefix); + + (qname, decl) + } +} + +/// The attribute serializer for the `quick-xml` crate. +pub struct AttributeSerializer<'t, W: Write> { name: ExpandedName<'static>, - on_end_add_to: &'t mut Vec>, + serializer: &'t mut Serializer, preferred_prefix: Option>, enforce_prefix: IncludePrefix, } -impl ser::SerializeAttributeAccess for AttributeSerializer<'_> { +impl ser::SerializeAttributeAccess for AttributeSerializer<'_, W> { type Ok = (); type Error = Error; @@ -310,27 +371,29 @@ impl ser::SerializeAttributeAccess for AttributeSerializer<'_> { } fn end>(self, value: S) -> Result { - self.on_end_add_to.push(Attribute { - name: self.name.into_owned(), - value: value.as_ref().to_owned(), - preferred_prefix: self.preferred_prefix, - enforce_prefix: self.enforce_prefix, - }); + let (qname, decl) = SerializeElement::resolve_name_or_declare( + self.name, + None, + IncludePrefix::default(), + self.serializer, + ); + + if let Some(decl) = decl { + self.serializer.push_decl_attr(decl); + } + + self.serializer.push_attr(qname, value.as_ref()); Ok(()) } } -pub struct AttributeVecSerializer<'t> { - attributes: &'t mut Vec>, -} - -impl ser::AttributeSerializer for AttributeVecSerializer<'_> { +impl<'t, W: Write> ser::AttributeSerializer for &mut SerializeElementAttributes<'t, W> { type Error = Error; type Ok = (); type SerializeAttribute<'a> - = AttributeSerializer<'a> + = AttributeSerializer<'a, W> where Self: 'a; @@ -340,7 +403,7 @@ impl ser::AttributeSerializer for AttributeVecSerializer<'_> { ) -> Result, Self::Error> { Ok(Self::SerializeAttribute { name: name.clone().into_owned(), - on_end_add_to: &mut self.attributes, + serializer: self.serializer.deref_mut(), preferred_prefix: None, enforce_prefix: IncludePrefix::default(), }) @@ -352,76 +415,75 @@ impl ser::AttributeSerializer for AttributeVecSerializer<'_> { } impl<'s, W: Write> SerializeElement<'s, W> { - fn finish_start(self) -> (OwnedBytesStart, QName<'static>, &'s mut Serializer) { + fn finish_start(self) -> (QName<'static>, &'s mut Serializer) { let Self { - serializer, name, - attributes, - enforce_prefix, + include_prefix, preferred_prefix, + serializer, } = self; - let mut resolve_name_or_declare = - |name: &ExpandedName<'_>, - preferred_prefix: Option<&Prefix<'_>>, - enforce_prefix: IncludePrefix| - -> (QName<'static>, Option>) { - let (qname, decl) = - serializer.resolve_name(name.clone(), preferred_prefix, enforce_prefix); + assert!( + serializer.buffered_bytes_start_empty, + "Should have been emptied by the serializer" + ); - (qname.into_owned(), decl.map(|a| a.into_owned())) - }; + serializer.buffered_bytes_start.attributes.clear(); - let (elem_qname, elem_name_decl) = - resolve_name_or_declare(&name, preferred_prefix.as_ref(), enforce_prefix); + let (qname, decl) = SerializeElement::resolve_name_or_declare( + name.clone(), + preferred_prefix.as_ref(), + include_prefix, + serializer, + ); + serializer.buffered_bytes_start.name = todo!(); - let (attr_prefixes, attr_decls): (Vec<_>, Vec<_>) = attributes - .iter() - .map(|a| &a.name) - .map(|name| resolve_name_or_declare(name, None, IncludePrefix::default())) - .unzip(); + if let Some(decl) = decl { + serializer.push_decl_attr(decl); + } + serializer.buffered_bytes_start_empty = false; - let decls = elem_name_decl - .into_iter() - .chain(attr_decls.into_iter().flatten()) - .collect::>(); + (qname, serializer) + } - // Add declared namespaces first - let mut q_attributes = decls - .iter() - .map(|decl| declaration_into_attribute(decl.clone())) - .map(|attr| { - ( - OwnedQuickName::new(&attr.name), - attr.value.as_bytes().to_owned(), - ) - }) - .collect::>(); - - // Then add the attributes - q_attributes.extend( - attributes - .into_iter() - .zip(attr_prefixes) - .map(|(attr, qname)| attr.resolve(qname.prefix().cloned())) - .map(|attr| { - ( - OwnedQuickName::new(&attr.name), - attr.value.as_bytes().to_owned(), - ) - }), + fn end_empty(serializer: &mut Serializer) -> Result<(), Error> { + assert!( + !serializer.buffered_bytes_start_empty, + "start should be buffered" ); + let start = &serializer.buffered_bytes_start; + + let name = start.name.borrow(); + let attributes: Vec> = start.attributes.iter().map(|a| a.borrow()).collect(); + let namespace = todo!(); + + serializer + .writer + .write(XmlEvent::StartElement { + name: name.clone(), + attributes: Cow::Borrowed(&attributes), + namespace: Cow::Borrowed(&namespace), + }) + .map_err(Error::XmlRs)?; + + serializer + .writer + .write(XmlEvent::EndElement { name: Some(name) }) + .map_err(Error::XmlRs)?; - let bytes_start = OwnedBytesStart { - name: OwnedQuickName::new(&elem_qname), - attributes: q_attributes, - }; + serializer.buffered_bytes_start_empty = true; - (bytes_start, elem_qname, serializer) + Ok(()) } } -impl ser::SerializeAttributes for SerializeElement<'_, W> { +/// Provides the implementation of [`ser::SerializeElement`] for the `quick-xml` crate. +pub struct SerializeElementAttributes<'s, W: Write> { + serializer: &'s mut Serializer, + end_name: OwnedName, +} + +impl ser::SerializeAttributes for SerializeElementAttributes<'_, W> { type Ok = (); type Error = Error; @@ -429,17 +491,33 @@ impl ser::SerializeAttributes for SerializeElement<'_, W> { &mut self, a: &A, ) -> Result { - a.serialize_attribute(AttributeVecSerializer { - attributes: &mut self.attributes, + a.serialize_attribute(self) + } +} + +impl<'s, W: Write> ser::SerializeElementAttributes for SerializeElementAttributes<'s, W> { + type ChildrenSerializeSeq = ChildrenSerializeSeq<'s, W>; + + fn serialize_children(self) -> Result { + Ok(ChildrenSerializeSeq { + serializer: self.serializer, + end_name: self.end_name, }) } + + fn end(self) -> Result { + SerializeElement::end_empty(self.serializer) + } } impl<'s, W: Write> ser::SerializeElement for SerializeElement<'s, W> { + type Ok = (); + type Error = Error; type ChildrenSerializeSeq = ChildrenSerializeSeq<'s, W>; + type SerializeElementAttributes = SerializeElementAttributes<'s, W>; fn include_prefix(&mut self, should_enforce: IncludePrefix) -> Result { - self.enforce_prefix = should_enforce; + self.include_prefix = should_enforce; Ok(()) } fn preferred_prefix( @@ -450,12 +528,20 @@ impl<'s, W: Write> ser::SerializeElement for SerializeElement<'s, W> { Ok(()) } + fn serialize_attributes(self) -> Result { + self.serializer.push_namespace_scope(); + let (end_name, serializer) = self.finish_start(); + Ok(SerializeElementAttributes { + serializer, + end_name, + }) + } + fn serialize_children(self) -> Result { self.serializer.push_namespace_scope(); - let (bytes_start, end_name, serializer) = self.finish_start(); + let (end_name, serializer) = self.finish_start(); Ok(ChildrenSerializeSeq { - bytes_start: Some(bytes_start), serializer, end_name, }) @@ -463,11 +549,9 @@ impl<'s, W: Write> ser::SerializeElement for SerializeElement<'s, W> { fn end(self) -> Result { self.serializer.push_namespace_scope(); - let (bytes_start, _, serializer) = self.finish_start(); + let (_, serializer) = self.finish_start(); - serializer - .writer - .write(XmlEvent::StartElement(bytes_start.as_quick_xml()))?; + SerializeElement::end_empty(serializer)?; serializer.pop_namespace_scope(); @@ -475,10 +559,10 @@ impl<'s, W: Write> ser::SerializeElement for SerializeElement<'s, W> { } } +///Provides the implementation of `SerializeSeq` trait for element children for the `quick-xml` crate. pub struct ChildrenSerializeSeq<'s, W: Write> { - bytes_start: Option, serializer: &'s mut Serializer, - end_name: QName<'static>, + end_name: OwnedName, } impl ser::SerializeSeq for ChildrenSerializeSeq<'_, W> { @@ -486,29 +570,16 @@ impl ser::SerializeSeq for ChildrenSerializeSeq<'_, W> { type Error = Error; fn serialize_element(&mut self, value: &V) -> Result { - value.serialize(SerializerWithPossibleBytesStart { - serializer: self.serializer, - possible_bytes_start: Some(&mut self.bytes_start), - }) + value.serialize(self.serializer.deref_mut()) } fn end(self) -> Result { - // If we have a bytes_start, then we never wrote the start event, so we need to write an empty element instead. - if let Some(bytes_start) = self.bytes_start { - self.serializer - .writer - .write(XmlEvent::Empty(bytes_start.as_quick_xml())) - .map_err(Error::Io)?; - } else { - let end_name = OwnedQuickName::new(&self.end_name); - - let bytes_end = BytesEnd::from(end_name.as_ref()); - - self.serializer - .writer - .write(XmlEvent::End(bytes_end)) - .map_err(Error::Io)?; - } + self.serializer + .writer + .write(XmlEvent::EndElement { + name: Some(self.end_name.borrow()), + }) + .map_err(Error::XmlRs)?; self.serializer.pop_namespace_scope(); @@ -516,20 +587,17 @@ impl ser::SerializeSeq for ChildrenSerializeSeq<'_, W> { } } -pub struct SerializeSeq<'e, 'b, W: Write> { +/// Provides the implementation of `SerializeSeq` trait for any nodes for the `quick-xml` crate. +pub struct SerializeSeq<'e, W: Write> { serializer: &'e mut Serializer, - possible_bytes_start: Option<&'b mut Option>, } -impl ser::SerializeSeq for SerializeSeq<'_, '_, W> { +impl ser::SerializeSeq for SerializeSeq<'_, W> { type Ok = (); type Error = Error; fn serialize_element(&mut self, v: &V) -> Result { - v.serialize(SerializerWithPossibleBytesStart { - serializer: self.serializer, - possible_bytes_start: self.possible_bytes_start.as_deref_mut(), - }) + v.serialize(self.serializer.deref_mut()) } fn end(self) -> Result { @@ -537,42 +605,74 @@ impl ser::SerializeSeq for SerializeSeq<'_, '_, W> { } } +impl Serializer { + fn try_start(&mut self) -> Result<(), Error> { + if !self.buffered_bytes_start_empty { + self.writer + .write(XmlEvent::StartElement { + name: todo!(), + attributes: todo!(), + namespace: todo!(), + }) + .map_err(Error::XmlRs)?; + + self.buffered_bytes_start_empty = true; + } + Ok(()) + } + + fn push_attr(&mut self, qname: QName<'_>, value: &str) { + self.buffered_bytes_start.attributes.push(OwnedAttribute { + name: todo!(), + value: value.to_owned(), + }); + } + + fn push_decl_attr(&mut self, decl: XmlnsDeclaration<'_>) { + let XmlnsDeclaration { namespace, prefix } = decl; + + let key = XmlnsDeclaration::xmlns_qname(prefix); + + self.push_attr(key, namespace.as_str()); + } +} + impl<'s, W: Write> xmlity::Serializer for &'s mut Serializer { type Ok = (); type Error = Error; type SerializeElement = SerializeElement<'s, W>; - type SerializeSeq = SerializeSeq<'s, 'static, W>; + type SerializeSeq = SerializeSeq<'s, W>; fn serialize_cdata>(self, text: S) -> Result { + self.try_start()?; self.writer - .write(XmlEvent::CData(BytesCData::new(text.as_ref()))) - .map_err(Error::Io) + .write(XmlEvent::CData(text.as_ref())) + .map_err(Error::XmlRs) } fn serialize_text>(self, text: S) -> Result { + self.try_start()?; self.writer - .write(XmlEvent::Text(BytesText::from_escaped(text.as_ref()))) - .map_err(Error::Io) + .write(XmlEvent::Characters(text.as_ref())) + .map_err(Error::XmlRs) } fn serialize_element<'a>( self, name: &'a ExpandedName<'a>, ) -> Result { + self.try_start()?; + Ok(SerializeElement { serializer: self, name: name.clone().into_owned(), - attributes: Vec::new(), + include_prefix: IncludePrefix::default(), preferred_prefix: None, - enforce_prefix: IncludePrefix::default(), }) } fn serialize_seq(self) -> Result { - Ok(SerializeSeq { - serializer: self, - possible_bytes_start: None, - }) + Ok(SerializeSeq { serializer: self }) } fn serialize_decl>( @@ -581,110 +681,41 @@ impl<'s, W: Write> xmlity::Serializer for &'s mut Serializer { encoding: Option, standalone: Option, ) -> Result { + self.try_start()?; self.writer - .write(XmlEvent::Decl(BytesDecl::new( - version.as_ref(), - encoding.as_ref().map(|s| s.as_ref()), - standalone.as_ref().map(|s| s.as_ref()), - ))) - .map_err(Error::Io) + .write(XmlEvent::StartDocument { + version: XmlVersion::Version11, //TODO + encoding: encoding.as_ref().map(|s| s.as_ref()), + standalone: standalone + .as_ref() + .map(|s| s.as_ref().parse()) + .transpose() + .map_err(|e| Error::Custom(format!("Invalid standalone: {e}")))?, + }) + .map_err(Error::XmlRs) } fn serialize_pi>(self, text: S) -> Result { + self.try_start()?; self.writer - .write(XmlEvent::ProcessingInstruction(BytesPI::new( - str::from_utf8(text.as_ref()).unwrap(), - ))) - .map_err(Error::Io) + .write(XmlEvent::ProcessingInstruction { + name: todo!(), + data: todo!(), + }) + .map_err(Error::XmlRs) } fn serialize_comment>(self, text: S) -> Result { + self.try_start()?; self.writer .write(XmlEvent::Comment(str::from_utf8(text.as_ref()).unwrap())) .map_err(Error::XmlRs) } fn serialize_doctype>(self, text: S) -> Result { - todo!() - } - - fn serialize_none(self) -> Result { - Ok(()) - } -} - -pub struct SerializerWithPossibleBytesStart<'a, 'b, W: Write> { - serializer: &'a mut Serializer, - possible_bytes_start: Option<&'b mut Option>, -} - -impl SerializerWithPossibleBytesStart<'_, '_, W> { - pub fn try_start(&mut self) -> Result<(), Error> { - if let Some(bytes_start) = self.possible_bytes_start.take().and_then(Option::take) { - self.serializer - .writer - .write(XmlEvent::Start(bytes_start.as_quick_xml())) - .map_err(Error::Io)?; - } - Ok(()) - } -} - -impl<'s, 'b, W: Write> xmlity::Serializer for SerializerWithPossibleBytesStart<'s, 'b, W> { - type Ok = (); - type Error = Error; - type SerializeElement = SerializeElement<'s, W>; - type SerializeSeq = SerializeSeq<'s, 'b, W>; - - fn serialize_cdata>(mut self, text: S) -> Result { - self.try_start()?; - self.serializer.serialize_cdata(text) - } - - fn serialize_text>(mut self, text: S) -> Result { - self.try_start()?; - self.serializer.serialize_text(text) - } - - fn serialize_element<'a>( - mut self, - name: &'a ExpandedName<'a>, - ) -> Result { - self.try_start()?; - self.serializer.serialize_element(name) - } - - fn serialize_seq(self) -> Result { - Ok(SerializeSeq { - serializer: self.serializer, - possible_bytes_start: self.possible_bytes_start, - }) - } - - fn serialize_decl>( - mut self, - version: S, - encoding: Option, - standalone: Option, - ) -> Result { self.try_start()?; - self.serializer - .serialize_decl(version, encoding, standalone) - } - - fn serialize_pi>(mut self, text: S) -> Result { - self.try_start()?; - self.serializer.serialize_pi(text) - } - - fn serialize_comment>(mut self, text: S) -> Result { - self.try_start()?; - self.serializer.serialize_comment(text) - } - - fn serialize_doctype>(mut self, text: S) -> Result { - self.try_start()?; - self.serializer.serialize_doctype(text) + todo!() + // self.writer.write(todo!()).map_err(Error::XmlRs) } fn serialize_none(self) -> Result {