xmlity/
lib.rs

1#![warn(missing_docs)]
2//! # XMLity - Powerful XML (de)serialization for Rust
3//!
4//! XMLity is a (de)serialization library for XML in Rust designed to allow for practically any kind of XML structure to be (de)serialized.
5//!
6//! Currently, XMLity does not currently optimize for ergonomics, which means that it is quite verbose to use. However, it is designed to be easy to use and extend.
7//!
8//! The most important types in XMLity are:
9//! - [`Serialize`], [`SerializeAttribute`] and [`Deserialize`] which lets you define how to (de)serialize types,
10//! - [`Serializer`] and [`Deserializer`] which lets you define readers and writers for XML documents.
11//! - [`SerializationGroup`] and [`DeserializationGroup`] which lets you define how to (de)serialize groups of types that can be extended upon in other elements/groups recursively.
12//! - [`XmlValue`] and its variants which allow for generic deserialization of XML documents, similar to [`serde_json::Value`].
13//!
14//! The library includes derive macros for [`Serialize`], [`SerializeAttribute`], [`Deserialize`], [`SerializationGroup`] and [`DeserializationGroup`] which can be enabled with the `derive` feature. The macros can be used to create nearly any kind of XML structure you want. If there is something it cannot do, please open an issue or a pull request.
15//!
16//! The macro [`xml`] can be used to create [`XmlValues`](`XmlValue`) in a more ergonomic way. It is also possible to create [`XmlValues`](`XmlValue`) manually, but it is quite verbose.
17use core::{fmt, str};
18use fmt::Display;
19use std::{borrow::Cow, str::FromStr};
20
21pub mod de;
22pub use de::{DeserializationGroup, Deserialize, DeserializeOwned, Deserializer};
23pub mod ser;
24pub use ser::{AttributeSerializer, SerializationGroup, Serialize, SerializeAttribute, Serializer};
25mod macros;
26pub mod types;
27pub mod value;
28pub use value::XmlValue;
29mod noop;
30pub use noop::NoopDeSerializer;
31
32#[cfg(feature = "derive")]
33extern crate xmlity_derive;
34
35#[cfg(feature = "derive")]
36pub use xmlity_derive::{
37    DeserializationGroup, Deserialize, SerializationGroup, Serialize, SerializeAttribute,
38};
39
40// Reference: https://www.w3.org/TR/xml/#sec-common-syn
41// [4]   	NameStartChar	   ::=   	":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
42// [4a]   	NameChar	   ::=   	NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
43// [5]   	Name	   ::=   	NameStartChar (NameChar)*
44// [6]   	Names	   ::=   	Name (#x20 Name)*
45// [7]   	Nmtoken	   ::=   	(NameChar)+
46// [8]   	Nmtokens	   ::=   	Nmtoken (#x20 Nmtoken)*
47mod name_tokens {
48    const fn is_name_start_char(c: char) -> bool {
49        matches!(
50            //Deliberately excluding : as we handle it separately
51            c, 'A'..='Z' | '_' | 'a'..='z' | '\u{00C0}'..='\u{00D6}' | '\u{00D8}'..='\u{00F6}' | '\u{00F8}'..='\u{02FF}' | '\u{0370}'..='\u{037D}' | '\u{037F}'..='\u{1FFF}' | '\u{200C}'..='\u{200D}' | '\u{2070}'..='\u{218F}' | '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}' | '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}' | '\u{10000}'..='\u{EFFFF}'
52        )
53    }
54    const fn is_name_char(c: char) -> bool {
55        is_name_start_char(c)
56            || matches!(c, '-' | '.' | '0'..='9' | '\u{00B7}' | '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}')
57    }
58
59    /// An error that occurs when a name is invalid.
60    #[derive(Debug, Clone, thiserror::Error, PartialEq, Eq, PartialOrd, Ord, Hash)]
61    #[non_exhaustive]
62    pub enum InvalidXmlNameError {
63        /// The name starts with an invalid character. The first character of an XML name is special and excludes some other characters incl "-" which is allowed in the middle of the name.
64        #[error("Invalid start character")]
65        InvalidStartChar,
66        /// The name contains an invalid character.
67        #[error("Invalid character at index {index}")]
68        InvalidChar {
69            /// The index of the invalid character.
70            index: usize,
71            /// The invalid character.
72            character: char,
73        },
74        /// The name is empty.
75        #[error("Empty name")]
76        Empty,
77    }
78
79    pub fn is_valid_name(name: &str) -> Result<(), InvalidXmlNameError> {
80        let mut chars = name.chars();
81        if let Some(c) = chars.next() {
82            if !is_name_start_char(c) {
83                return Err(InvalidXmlNameError::InvalidStartChar);
84            }
85        } else {
86            return Err(InvalidXmlNameError::Empty);
87        }
88        for (index, character) in chars.enumerate().map(|(i, c)| (i + 1, c)) {
89            if !is_name_char(character) {
90                return Err(InvalidXmlNameError::InvalidChar { index, character });
91            }
92        }
93
94        Ok(())
95    }
96}
97
98pub use name_tokens::InvalidXmlNameError;
99
100/// # XML Expanded Name
101/// An [`ExpandedName`] is a [`LocalName`] together with its associated [`XmlNamespace`]. This can convert to and from a [`QName`] with a [`Prefix`] mapping.
102#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
103pub struct ExpandedName<'a> {
104    local_name: LocalName<'a>,
105    namespace: Option<XmlNamespace<'a>>,
106}
107
108impl<'a> ExpandedName<'a> {
109    /// Creates a new [`ExpandedName`].
110    pub fn new(local_name: LocalName<'a>, namespace: Option<XmlNamespace<'a>>) -> Self {
111        Self {
112            local_name,
113            namespace,
114        }
115    }
116
117    /// Converts this [`ExpandedName`] into its parts.
118    pub fn into_parts(self) -> (LocalName<'a>, Option<XmlNamespace<'a>>) {
119        (self.local_name, self.namespace)
120    }
121
122    /// Converts this [`ExpandedName`] into an owned version.
123    pub fn into_owned(self) -> ExpandedName<'static> {
124        ExpandedName::new(
125            self.local_name.into_owned(),
126            self.namespace.map(|n| n.into_owned()),
127        )
128    }
129
130    /// Returns this [`ExpandedName`] as a reference.
131    pub fn as_ref(&self) -> ExpandedName<'_> {
132        ExpandedName::new(
133            self.local_name.as_ref(),
134            self.namespace.as_ref().map(|n| n.as_ref()),
135        )
136    }
137
138    /// Returns the local name of this [`ExpandedName`].
139    pub fn local_name(&self) -> &LocalName<'_> {
140        &self.local_name
141    }
142
143    /// Returns the namespace of this [`ExpandedName`].
144    pub fn namespace(&self) -> Option<&XmlNamespace<'_>> {
145        self.namespace.as_ref()
146    }
147
148    /// Converts this [`ExpandedName`] into a [`QName`] name using the given [`Prefix`].
149    pub fn to_q_name(self, resolved_prefix: Option<Prefix<'a>>) -> QName<'a> {
150        QName::new(resolved_prefix, self.local_name.clone())
151    }
152}
153
154impl Display for ExpandedName<'_> {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        self.local_name.fmt(f)
157    }
158}
159
160/// # XML Qualified Name
161/// A [`QName`] is a [`LocalName`] together with a namespace [`Prefix`], indicating it belongs to a specific declared [`XmlNamespace`].
162#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
163pub struct QName<'a> {
164    prefix: Option<Prefix<'a>>,
165    local_name: LocalName<'a>,
166}
167
168/// An error that can occur when parsing a [`QName`].
169#[derive(Debug, thiserror::Error, PartialEq, Eq)]
170pub enum QNameParseError {
171    /// The [`Prefix`] is invalid.
172    #[error("Invalid prefix: {0}")]
173    InvalidPrefix(#[from] PrefixParseError),
174    /// The [`LocalName`] is invalid.
175    #[error("Invalid local name: {0}")]
176    InvalidLocalName(#[from] LocalNameParseError),
177}
178
179impl<'a> QName<'a> {
180    /// Creates a new [`QName`].
181    pub fn new<P: Into<Option<Prefix<'a>>>, L: Into<LocalName<'a>>>(
182        prefix: P,
183        local_name: L,
184    ) -> Self {
185        QName {
186            prefix: prefix.into(),
187            local_name: local_name.into(),
188        }
189    }
190
191    /// Converts this [`QName`] into its parts.
192    pub fn into_parts(self) -> (Option<Prefix<'a>>, LocalName<'a>) {
193        (self.prefix, self.local_name)
194    }
195
196    /// Converts this [`QName`] into being owned.
197    pub fn into_owned(self) -> QName<'static> {
198        QName {
199            prefix: self.prefix.map(|prefix| prefix.into_owned()),
200            local_name: self.local_name.into_owned(),
201        }
202    }
203
204    /// Returns this [`QName`] as a reference.
205    pub fn as_ref(&self) -> QName<'_> {
206        QName {
207            prefix: self.prefix.as_ref().map(|prefix| prefix.as_ref()),
208            local_name: self.local_name.as_ref(),
209        }
210    }
211
212    /// Returns the [`Prefix`] of this [`QName`].
213    pub fn prefix(&self) -> Option<&Prefix<'a>> {
214        self.prefix.as_ref()
215    }
216
217    /// Returns the [`LocalName`] of this [`QName`].
218    pub fn local_name(&self) -> &LocalName<'a> {
219        &self.local_name
220    }
221}
222
223impl Display for QName<'_> {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        if let Some(prefix) = &self.prefix.as_ref().filter(|prefix| !prefix.is_default()) {
226            write!(f, "{}:{}", prefix, self.local_name)
227        } else {
228            write!(f, "{}", self.local_name)
229        }
230    }
231}
232impl FromStr for QName<'_> {
233    type Err = QNameParseError;
234    fn from_str(s: &str) -> Result<Self, Self::Err> {
235        let (prefix, local_name) = s.split_once(':').unwrap_or(("", s));
236
237        let prefix = Prefix::from_str(prefix)?;
238        let local_name = LocalName::from_str(local_name)?;
239
240        Ok(QName::new(prefix, local_name))
241    }
242}
243
244impl<'a> From<QName<'a>> for Option<Prefix<'a>> {
245    fn from(value: QName<'a>) -> Self {
246        value.prefix
247    }
248}
249
250impl<'a> From<QName<'a>> for LocalName<'a> {
251    fn from(value: QName<'a>) -> Self {
252        value.local_name
253    }
254}
255
256/// # XML Namespace
257/// A namespace URI, to which [`LocalNames`](`LocalName`) are scoped under.
258#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
259pub struct XmlNamespace<'a>(Cow<'a, str>);
260
261/// An error that can occur when parsing a [`XmlNamespace`].
262#[derive(Debug, thiserror::Error)]
263pub enum XmlNamespaceParseError {}
264
265impl<'a> XmlNamespace<'a> {
266    /// Creates a new [`XmlNamespace`] from a string.
267    pub fn new<T: Into<Cow<'a, str>>>(value: T) -> Result<Self, XmlNamespaceParseError> {
268        Ok(Self(value.into()))
269    }
270
271    /// Creates a new [`XmlNamespace`] from a string without validating it, but it works in a const context.
272    ///
273    /// # Safety
274    /// This function does not validate the input value due to const context limitations involving the validation function. While this cannot create a memory safety issue, it can create a logical one.
275    pub const fn new_dangerous(value: &'a str) -> Self {
276        Self(Cow::Borrowed(value))
277    }
278
279    /// Converts this [`XmlNamespace`] into an owned version.
280    pub fn into_owned(self) -> XmlNamespace<'static> {
281        XmlNamespace(Cow::Owned(self.0.into_owned()))
282    }
283
284    /// Returns this [`XmlNamespace`] as a reference.
285    pub fn as_ref(&self) -> XmlNamespace<'_> {
286        XmlNamespace(Cow::Borrowed(&self.0))
287    }
288
289    /// Returns this [`XmlNamespace`] as a string slice.
290    pub fn as_str(&self) -> &str {
291        &self.0
292    }
293
294    /// The namespace for XML namespace declarations.
295    pub const XMLNS: XmlNamespace<'static> =
296        XmlNamespace::new_dangerous("http://www.w3.org/2000/xmlns/");
297    /// The namespace for built-in XML attributes.
298    pub const XML: XmlNamespace<'static> =
299        XmlNamespace::new_dangerous("http://www.w3.org/XML/1998/namespace");
300    /// The namespace for XHTML.
301    pub const XHTML: XmlNamespace<'static> =
302        XmlNamespace::new_dangerous("http://www.w3.org/1999/xhtml");
303    /// The namespace for XML Schema.
304    pub const XS: XmlNamespace<'static> =
305        XmlNamespace::new_dangerous("http://www.w3.org/2001/XMLSchema");
306    /// The namespace for XML Schema Instance.
307    pub const XSI: XmlNamespace<'static> =
308        XmlNamespace::new_dangerous("http://www.w3.org/2001/XMLSchema-instance");
309}
310
311impl FromStr for XmlNamespace<'_> {
312    type Err = XmlNamespaceParseError;
313
314    fn from_str(value: &str) -> Result<Self, Self::Err> {
315        Self::new(value.to_owned())
316    }
317}
318
319impl Display for XmlNamespace<'_> {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        self.0.fmt(f)
322    }
323}
324
325impl Serialize for XmlNamespace<'_> {
326    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
327    where
328        S: Serializer,
329    {
330        serializer.serialize_text(&self.0)
331    }
332}
333
334impl<'de> Deserialize<'de> for XmlNamespace<'_> {
335    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
336    where
337        D: Deserializer<'de>,
338    {
339        deserializer.deserialize_any(types::string::FromStrVisitor::default())
340    }
341}
342
343/// # XML Prefix
344/// A namespace [`Prefix`] used to map a [`LocalName`] to a [`XmlNamespace`] within an XML document.
345#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
346pub struct Prefix<'a>(Cow<'a, str>);
347
348/// An error that can occur when parsing a [`Prefix`].
349#[derive(Debug, thiserror::Error, PartialEq, Eq)]
350pub enum PrefixParseError {
351    /// The [`Prefix`] is not a valid XML name.
352    #[error("Prefix has an invalid XML name: {0}")]
353    InvalidXmlName(#[from] name_tokens::InvalidXmlNameError),
354}
355
356impl<'a> Prefix<'a> {
357    /// Creates a new [`Prefix`] from a string.
358    pub fn new<T: Into<Cow<'a, str>>>(value: T) -> Result<Self, PrefixParseError> {
359        let value = value.into();
360
361        name_tokens::is_valid_name(&value)?;
362
363        Ok(Self(value))
364    }
365
366    /// Creates a new [`Prefix`] from a string without validating it, but it works in a const context.
367    ///
368    /// # Safety
369    /// This function does not validate the input value due to const context limitations involving the validation function. While this cannot create a memory safety issue, it can create a logical one.
370    pub const fn new_dangerous(value: &'a str) -> Self {
371        Self(Cow::Borrowed(value))
372    }
373
374    /// Converts this [`Prefix`] into an owned version.
375    pub fn into_owned(self) -> Prefix<'static> {
376        Prefix(Cow::Owned(self.0.into_owned()))
377    }
378
379    /// Returns this [`Prefix`] as a reference.
380    pub fn as_ref(&self) -> Prefix<'_> {
381        Prefix(Cow::Borrowed(&self.0))
382    }
383
384    /// Returns this [`Prefix`] as a string slice.
385    pub fn as_str(&self) -> &str {
386        &self.0
387    }
388
389    /// Returns this [`Prefix`] as a [`QName`] with the `xmlns` prefix. This is useful for serializing namespaces.
390    pub fn xmlns(&'a self) -> QName<'a> {
391        QName::new(
392            Prefix::new("xmlns").expect("xmlns is a valid prefix"),
393            LocalName::from(self.clone()),
394        )
395    }
396
397    /// Returns `true` if this [`Prefix`] is the default prefix.
398    pub fn is_default(&self) -> bool {
399        self.0.is_empty()
400    }
401}
402
403impl<'a> From<Prefix<'a>> for LocalName<'a> {
404    fn from(value: Prefix<'a>) -> Self {
405        LocalName(value.0)
406    }
407}
408
409impl<'a> From<Option<Prefix<'a>>> for Prefix<'a> {
410    fn from(value: Option<Prefix<'a>>) -> Self {
411        value.unwrap_or_default()
412    }
413}
414
415impl FromStr for Prefix<'_> {
416    type Err = PrefixParseError;
417
418    fn from_str(value: &str) -> Result<Self, Self::Err> {
419        Self::new(value.to_owned())
420    }
421}
422
423impl Display for Prefix<'_> {
424    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425        self.0.fmt(f)
426    }
427}
428
429impl Serialize for Prefix<'_> {
430    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
431    where
432        S: Serializer,
433    {
434        serializer.serialize_text(&self.0)
435    }
436}
437
438impl<'de> Deserialize<'de> for Prefix<'_> {
439    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
440    where
441        D: Deserializer<'de>,
442    {
443        deserializer.deserialize_any(types::string::FromStrVisitor::default())
444    }
445}
446
447/// # XML Local Name
448/// A local name of an XML element or attribute within a [`XmlNamespace`].
449///
450/// Together with a [`XmlNamespace`], it forms an [`ExpandedName`].
451#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
452pub struct LocalName<'a>(Cow<'a, str>);
453
454/// An error that can occur when parsing a [`LocalName`].
455#[derive(Debug, thiserror::Error, PartialEq, Eq)]
456pub enum LocalNameParseError {
457    /// The [`LocalName`] is not a valid XML name.
458    #[error("Local name has an invalid XML name: {0}")]
459    InvalidXmlName(#[from] name_tokens::InvalidXmlNameError),
460}
461
462impl<'a> LocalName<'a> {
463    /// Creates a new [`LocalName`] from a string.
464    pub fn new<T: Into<Cow<'a, str>>>(value: T) -> Result<Self, LocalNameParseError> {
465        let value = value.into();
466
467        name_tokens::is_valid_name(&value)?;
468
469        Ok(Self(value))
470    }
471
472    /// Creates a new [`LocalName`] from a string without validating it, but it works in a const context.
473    ///
474    /// # Safety
475    /// This function does not validate the input value due to const context limitations involving the validation function. While this cannot create a memory safety issue, it can create a logical one.
476    pub const fn new_dangerous(value: &'a str) -> Self {
477        Self(Cow::Borrowed(value))
478    }
479
480    /// Converts this [`LocalName`] into an owned version.
481    pub fn into_owned(self) -> LocalName<'static> {
482        LocalName(Cow::Owned(self.0.into_owned()))
483    }
484
485    /// Returns this [`LocalName`] as a reference.
486    pub fn as_ref(&self) -> LocalName<'_> {
487        LocalName(Cow::Borrowed(&self.0))
488    }
489}
490
491impl FromStr for LocalName<'_> {
492    type Err = LocalNameParseError;
493    fn from_str(s: &str) -> Result<Self, Self::Err> {
494        Self::new(s.to_owned())
495    }
496}
497
498impl Display for LocalName<'_> {
499    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500        self.0.fmt(f)
501    }
502}
503
504impl Serialize for LocalName<'_> {
505    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
506    where
507        S: Serializer,
508    {
509        serializer.serialize_text(&self.0)
510    }
511}
512
513impl<'de> Deserialize<'de> for LocalName<'_> {
514    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
515    where
516        D: Deserializer<'de>,
517    {
518        deserializer.deserialize_any(types::string::FromStrVisitor::default())
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    use rstest::rstest;
525
526    use super::*;
527    use std::str::FromStr;
528
529    #[rstest]
530    #[case::basic("prefix")]
531    fn test_prefix(#[case] prefix_text: &str) {
532        let prefix = Prefix::from_str(prefix_text).unwrap();
533        assert_eq!(prefix.to_string(), prefix_text);
534        assert_eq!(prefix.into_owned().to_string(), prefix_text);
535    }
536
537    #[rstest]
538    #[case::empty("", PrefixParseError::InvalidXmlName(InvalidXmlNameError::Empty))]
539    #[case::space("invalid prefix", PrefixParseError::InvalidXmlName(InvalidXmlNameError::InvalidChar { index: 7, character: ' ' }))]
540    fn invalid_prefix_invalid_characters(
541        #[case] prefix: &str,
542        #[case] expected_error: PrefixParseError,
543    ) {
544        let error = Prefix::from_str(prefix).unwrap_err();
545        assert_eq!(error, expected_error);
546    }
547
548    #[rstest]
549    #[case::basic("localName")]
550    fn test_local_name(#[case] local_name_text: &str) {
551        let local_name = LocalName::from_str(local_name_text).unwrap();
552        assert_eq!(local_name.to_string(), local_name_text);
553        assert_eq!(local_name.into_owned().to_string(), local_name_text);
554    }
555
556    #[rstest]
557    #[case::empty("", LocalNameParseError::InvalidXmlName(InvalidXmlNameError::Empty))]
558    #[case::space("invalid localName", LocalNameParseError::InvalidXmlName(InvalidXmlNameError::InvalidChar { index: 7, character: ' ' }))]
559    fn invalid_local_name_invalid_characters(
560        #[case] local_name: &str,
561        #[case] expected_error: LocalNameParseError,
562    ) {
563        let error = LocalName::from_str(local_name).unwrap_err();
564        assert_eq!(error, expected_error);
565    }
566
567    #[rstest]
568    #[case::basic("localName", None)]
569    #[case::with_namespace("localName", Some(XmlNamespace::new("http://example.com").unwrap()))]
570    fn test_expanded_name(#[case] local_name_text: &str, #[case] namespace: Option<XmlNamespace>) {
571        let local_name = LocalName::from_str(local_name_text).unwrap();
572        let expanded_name = ExpandedName::new(local_name.clone(), namespace.clone());
573        assert_eq!(expanded_name.local_name(), &local_name);
574        assert_eq!(expanded_name.namespace(), namespace.as_ref());
575        assert_eq!(expanded_name.to_string(), local_name_text);
576        assert_eq!(expanded_name.into_owned().to_string(), local_name_text);
577    }
578
579    #[rstest]
580    #[case::basic("prefix:localName")]
581    fn test_qname(#[case] qname_text: &str) {
582        let qname = QName::from_str(qname_text).unwrap();
583        assert_eq!(qname.to_string(), qname_text);
584        assert_eq!(qname.into_owned().to_string(), qname_text);
585    }
586
587    #[rstest]
588    #[case::invalid_local_name("prefix:invalid localName", QNameParseError::InvalidLocalName(LocalNameParseError::InvalidXmlName(InvalidXmlNameError::InvalidChar { index: 7, character: ' ' })))]
589    fn invalid_qname_invalid_characters(
590        #[case] qname: &str,
591        #[case] expected_error: QNameParseError,
592    ) {
593        let error = QName::from_str(qname).unwrap_err();
594        assert_eq!(error, expected_error);
595    }
596}