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::str;
18use std::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 use types::value::XmlValue;
28
29#[cfg(feature = "derive")]
30extern crate xmlity_derive;
31
32#[cfg(feature = "derive")]
33pub use xmlity_derive::{
34    DeserializationGroup, Deserialize, SerializationGroup, Serialize, SerializeAttribute,
35};
36
37// Reference: https://www.w3.org/TR/xml/#sec-common-syn
38// [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]
39// [4a]   	NameChar	   ::=   	NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
40// [5]   	Name	   ::=   	NameStartChar (NameChar)*
41// [6]   	Names	   ::=   	Name (#x20 Name)*
42// [7]   	Nmtoken	   ::=   	(NameChar)+
43// [8]   	Nmtokens	   ::=   	Nmtoken (#x20 Nmtoken)*
44mod name_tokens {
45    const fn is_name_start_char(c: char) -> bool {
46        matches!(
47            //Deliberately excluding : as we handle it separately
48            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}'
49        )
50    }
51    const fn is_name_char(c: char) -> bool {
52        is_name_start_char(c)
53            || matches!(c, '-' | '.' | '0'..='9' | '\u{00B7}' | '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}')
54    }
55
56    /// An error that occurs when a name is invalid.
57    #[derive(Debug, Clone, thiserror::Error, PartialEq, Eq, PartialOrd, Ord, Hash)]
58    #[non_exhaustive]
59    pub enum InvalidXmlNameError {
60        /// 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.
61        #[error("Invalid start character")]
62        InvalidStartChar,
63        /// The name contains an invalid character.
64        #[error("Invalid character at index {index}")]
65        InvalidChar {
66            /// The index of the invalid character.
67            index: usize,
68            /// The invalid character.
69            character: char,
70        },
71        /// The name is empty.
72        #[error("Empty name")]
73        Empty,
74    }
75
76    pub fn is_valid_name(name: &str) -> Result<(), InvalidXmlNameError> {
77        let mut chars = name.chars();
78        if let Some(c) = chars.next() {
79            if !is_name_start_char(c) {
80                return Err(InvalidXmlNameError::InvalidStartChar);
81            }
82        } else {
83            return Err(InvalidXmlNameError::Empty);
84        }
85        for (index, character) in chars.enumerate().map(|(i, c)| (i + 1, c)) {
86            if !is_name_char(character) {
87                return Err(InvalidXmlNameError::InvalidChar { index, character });
88            }
89        }
90
91        Ok(())
92    }
93}
94
95pub use name_tokens::InvalidXmlNameError;
96
97/// # XML Expanded Name
98/// An [`ExpandedName`] is a [`LocalName`] together with its associated [`XmlNamespace`]. This can convert to and from a [`QName`] with a [`Prefix`] mapping.
99#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
100pub struct ExpandedName<'a> {
101    local_name: LocalName<'a>,
102    namespace: Option<XmlNamespace<'a>>,
103}
104
105impl<'a> ExpandedName<'a> {
106    /// Creates a new [`ExpandedName`].
107    pub fn new(local_name: LocalName<'a>, namespace: Option<XmlNamespace<'a>>) -> Self {
108        Self {
109            local_name,
110            namespace,
111        }
112    }
113
114    /// Converts this [`ExpandedName`] into its parts.
115    pub fn into_parts(self) -> (LocalName<'a>, Option<XmlNamespace<'a>>) {
116        (self.local_name, self.namespace)
117    }
118
119    /// Converts this [`ExpandedName`] into an owned version.
120    pub fn into_owned(self) -> ExpandedName<'static> {
121        ExpandedName::new(
122            self.local_name.into_owned(),
123            self.namespace.map(|n| n.into_owned()),
124        )
125    }
126
127    /// Returns the local name of this [`ExpandedName`].
128    pub fn local_name(&self) -> &LocalName<'_> {
129        &self.local_name
130    }
131
132    /// Returns the namespace of this [`ExpandedName`].
133    pub fn namespace(&self) -> Option<&XmlNamespace<'_>> {
134        self.namespace.as_ref()
135    }
136
137    /// Converts this [`ExpandedName`] into a [`QName`] name using the given [`Prefix`].
138    pub fn to_q_name(self, resolved_prefix: Option<Prefix<'a>>) -> QName<'a> {
139        QName::new(resolved_prefix, self.local_name.clone())
140    }
141}
142
143impl Display for ExpandedName<'_> {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        self.local_name.fmt(f)
146    }
147}
148
149/// # XML Qualified Name
150/// A [`QName`] is a [`LocalName`] together with a namespace [`Prefix`], indicating it belongs to a specific declared [`XmlNamespace`].
151#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
152pub struct QName<'a> {
153    prefix: Option<Prefix<'a>>,
154    local_name: LocalName<'a>,
155}
156
157/// An error that can occur when parsing a [`QName`].
158#[derive(Debug, thiserror::Error, PartialEq, Eq)]
159pub enum QNameParseError {
160    /// The [`Prefix`] is invalid.
161    #[error("Invalid prefix: {0}")]
162    InvalidPrefix(#[from] PrefixParseError),
163    /// The [`LocalName`] is invalid.
164    #[error("Invalid local name: {0}")]
165    InvalidLocalName(#[from] LocalNameParseError),
166}
167
168impl<'a> QName<'a> {
169    /// Creates a new [`QName`].
170    pub fn new<P: Into<Option<Prefix<'a>>>, L: Into<LocalName<'a>>>(
171        prefix: P,
172        local_name: L,
173    ) -> Self {
174        QName {
175            prefix: prefix.into(),
176            local_name: local_name.into(),
177        }
178    }
179
180    /// Converts this [`QName`] into its parts.
181    pub fn into_parts(self) -> (Option<Prefix<'a>>, LocalName<'a>) {
182        (self.prefix, self.local_name)
183    }
184
185    /// Converts this [`QName`] into being owned.
186    pub fn into_owned(self) -> QName<'static> {
187        QName {
188            prefix: self.prefix.map(|prefix| prefix.into_owned()),
189            local_name: self.local_name.into_owned(),
190        }
191    }
192
193    /// Returns the [`Prefix`] of this [`QName`].
194    pub fn prefix(&self) -> Option<&Prefix<'a>> {
195        self.prefix.as_ref()
196    }
197
198    /// Returns the [`LocalName`] of this [`QName`].
199    pub fn local_name(&self) -> &LocalName<'a> {
200        &self.local_name
201    }
202}
203
204impl Display for QName<'_> {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        if let Some(prefix) = &self.prefix.as_ref().filter(|prefix| !prefix.is_default()) {
207            write!(f, "{}:{}", prefix, self.local_name)
208        } else {
209            write!(f, "{}", self.local_name)
210        }
211    }
212}
213impl FromStr for QName<'_> {
214    type Err = QNameParseError;
215    fn from_str(s: &str) -> Result<Self, Self::Err> {
216        let (prefix, local_name) = s.split_once(':').unwrap_or(("", s));
217
218        let prefix = Prefix::from_str(prefix)?;
219        let local_name = LocalName::from_str(local_name)?;
220
221        Ok(QName::new(prefix, local_name))
222    }
223}
224
225impl<'a> From<QName<'a>> for Option<Prefix<'a>> {
226    fn from(value: QName<'a>) -> Self {
227        value.prefix
228    }
229}
230
231impl<'a> From<QName<'a>> for LocalName<'a> {
232    fn from(value: QName<'a>) -> Self {
233        value.local_name
234    }
235}
236
237/// # XML Namespace
238/// A namespace URI, to which [`LocalNames`](`LocalName`) are scoped under.
239#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
240pub struct XmlNamespace<'a>(Cow<'a, str>);
241
242/// An error that can occur when parsing a [`XmlNamespace`].
243#[derive(Debug, thiserror::Error)]
244pub enum XmlNamespaceParseError {}
245
246impl FromStr for XmlNamespace<'static> {
247    type Err = XmlNamespaceParseError;
248
249    fn from_str(value: &str) -> Result<Self, Self::Err> {
250        Self::new(value.to_owned())
251    }
252}
253
254impl<'a> XmlNamespace<'a> {
255    /// Creates a new [`XmlNamespace`] from a string.
256    pub fn new<T: Into<Cow<'a, str>>>(value: T) -> Result<Self, XmlNamespaceParseError> {
257        Ok(Self(value.into()))
258    }
259
260    /// Creates a new [`XmlNamespace`] from a string without validating it, but it works in a const context.
261    ///
262    /// # Safety
263    /// 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.
264    pub const fn new_dangerous(value: &'a str) -> Self {
265        Self(Cow::Borrowed(value))
266    }
267
268    /// Converts this [`XmlNamespace`] into an owned version.
269    pub fn into_owned(self) -> XmlNamespace<'static> {
270        XmlNamespace(Cow::Owned(self.0.into_owned()))
271    }
272
273    /// Returns this [`XmlNamespace`] as a string slice.
274    pub fn as_str(&self) -> &str {
275        &self.0
276    }
277}
278
279/// # XML Prefix
280/// A namespace [`Prefix`] used to map a [`LocalName`] to a [`XmlNamespace`] within an XML document.
281#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
282pub struct Prefix<'a>(Cow<'a, str>);
283
284/// An error that can occur when parsing a [`Prefix`].
285#[derive(Debug, thiserror::Error, PartialEq, Eq)]
286pub enum PrefixParseError {
287    /// The [`Prefix`] is not a valid XML name.
288    #[error("Prefix has an invalid XML name: {0}")]
289    InvalidXmlName(#[from] name_tokens::InvalidXmlNameError),
290}
291
292impl<'a> Prefix<'a> {
293    /// Creates a new [`Prefix`] from a string.
294    pub fn new<T: Into<Cow<'a, str>>>(value: T) -> Result<Self, PrefixParseError> {
295        let value = value.into();
296
297        name_tokens::is_valid_name(&value)?;
298
299        Ok(Self(value))
300    }
301
302    /// Creates a new [`Prefix`] from a string without validating it, but it works in a const context.
303    ///
304    /// # Safety
305    /// 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.
306    pub const fn new_dangerous(value: &'a str) -> Self {
307        Self(Cow::Borrowed(value))
308    }
309
310    /// Converts this [`Prefix`] into an owned version.
311    pub fn into_owned(self) -> Prefix<'static> {
312        Prefix(Cow::Owned(self.0.into_owned()))
313    }
314
315    /// Returns this [`Prefix`] as a string slice.
316    pub fn as_str(&self) -> &str {
317        &self.0
318    }
319
320    /// Returns this [`Prefix`] as a [`QName`] with the `xmlns` prefix. This is useful for serializing namespaces.
321    pub fn xmlns(&'a self) -> QName<'a> {
322        QName::new(
323            Prefix::new("xmlns").expect("xmlns is a valid prefix"),
324            LocalName::from(self.clone()),
325        )
326    }
327
328    /// Returns `true` if this [`Prefix`] is the default prefix.
329    pub fn is_default(&self) -> bool {
330        self.0.is_empty()
331    }
332}
333
334impl<'a> From<Prefix<'a>> for LocalName<'a> {
335    fn from(value: Prefix<'a>) -> Self {
336        LocalName(value.0)
337    }
338}
339
340impl<'a> From<Option<Prefix<'a>>> for Prefix<'a> {
341    fn from(value: Option<Prefix<'a>>) -> Self {
342        value.unwrap_or_default()
343    }
344}
345
346impl FromStr for Prefix<'static> {
347    type Err = PrefixParseError;
348
349    fn from_str(value: &str) -> Result<Self, Self::Err> {
350        Self::new(value.to_owned())
351    }
352}
353
354impl Display for Prefix<'_> {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        self.0.fmt(f)
357    }
358}
359
360/// # XML Local Name
361/// A local name of an XML element or attribute within a [`XmlNamespace`].
362///
363/// Together with a [`XmlNamespace`], it forms an [`ExpandedName`].
364#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
365pub struct LocalName<'a>(Cow<'a, str>);
366
367/// An error that can occur when parsing a [`LocalName`].
368#[derive(Debug, thiserror::Error, PartialEq, Eq)]
369pub enum LocalNameParseError {
370    /// The [`LocalName`] is not a valid XML name.
371    #[error("Local name has an invalid XML name: {0}")]
372    InvalidXmlName(#[from] name_tokens::InvalidXmlNameError),
373}
374
375impl<'a> LocalName<'a> {
376    /// Creates a new [`LocalName`] from a string.
377    pub fn new<T: Into<Cow<'a, str>>>(value: T) -> Result<Self, LocalNameParseError> {
378        let value = value.into();
379
380        name_tokens::is_valid_name(&value)?;
381
382        Ok(Self(value))
383    }
384
385    /// Creates a new [`LocalName`] from a string without validating it, but it works in a const context.
386    ///
387    /// # Safety
388    /// 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.
389    pub const fn new_dangerous(value: &'a str) -> Self {
390        Self(Cow::Borrowed(value))
391    }
392
393    /// Converts this [`LocalName`] into an owned version.
394    pub fn into_owned(self) -> LocalName<'static> {
395        LocalName(Cow::Owned(self.0.into_owned()))
396    }
397}
398
399impl FromStr for LocalName<'_> {
400    type Err = LocalNameParseError;
401    fn from_str(s: &str) -> Result<Self, Self::Err> {
402        Self::new(s.to_owned())
403    }
404}
405
406impl Display for LocalName<'_> {
407    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408        self.0.fmt(f)
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use rstest::rstest;
415
416    use super::*;
417    use std::str::FromStr;
418
419    #[rstest]
420    #[case::basic("prefix")]
421    fn test_prefix(#[case] prefix_text: &str) {
422        let prefix = Prefix::from_str(prefix_text).unwrap();
423        assert_eq!(prefix.to_string(), prefix_text);
424        assert_eq!(prefix.into_owned().to_string(), prefix_text);
425    }
426
427    #[rstest]
428    #[case::empty("", PrefixParseError::InvalidXmlName(InvalidXmlNameError::Empty))]
429    #[case::space("invalid prefix", PrefixParseError::InvalidXmlName(InvalidXmlNameError::InvalidChar { index: 7, character: ' ' }))]
430    fn invalid_prefix_invalid_characters(
431        #[case] prefix: &str,
432        #[case] expected_error: PrefixParseError,
433    ) {
434        let error = Prefix::from_str(prefix).unwrap_err();
435        assert_eq!(error, expected_error);
436    }
437
438    #[rstest]
439    #[case::basic("localName")]
440    fn test_local_name(#[case] local_name_text: &str) {
441        let local_name = LocalName::from_str(local_name_text).unwrap();
442        assert_eq!(local_name.to_string(), local_name_text);
443        assert_eq!(local_name.into_owned().to_string(), local_name_text);
444    }
445
446    #[rstest]
447    #[case::empty("", LocalNameParseError::InvalidXmlName(InvalidXmlNameError::Empty))]
448    #[case::space("invalid localName", LocalNameParseError::InvalidXmlName(InvalidXmlNameError::InvalidChar { index: 7, character: ' ' }))]
449    fn invalid_local_name_invalid_characters(
450        #[case] local_name: &str,
451        #[case] expected_error: LocalNameParseError,
452    ) {
453        let error = LocalName::from_str(local_name).unwrap_err();
454        assert_eq!(error, expected_error);
455    }
456
457    #[rstest]
458    #[case::basic("localName", None)]
459    #[case::with_namespace("localName", Some(XmlNamespace::new("http://example.com").unwrap()))]
460    fn test_expanded_name(#[case] local_name_text: &str, #[case] namespace: Option<XmlNamespace>) {
461        let local_name = LocalName::from_str(local_name_text).unwrap();
462        let expanded_name = ExpandedName::new(local_name.clone(), namespace.clone());
463        assert_eq!(expanded_name.local_name(), &local_name);
464        assert_eq!(expanded_name.namespace(), namespace.as_ref());
465        assert_eq!(expanded_name.to_string(), local_name_text);
466        assert_eq!(expanded_name.into_owned().to_string(), local_name_text);
467    }
468
469    #[rstest]
470    #[case::basic("prefix:localName")]
471    fn test_qname(#[case] qname_text: &str) {
472        let qname = QName::from_str(qname_text).unwrap();
473        assert_eq!(qname.to_string(), qname_text);
474        assert_eq!(qname.into_owned().to_string(), qname_text);
475    }
476
477    #[rstest]
478    #[case::invalid_local_name("prefix:invalid localName", QNameParseError::InvalidLocalName(LocalNameParseError::InvalidXmlName(InvalidXmlNameError::InvalidChar { index: 7, character: ' ' })))]
479    fn invalid_qname_invalid_characters(
480        #[case] qname: &str,
481        #[case] expected_error: QNameParseError,
482    ) {
483        let error = QName::from_str(qname).unwrap_err();
484        assert_eq!(error, expected_error);
485    }
486}