diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index dc4feb71..33eb429f 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -17,7 +17,7 @@ jobs: language: rust fuzz-seconds: 600 - name: Upload Crash - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() && steps.build.outcome == 'success' with: name: artifacts diff --git a/Cargo.toml b/Cargo.toml index 793324a9..1926aa30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quick-xml" -version = "0.37.2" +version = "0.37.3" description = "High performance xml reader and writer" edition = "2021" @@ -231,6 +231,11 @@ name = "serde-de-seq" required-features = ["serialize"] path = "tests/serde-de-seq.rs" +[[test]] +name = "serde-de-xsi" +required-features = ["serialize"] +path = "tests/serde-de-xsi.rs" + [[test]] name = "serde-se" required-features = ["serialize"] diff --git a/Changelog.md b/Changelog.md index 2e0ee8de..b9673ed5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,13 +15,23 @@ ### New Features -- [#836]: Add `se::to_utf8_io_writer()` helper compatible with `std::io::Write` and restricted to UTF-8 encoding. - ### Bug Fixes ### Misc Changes +## 0.37.3 -- 2025-03-25 + +### New Features + +- [#850]: Add `Attribute::as_bool()` method to get an attribute value as a boolean. +- [#850]: Add `Attributes::has_nil()` method to check if attributes has `xsi:nil` attribute set to `true`. +- [#497]: Handle `xsi:nil` attribute in serde Deserializer to better process optional fields. + +[#497]: https://github.com/tafia/quick-xml/issues/497 +[#850]: https://github.com/tafia/quick-xml/pull/850 + + ## 0.37.2 -- 2024-12-29 ### New Features diff --git a/src/de/map.rs b/src/de/map.rs index 9f2a9876..7165f6a6 100644 --- a/src/de/map.rs +++ b/src/de/map.rs @@ -215,6 +215,21 @@ where has_value_field: fields.contains(&VALUE_KEY), }) } + + /// Determines if subtree started with the specified event shoould be skipped. + /// + /// Used to map elements with `xsi:nil` attribute set to true to `None` in optional contexts. + /// + /// We need to handle two attributes: + /// - on parent element: + /// - on this element: + /// + /// We check parent element too because `xsi:nil` affects only nested elements of the + /// tag where it is defined. We can map structure with fields mapped to attributes to + /// the `` element and set to `None` all its optional elements. + fn should_skip_subtree(&self, start: &BytesStart) -> bool { + self.de.reader.reader.has_nil_attr(&self.start) || self.de.reader.reader.has_nil_attr(start) + } } impl<'de, 'd, R, E> MapAccess<'de> for ElementMapAccess<'de, 'd, R, E> @@ -540,8 +555,14 @@ where where V: Visitor<'de>, { - match self.map.de.peek()? { + // We cannot use result of `peek()` directly because of borrow checker + let _ = self.map.de.peek()?; + match self.map.de.last_peeked() { DeEvent::Text(t) if t.is_empty() => visitor.visit_none(), + DeEvent::Start(start) if self.map.should_skip_subtree(start) => { + self.map.de.skip_next_tree()?; + visitor.visit_none() + } _ => visitor.visit_some(self), } } diff --git a/src/de/mod.rs b/src/de/mod.rs index 484c31b0..5f18e917 100644 --- a/src/de/mod.rs +++ b/src/de/mod.rs @@ -19,6 +19,7 @@ //! - [Optional attributes and elements](#optional-attributes-and-elements) //! - [Choices (`xs:choice` XML Schema type)](#choices-xschoice-xml-schema-type) //! - [Sequences (`xs:all` and `xs:sequence` XML Schema types)](#sequences-xsall-and-xssequence-xml-schema-types) +//! - [Mapping of `xsi:nil`](#mapping-of-xsinil) //! - [Generate Rust types from XML](#generate-rust-types-from-xml) //! - [Composition Rules](#composition-rules) //! - [Enum Representations](#enum-representations) @@ -413,6 +414,13 @@ //! //! //! ``` +//!
+//! +//! NOTE: The behaviour is not symmetric by default. `None` will be serialized as +//! `optional=""`. This behaviour is consistent across serde crates. You should add +//! `#[serde(skip_serializing_if = "Option::is_none")]` attribute to the field to +//! skip `None`s. +//!
//! //! //! @@ -454,9 +462,15 @@ //! When the XML element is present, type `T` will be deserialized from an //! element (which is a string or a multi-mapping -- i.e. mapping which can have //! duplicated keys). -//!
+//!
//! -//! Currently some edge cases exists described in the issue [#497]. +//! NOTE: The behaviour is not symmetric by default. `None` will be serialized as +//! ``. This behaviour is consistent across serde crates. You should add +//! `#[serde(skip_serializing_if = "Option::is_none")]` attribute to the field to +//! skip `None`s. +//! +//! NOTE: Deserializer will automatically handle a [`xsi:nil`] attribute and set field to `None`. +//! For more info see [Mapping of `xsi:nil`](#mapping-of-xsinil). //!
//! //! @@ -1312,6 +1326,65 @@ //! //! //! +//! Mapping of `xsi:nil` +//! ==================== +//! +//! quick-xml supports handling of [`xsi:nil`] special attribute. When field of optional +//! type is mapped to the XML element which have `xsi:nil="true"` set, or if that attribute +//! is placed on parent XML element, the deserializer will call [`Visitor::visit_none`] +//! and skip XML element corresponding to a field. +//! +//! Examples: +//! +//! ``` +//! # use pretty_assertions::assert_eq; +//! # use serde::Deserialize; +//! #[derive(Deserialize, Debug, PartialEq)] +//! struct TypeWithOptionalField { +//! element: Option, +//! } +//! +//! assert_eq!( +//! TypeWithOptionalField { +//! element: None, +//! }, +//! quick_xml::de::from_str(" +//! +//! Content is skiped because of xsi:nil='true' +//! +//! ").unwrap(), +//! ); +//! ``` +//! +//! You can capture attributes from the optional type, because ` xsi:nil="true"` elements can have +//! attributes: +//! ``` +//! # use pretty_assertions::assert_eq; +//! # use serde::Deserialize; +//! #[derive(Deserialize, Debug, PartialEq)] +//! struct TypeWithOptionalField { +//! #[serde(rename = "@attribute")] +//! attribute: usize, +//! +//! element: Option, +//! non_optional: String, +//! } +//! +//! assert_eq!( +//! TypeWithOptionalField { +//! attribute: 42, +//! element: None, +//! non_optional: "Note, that non-optional fields will be deserialized as usual".to_string(), +//! }, +//! quick_xml::de::from_str(" +//! +//! Content is skiped because of xsi:nil='true' +//! Note, that non-optional fields will be deserialized as usual +//! +//! ").unwrap(), +//! ); +//! ``` +//! //! Generate Rust types from XML //! ============================ //! @@ -1820,7 +1893,7 @@ //! [`overlapped-lists`]: ../index.html#overlapped-lists //! [specification]: https://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition //! [`deserialize_with`]: https://serde.rs/field-attrs.html#deserialize_with -//! [#497]: https://github.com/tafia/quick-xml/issues/497 +//! [`xsi:nil`]: https://www.w3.org/TR/xmlschema-1/#xsi_nil //! [`Serializer::serialize_unit_variant`]: serde::Serializer::serialize_unit_variant //! [`Deserializer::deserialize_enum`]: serde::Deserializer::deserialize_enum //! [`SeError::Unsupported`]: crate::errors::serialize::SeError::Unsupported @@ -2016,7 +2089,7 @@ use crate::{ errors::Error, events::{BytesCData, BytesEnd, BytesStart, BytesText, Event}, name::QName, - reader::Reader, + reader::NsReader, utils::CowRef, }; use serde::de::{ @@ -2415,7 +2488,7 @@ where /// # use pretty_assertions::assert_eq; /// use serde::Deserialize; /// use quick_xml::de::Deserializer; - /// use quick_xml::Reader; + /// use quick_xml::NsReader; /// /// #[derive(Deserialize)] /// struct SomeStruct { @@ -2432,7 +2505,7 @@ where /// let err = SomeStruct::deserialize(&mut de); /// assert!(err.is_err()); /// - /// let reader: &Reader<_> = de.get_ref().get_ref(); + /// let reader: &NsReader<_> = de.get_ref().get_ref(); /// /// assert_eq!(reader.error_position(), 28); /// assert_eq!(reader.buffer_position(), 41); @@ -2534,6 +2607,22 @@ where } } + #[inline] + fn last_peeked(&self) -> &DeEvent<'de> { + #[cfg(feature = "overlapped-lists")] + { + self.read + .front() + .expect("`Deserializer::peek()` should be called") + } + #[cfg(not(feature = "overlapped-lists"))] + { + self.peek + .as_ref() + .expect("`Deserializer::peek()` should be called") + } + } + fn next(&mut self) -> Result, DeError> { // Replay skipped or peeked events #[cfg(feature = "overlapped-lists")] @@ -2764,6 +2853,14 @@ where } self.reader.read_to_end(name) } + + fn skip_next_tree(&mut self) -> Result<(), DeError> { + let DeEvent::Start(start) = self.next()? else { + unreachable!("Only call this if the next event is a start event") + }; + let name = start.name(); + self.read_to_end(name) + } } impl<'de> Deserializer<'de, SliceReader<'de>> { @@ -2783,7 +2880,7 @@ where /// Create new deserializer that will borrow data from the specified string /// and use specified entity resolver. pub fn from_str_with_resolver(source: &'de str, entity_resolver: E) -> Self { - let mut reader = Reader::from_str(source); + let mut reader = NsReader::from_str(source); let config = reader.config_mut(); config.expand_empty_elements = true; @@ -2826,7 +2923,7 @@ where /// will borrow instead of copy. If you have `&[u8]` which is known to represent /// UTF-8, you can decode it first before using [`from_str`]. pub fn with_resolver(reader: R, entity_resolver: E) -> Self { - let mut reader = Reader::from_reader(reader); + let mut reader = NsReader::from_reader(reader); let config = reader.config_mut(); config.expand_empty_elements = true; @@ -2945,9 +3042,16 @@ where where V: Visitor<'de>, { - match self.peek()? { + // We cannot use result of `peek()` directly because of borrow checker + let _ = self.peek()?; + match self.last_peeked() { DeEvent::Text(t) if t.is_empty() => visitor.visit_none(), DeEvent::Eof => visitor.visit_none(), + // if the `xsi:nil` attribute is set to true we got a none value + DeEvent::Start(start) if self.reader.reader.has_nil_attr(&start) => { + self.skip_next_tree()?; + visitor.visit_none() + } _ => visitor.visit_some(self), } } @@ -3071,6 +3175,12 @@ pub trait XmlRead<'i> { /// A copy of the reader's decoder used to decode strings. fn decoder(&self) -> Decoder; + + /// Checks if the `start` tag has a [`xsi:nil`] attribute. This method ignores + /// any errors in attributes. + /// + /// [`xsi:nil`]: https://www.w3.org/TR/xmlschema-1/#xsi_nil + fn has_nil_attr(&self, start: &BytesStart) -> bool; } /// XML input source that reads from a std::io input stream. @@ -3078,7 +3188,7 @@ pub trait XmlRead<'i> { /// You cannot create it, it is created automatically when you call /// [`Deserializer::from_reader`] pub struct IoReader { - reader: Reader, + reader: NsReader, start_trimmer: StartTrimmer, buf: Vec, } @@ -3091,7 +3201,7 @@ impl IoReader { /// use serde::Deserialize; /// use std::io::Cursor; /// use quick_xml::de::Deserializer; - /// use quick_xml::Reader; + /// use quick_xml::NsReader; /// /// #[derive(Deserialize)] /// struct SomeStruct { @@ -3108,12 +3218,12 @@ impl IoReader { /// let err = SomeStruct::deserialize(&mut de); /// assert!(err.is_err()); /// - /// let reader: &Reader> = de.get_ref().get_ref(); + /// let reader: &NsReader> = de.get_ref().get_ref(); /// /// assert_eq!(reader.error_position(), 28); /// assert_eq!(reader.buffer_position(), 41); /// ``` - pub const fn get_ref(&self) -> &Reader { + pub const fn get_ref(&self) -> &NsReader { &self.reader } } @@ -3140,6 +3250,10 @@ impl<'i, R: BufRead> XmlRead<'i> for IoReader { fn decoder(&self) -> Decoder { self.reader.decoder() } + + fn has_nil_attr(&self, start: &BytesStart) -> bool { + start.attributes().has_nil(&self.reader) + } } /// XML input source that reads from a slice of bytes and can borrow from it. @@ -3147,7 +3261,7 @@ impl<'i, R: BufRead> XmlRead<'i> for IoReader { /// You cannot create it, it is created automatically when you call /// [`Deserializer::from_str`]. pub struct SliceReader<'de> { - reader: Reader<&'de [u8]>, + reader: NsReader<&'de [u8]>, start_trimmer: StartTrimmer, } @@ -3158,7 +3272,7 @@ impl<'de> SliceReader<'de> { /// # use pretty_assertions::assert_eq; /// use serde::Deserialize; /// use quick_xml::de::Deserializer; - /// use quick_xml::Reader; + /// use quick_xml::NsReader; /// /// #[derive(Deserialize)] /// struct SomeStruct { @@ -3175,12 +3289,12 @@ impl<'de> SliceReader<'de> { /// let err = SomeStruct::deserialize(&mut de); /// assert!(err.is_err()); /// - /// let reader: &Reader<&[u8]> = de.get_ref().get_ref(); + /// let reader: &NsReader<&[u8]> = de.get_ref().get_ref(); /// /// assert_eq!(reader.error_position(), 28); /// assert_eq!(reader.buffer_position(), 41); /// ``` - pub const fn get_ref(&self) -> &Reader<&'de [u8]> { + pub const fn get_ref(&self) -> &NsReader<&'de [u8]> { &self.reader } } @@ -3205,6 +3319,10 @@ impl<'de> XmlRead<'de> for SliceReader<'de> { fn decoder(&self) -> Decoder { self.reader.decoder() } + + fn has_nil_attr(&self, start: &BytesStart) -> bool { + start.attributes().has_nil(&self.reader) + } } #[cfg(test)] @@ -3781,12 +3899,12 @@ mod tests { "#; let mut reader1 = IoReader { - reader: Reader::from_reader(s.as_bytes()), + reader: NsReader::from_reader(s.as_bytes()), start_trimmer: StartTrimmer::default(), buf: Vec::new(), }; let mut reader2 = SliceReader { - reader: Reader::from_str(s), + reader: NsReader::from_str(s), start_trimmer: StartTrimmer::default(), }; @@ -3812,7 +3930,7 @@ mod tests { "#; let mut reader = SliceReader { - reader: Reader::from_str(s), + reader: NsReader::from_str(s), start_trimmer: StartTrimmer::default(), }; diff --git a/src/events/attributes.rs b/src/events/attributes.rs index dd35c4d5..c5be1b3c 100644 --- a/src/events/attributes.rs +++ b/src/events/attributes.rs @@ -5,8 +5,9 @@ use crate::encoding::Decoder; use crate::errors::Result as XmlResult; use crate::escape::{escape, resolve_predefined_entity, unescape_with}; -use crate::name::QName; -use crate::utils::{is_whitespace, write_byte_string, write_cow_string, Bytes}; +use crate::name::{LocalName, Namespace, QName}; +use crate::reader::NsReader; +use crate::utils::{is_whitespace, Bytes}; use std::fmt::{self, Debug, Display, Formatter}; use std::iter::FusedIterator; @@ -96,15 +97,50 @@ impl<'a> Attribute<'a> { Cow::Owned(s) => Ok(s.into()), } } + + /// If attribute value [represents] valid boolean values, returns `Some`, otherwise returns `None`. + /// + /// The valid boolean representations are only `"true"`, `"false"`, `"1"`, and `"0"`. + /// + /// # Examples + /// + /// ``` + /// # use pretty_assertions::assert_eq; + /// use quick_xml::events::attributes::Attribute; + /// + /// let attr = Attribute::from(("attr", "false")); + /// assert_eq!(attr.as_bool(), Some(false)); + /// + /// let attr = Attribute::from(("attr", "0")); + /// assert_eq!(attr.as_bool(), Some(false)); + /// + /// let attr = Attribute::from(("attr", "true")); + /// assert_eq!(attr.as_bool(), Some(true)); + /// + /// let attr = Attribute::from(("attr", "1")); + /// assert_eq!(attr.as_bool(), Some(true)); + /// + /// let attr = Attribute::from(("attr", "bot bool")); + /// assert_eq!(attr.as_bool(), None); + /// ``` + /// + /// [represents]: https://www.w3.org/TR/xmlschema11-2/#boolean + #[inline] + pub fn as_bool(&self) -> Option { + match self.value.as_ref() { + b"1" | b"true" => Some(true), + b"0" | b"false" => Some(false), + _ => None, + } + } } impl<'a> Debug for Attribute<'a> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Attribute {{ key: ")?; - write_byte_string(f, self.key.as_ref())?; - write!(f, ", value: ")?; - write_cow_string(f, &self.value)?; - write!(f, " }}") + f.debug_struct("Attribute") + .field("key", &Bytes(self.key.as_ref())) + .field("value", &Bytes(&self.value)) + .finish() } } @@ -196,7 +232,7 @@ impl<'a> From> for Attribute<'a> { /// The duplicate check can be turned off by calling [`with_checks(false)`]. /// /// [`with_checks(false)`]: Self::with_checks -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct Attributes<'a> { /// Slice of `BytesStart` corresponding to attributes bytes: &'a [u8], @@ -234,6 +270,90 @@ impl<'a> Attributes<'a> { self.state.check_duplicates = val; self } + + /// Checks if the current tag has a [`xsi:nil`] attribute. This method ignores any errors in + /// attributes. + /// + /// # Examples + /// + /// ``` + /// # use pretty_assertions::assert_eq; + /// use quick_xml::events::Event; + /// use quick_xml::name::QName; + /// use quick_xml::reader::NsReader; + /// + /// let mut reader = NsReader::from_str(" + /// + /// + /// + /// + /// + /// + /// + /// + /// "); + /// reader.config_mut().trim_text(true); + /// + /// macro_rules! check { + /// ($reader:expr, $name:literal, $value:literal) => { + /// let event = match $reader.read_event().unwrap() { + /// Event::Empty(e) => e, + /// e => panic!("Unexpected event {:?}", e), + /// }; + /// assert_eq!( + /// (event.name(), event.attributes().has_nil(&$reader)), + /// (QName($name.as_bytes()), $value), + /// ); + /// }; + /// } + /// + /// let root = match reader.read_event().unwrap() { + /// Event::Start(e) => e, + /// e => panic!("Unexpected event {:?}", e), + /// }; + /// assert_eq!(root.attributes().has_nil(&reader), false); + /// + /// // definitely true + /// check!(reader, "true", true); + /// // definitely false + /// check!(reader, "false", false); + /// // absence of the attribute means that attribute is not set + /// check!(reader, "none", false); + /// // attribute not bound to the correct namespace + /// check!(reader, "non-xsi", false); + /// // attributes without prefix not bound to any namespace + /// check!(reader, "unbound-nil", false); + /// // prefix can be any while it is bound to the correct namespace + /// check!(reader, "another-xmlns", true); + /// ``` + /// + /// [`xsi:nil`]: https://www.w3.org/TR/xmlschema-1/#xsi_nil + pub fn has_nil(&mut self, reader: &NsReader) -> bool { + use crate::name::ResolveResult::*; + + self.any(|attr| { + if let Ok(attr) = attr { + match reader.resolve_attribute(attr.key) { + ( + Bound(Namespace(b"http://www.w3.org/2001/XMLSchema-instance")), + LocalName(b"nil"), + ) => attr.as_bool().unwrap_or_default(), + _ => false, + } + } else { + false + } + }) + } +} + +impl<'a> Debug for Attributes<'a> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct("Attributes") + .field("bytes", &Bytes(&self.bytes)) + .field("state", &self.state) + .finish() + } } impl<'a> Iterator for Attributes<'a> { diff --git a/src/name.rs b/src/name.rs index b7a8de16..6bed83a8 100644 --- a/src/name.rs +++ b/src/name.rs @@ -200,7 +200,7 @@ impl<'a> AsRef<[u8]> for QName<'a> { /// [local (unqualified) name]: https://www.w3.org/TR/xml-names11/#dt-localname #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde-types", derive(serde::Deserialize, serde::Serialize))] -pub struct LocalName<'a>(&'a [u8]); +pub struct LocalName<'a>(pub(crate) &'a [u8]); impl<'a> LocalName<'a> { /// Converts this name to an internal slice representation. #[inline(always)] diff --git a/src/reader/async_tokio.rs b/src/reader/async_tokio.rs index eaf56f26..c5e1eaaa 100644 --- a/src/reader/async_tokio.rs +++ b/src/reader/async_tokio.rs @@ -119,7 +119,8 @@ impl Reader { mut buf: &'b mut Vec, ) -> Result> { read_event_impl!( - self, buf, + self, + buf, TokioAdapter(&mut self.reader), read_until_close_async, await @@ -181,7 +182,16 @@ impl Reader { end: QName<'n>, buf: &mut Vec, ) -> Result { - Ok(read_to_end!(self, end, buf, read_event_into_async, { buf.clear(); }, await)) + Ok(read_to_end!( + self, + end, + buf, + read_event_into_async, + { + buf.clear(); + }, + await + )) } /// Private function to read until `>` is found. This function expects that @@ -410,7 +420,8 @@ mod test { read_until_close_async, TokioAdapter, &mut Vec::new(), - async, await + async, + await ); #[test] diff --git a/tests/serde-de-xsi.rs b/tests/serde-de-xsi.rs new file mode 100644 index 00000000..4db9e605 --- /dev/null +++ b/tests/serde-de-xsi.rs @@ -0,0 +1,704 @@ +//! Tests for ensure behavior of `xsi:nil` handling. +//! +//! We want to threat element with `xsi:nil="true"` as `None` in optional contexts. +use quick_xml::se::to_string; +use quick_xml::DeError; + +use serde::{Deserialize, Serialize}; + +mod serde_helpers; +use serde_helpers::from_str; + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct Foo { + elem: String, +} + +macro_rules! assert_error_matches { + ($res: expr, $err: pat) => { + assert!( + matches!($res, Err($err)), + concat!("Expected `", stringify!($err), "`, but got `{:?}`"), + $res + ); + }; +} + +mod top_level_option { + use super::*; + + mod empty { + use super::*; + + /// Without `xsi:nil="true"` tags in optional contexts are always considered as having + /// `Some` value, but because we do not have `tag` element, deserialization failed + #[test] + fn none() { + let xml = r#""#; + assert_error_matches!(from_str::>(xml), DeError::Custom(_)); + } + + /// When prefix is not defined, attributes not bound to any namespace (unlike elements), + /// so just `nil="true"` does not mean that `xsi:nil` is set + mod no_prefix { + use super::*; + + #[test] + fn true_() { + let xml = r#""#; + assert_error_matches!(from_str::>(xml), DeError::Custom(_)); + } + + #[test] + fn false_() { + let xml = r#""#; + assert_error_matches!(from_str::>(xml), DeError::Custom(_)); + } + } + + /// Check canonical prefix + mod xsi { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn true_() { + let xml = r#""#; + assert_eq!(from_str::>(xml).unwrap(), None); + } + + #[test] + fn false_() { + let xml = r#""#; + assert_error_matches!(from_str::>(xml), DeError::Custom(_)); + } + } + + /// Check other prefix to be sure that we not process only canonical prefixes + mod ns0 { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn true_() { + let xml = r#""#; + assert_eq!(from_str::>(xml).unwrap(), None); + } + + #[test] + fn false_() { + let xml = r#""#; + assert_error_matches!(from_str::>(xml), DeError::Custom(_)); + } + } + } + + /// We have no place to store attribute of the element, so the behavior must be the same + /// as without attributes. + mod with_attr { + use super::*; + + #[test] + fn none() { + let xml = r#""#; + assert_error_matches!(from_str::>(xml), DeError::Custom(_)); + } + + /// When prefix is not defined, attributes not bound to any namespace (unlike elements), + /// so just `nil="true"` does not mean that `xsi:nil` is set + mod no_prefix { + use super::*; + + #[test] + fn true_() { + let xml = r#""#; + assert_error_matches!(from_str::>(xml), DeError::Custom(_)); + } + + #[test] + fn false_() { + let xml = r#""#; + assert_error_matches!(from_str::>(xml), DeError::Custom(_)); + } + } + + /// Check canonical prefix + mod xsi { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn true_() { + let xml = r#""#; + assert_eq!(from_str::>(xml).unwrap(), None); + } + + #[test] + fn false_() { + let xml = r#""#; + assert_error_matches!(from_str::>(xml), DeError::Custom(_)); + } + } + + /// Check other prefix to be sure that we not process only canonical prefixes + mod ns0 { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn true_() { + let xml = r#""#; + assert_eq!(from_str::>(xml).unwrap(), None); + } + + #[test] + fn false_() { + let xml = r#""#; + assert_error_matches!(from_str::>(xml), DeError::Custom(_)); + } + } + } + + mod with_element { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn none() { + let xml = r#"Foo"#; + assert_eq!( + from_str::>(xml).unwrap(), + Some(Foo { elem: "Foo".into() }) + ); + } + + /// When prefix is not defined, attributes not bound to any namespace (unlike elements), + /// so just `nil="true"` does not mean that `xsi:nil` is set + mod no_prefix { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn true_() { + let xml = r#"Foo"#; + assert_eq!( + from_str::>(xml).unwrap(), + Some(Foo { elem: "Foo".into() }) + ); + } + + #[test] + fn false_() { + let xml = r#"Foo"#; + assert_eq!( + from_str::>(xml).unwrap(), + Some(Foo { elem: "Foo".into() }) + ); + } + } + + /// Check canonical prefix + mod xsi { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn true_() { + let xml = r#"Foo"#; + assert_eq!(from_str::>(xml).unwrap(), None); + } + + #[test] + fn false_() { + let xml = r#"Foo"#; + assert_eq!( + from_str::>(xml).unwrap(), + Some(Foo { elem: "Foo".into() }) + ); + } + } + + /// Check other prefix to be sure that we not process only canonical prefixes + mod ns0 { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn true_() { + let xml = r#"Foo"#; + assert_eq!(from_str::>(xml).unwrap(), None); + } + + #[test] + fn false_() { + let xml = r#"Foo"#; + assert_eq!( + from_str::>(xml).unwrap(), + Some(Foo { elem: "Foo".into() }) + ); + } + } + } +} + +mod as_field { + use super::*; + + /// According to the [specification], `xsi:nil` controls only ability to (not) have nested + /// elements, but it does not applied to attributes. Due to that we ensure, that attributes + /// are still can be accessed. + /// + /// [specification]: https://www.w3.org/TR/xmlschema11-1/#Instance_Document_Constructions + #[derive(Debug, Deserialize, PartialEq, Serialize)] + struct AnyName { + #[serde(rename = "@attr")] + attr: Option, + + elem: Option, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct Root { + foo: AnyName, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct Bar { + foo: Option, + } + + macro_rules! check { + ( + $name:ident, + $true_xml:literal, + $false_xml:literal, + $se_xml:literal, + $attr:expr, + ) => { + mod $name { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn true_() { + let value = AnyName { + attr: $attr, + elem: None, + }; + + assert_eq!(to_string(&value).unwrap(), $se_xml); + assert_eq!(from_str::($true_xml).unwrap(), value); + } + + #[test] + fn false_() { + let value = AnyName { + attr: $attr, + elem: None, + }; + + assert_eq!(to_string(&value).unwrap(), $se_xml); + assert_eq!(from_str::($false_xml).unwrap(), value); + } + } + }; + } + + mod empty { + use super::*; + use pretty_assertions::assert_eq; + + /// Without `xsi:nil="true"` tags in optional contexts are always considered as having + /// `Some` value, but because we do not have `tag` element, deserialization failed + #[test] + fn none() { + let value = AnyName { + attr: None, + elem: None, + }; + + assert_eq!( + to_string(&value).unwrap(), + r#""# + ); + assert_eq!( + from_str::("").unwrap(), + AnyName { + attr: None, + elem: None, + } + ); + } + + // When prefix is not defined, attributes not bound to any namespace (unlike elements), + // so just `nil="true"` does not mean that `xsi:nil` is set. But because `AnyName` is empty + // there anyway nothing inside, so all fields will be set to `None` + check!( + no_prefix, + r#""#, + r#""#, + r#""#, + None, + ); + + // Check canonical prefix + check!( + xsi, + r#""#, + r#""#, + r#""#, + None, + ); + + // Check other prefix to be sure that we do not process only canonical prefixes + check!( + ns0, + r#""#, + r#""#, + r#""#, + None, + ); + + mod nested { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn none() { + let xml = + r#""#; + + assert_error_matches!(from_str::(xml), DeError::Custom(_)); + assert_eq!( + from_str::(xml).unwrap(), + Root { + foo: AnyName { + attr: None, + elem: None, + }, + } + ); + } + + #[test] + fn true_() { + let xml = r#""#; + + assert_eq!(from_str::(xml).unwrap(), Bar { foo: None }); + assert_eq!( + from_str::(xml).unwrap(), + Root { + foo: AnyName { + attr: None, + elem: None, + }, + } + ); + } + + #[test] + fn false_() { + let xml = r#""#; + + assert_error_matches!(from_str::(xml), DeError::Custom(_)); + assert_eq!( + from_str::(xml).unwrap(), + Root { + foo: AnyName { + attr: None, + elem: None, + }, + } + ); + } + } + } + + /// We have no place to store attribute of the element, so the behavior must be the same + /// as without attributes. + mod with_attr { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn none() { + let value = AnyName { + attr: Some("value".into()), + elem: None, + }; + + assert_eq!( + to_string(&value).unwrap(), + r#""# + ); + assert_eq!( + from_str::(r#""#).unwrap(), + value + ); + } + + // When prefix is not defined, attributes not bound to any namespace (unlike elements), + // so just `nil="true"` does not mean that `xsi:nil` is set. But because `AnyName` is empty + // there anyway nothing inside, so all element fields will be set to `None` + check!( + no_prefix, + r#""#, + r#""#, + r#""#, + Some("value".into()), + ); + + // Check canonical prefix + check!( + xsi, + r#""#, + r#""#, + r#""#, + Some("value".into()), + ); + + // Check other prefix to be sure that we do not process only canonical prefixes + check!( + ns0, + r#""#, + r#""#, + r#""#, + Some("value".into()), + ); + + mod nested { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn none() { + let xml = r#""#; + + // Without `xsi:nil="true"` is mapped to `foo` field, + // but failed to deserialzie because of missing required tag + assert_error_matches!(from_str::(xml), DeError::Custom(_)); + assert_eq!( + from_str::(xml).unwrap(), + Root { + foo: AnyName { + attr: Some("value".into()), + elem: None, + }, + } + ); + } + + #[test] + fn true_() { + let xml = r#""#; + + assert_eq!(from_str::(xml).unwrap(), Bar { foo: None }); + assert_eq!( + from_str::(xml).unwrap(), + Root { + foo: AnyName { + attr: Some("value".into()), + elem: None, + }, + } + ); + } + + #[test] + fn false_() { + let xml = r#""#; + + // With `xsi:nil="false"` is mapped to `foo` field, + // but failed to deserialzie because of missing required tag + assert_error_matches!(from_str::(xml), DeError::Custom(_)); + assert_eq!( + from_str::(xml).unwrap(), + Root { + foo: AnyName { + attr: Some("value".into()), + elem: None, + }, + } + ); + } + } + } + + mod with_element { + use super::*; + use pretty_assertions::assert_eq; + + macro_rules! check { + ( + $name:ident, + + $de_true_xml:literal, + $se_true_xml:literal, + + $de_false_xml:literal, + $se_false_xml:literal, + ) => { + mod $name { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn true_() { + let value = AnyName { + attr: None, + // Becase `nil=true``, element deserialized as `None` + elem: None, + }; + + assert_eq!(to_string(&value).unwrap(), $se_true_xml); + assert_eq!(from_str::($de_true_xml).unwrap(), value); + } + + #[test] + fn false_() { + let value = AnyName { + attr: None, + elem: Some("Foo".into()), + }; + + assert_eq!(to_string(&value).unwrap(), $se_false_xml); + assert_eq!(from_str::($de_false_xml).unwrap(), value); + } + } + }; + } + + #[test] + fn none() { + let value = AnyName { + attr: None, + elem: Some("Foo".into()), + }; + + assert_eq!( + to_string(&value).unwrap(), + r#"Foo"# + ); + assert_eq!( + from_str::(r#"Foo"#).unwrap(), + value + ); + } + + /// When prefix is not defined, attributes not bound to any namespace (unlike elements), + /// so just `nil="true"` does not mean that `xsi:nil` is set + mod no_prefix { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn true_() { + let se_xml = r#"Foo"#; + let de_xml = r#"Foo"#; + + let value = AnyName { + attr: None, + elem: Some("Foo".into()), + }; + + assert_eq!(to_string(&value).unwrap(), se_xml); + assert_eq!(from_str::(de_xml).unwrap(), value); + } + + #[test] + fn false_() { + let se_xml = r#"Foo"#; + let de_xml = r#"Foo"#; + + let value = AnyName { + attr: None, + elem: Some("Foo".into()), + }; + + assert_eq!(to_string(&value).unwrap(), se_xml); + assert_eq!(from_str::(de_xml).unwrap(), value); + } + } + + // Check canonical prefix + check!( + xsi, + r#"Foo"#, + r#""#, + r#"Foo"#, + r#"Foo"#, + ); + + // Check other prefix to be sure that we do not process only canonical prefixes + check!( + ns0, + r#"Foo"#, + r#""#, + r#"Foo"#, + r#"Foo"#, + ); + + mod nested { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn none() { + let xml = r#"Foo"#; + + assert_eq!( + from_str::(xml).unwrap(), + Bar { + foo: Some(Foo { elem: "Foo".into() }), + } + ); + assert_eq!( + from_str::(xml).unwrap(), + Root { + foo: AnyName { + attr: None, + elem: Some("Foo".into()), + }, + } + ); + } + + #[test] + fn true_() { + let xml = r#"Foo"#; + + assert_eq!(from_str::(xml).unwrap(), Bar { foo: None }); + assert_eq!( + from_str::(xml).unwrap(), + Root { + foo: AnyName { + attr: None, + elem: None, + }, + } + ); + } + + #[test] + fn false_() { + let xml = r#"Foo"#; + + assert_eq!( + from_str::(xml).unwrap(), + Bar { + foo: Some(Foo { elem: "Foo".into() }), + } + ); + assert_eq!( + from_str::(xml).unwrap(), + Root { + foo: AnyName { + attr: None, + elem: Some("Foo".into()), + }, + } + ); + } + } + } +} diff --git a/tests/serde-issues.rs b/tests/serde-issues.rs index bac7d416..7826669d 100644 --- a/tests/serde-issues.rs +++ b/tests/serde-issues.rs @@ -218,6 +218,23 @@ fn issue429() { ); } +/// Regression test for https://github.com/tafia/quick-xml/issues/497. +#[test] +fn issue497() { + #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] + struct Player { + #[serde(skip_serializing_if = "Option::is_none")] + spawn_forced: Option, + } + let data = Player { spawn_forced: None }; + + let deserialize_buffer = to_string(&data).unwrap(); + dbg!(&deserialize_buffer); + + let p: Player = from_reader(deserialize_buffer.as_bytes()).unwrap(); + assert_eq!(p, data); +} + /// Regression test for https://github.com/tafia/quick-xml/issues/500. #[test] fn issue500() {