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 an owned version.
115    pub fn into_owned(self) -> ExpandedName<'static> {
116        ExpandedName::new(
117            self.local_name.into_owned(),
118            self.namespace.map(|n| n.into_owned()),
119        )
120    }
121
122    /// Returns the local name of this [`ExpandedName`].
123    pub fn local_name(&self) -> &LocalName<'_> {
124        &self.local_name
125    }
126
127    /// Returns the namespace of this [`ExpandedName`].
128    pub fn namespace(&self) -> Option<&XmlNamespace<'_>> {
129        self.namespace.as_ref()
130    }
131
132    /// Converts this [`ExpandedName`] into a [`QName`] name using the given [`Prefix`].
133    pub fn to_q_name(self, resolved_prefix: Option<Prefix<'a>>) -> QName<'a> {
134        QName::new(resolved_prefix, self.local_name.clone())
135    }
136}
137
138impl Display for ExpandedName<'_> {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        self.local_name.fmt(f)
141    }
142}
143
144/// # XML Qualified Name
145/// A [`QName`] is a [`LocalName`] together with a namespace [`Prefix`], indicating it belongs to a specific declared [`XmlNamespace`].
146#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
147pub struct QName<'a> {
148    prefix: Option<Prefix<'a>>,
149    local_name: LocalName<'a>,
150}
151
152/// An error that can occur when parsing a [`QName`].
153#[derive(Debug, thiserror::Error, PartialEq, Eq)]
154pub enum QNameParseError {
155    /// The [`Prefix`] is invalid.
156    #[error("Invalid prefix: {0}")]
157    InvalidPrefix(#[from] PrefixParseError),
158    /// The [`LocalName`] is invalid.
159    #[error("Invalid local name: {0}")]
160    InvalidLocalName(#[from] LocalNameParseError),
161}
162
163impl<'a> QName<'a> {
164    /// Creates a new [`QName`].
165    pub fn new<P: Into<Option<Prefix<'a>>>, L: Into<LocalName<'a>>>(
166        prefix: P,
167        local_name: L,
168    ) -> Self {
169        QName {
170            prefix: prefix.into(),
171            local_name: local_name.into(),
172        }
173    }
174
175    /// Converts this [`QName`] into being owned.
176    pub fn into_owned(self) -> QName<'static> {
177        QName {
178            prefix: self.prefix.map(|prefix| prefix.into_owned()),
179            local_name: self.local_name.into_owned(),
180        }
181    }
182
183    /// Returns the [`Prefix`] of this [`QName`].
184    pub fn prefix(&self) -> Option<&Prefix<'a>> {
185        self.prefix.as_ref()
186    }
187
188    /// Returns the [`LocalName`] of this [`QName`].
189    pub fn local_name(&self) -> &LocalName<'a> {
190        &self.local_name
191    }
192}
193
194impl Display for QName<'_> {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        if let Some(prefix) = &self.prefix.as_ref().filter(|prefix| !prefix.is_default()) {
197            write!(f, "{}:{}", prefix, self.local_name)
198        } else {
199            write!(f, "{}", self.local_name)
200        }
201    }
202}
203impl FromStr for QName<'_> {
204    type Err = QNameParseError;
205    fn from_str(s: &str) -> Result<Self, Self::Err> {
206        let (prefix, local_name) = s.split_once(':').unwrap_or(("", s));
207
208        let prefix = Prefix::from_str(prefix)?;
209        let local_name = LocalName::from_str(local_name)?;
210
211        Ok(QName::new(prefix, local_name))
212    }
213}
214
215impl<'a> From<QName<'a>> for Option<Prefix<'a>> {
216    fn from(value: QName<'a>) -> Self {
217        value.prefix
218    }
219}
220
221impl<'a> From<QName<'a>> for LocalName<'a> {
222    fn from(value: QName<'a>) -> Self {
223        value.local_name
224    }
225}
226
227/// # XML Namespace
228/// A namespace URI, to which [`LocalNames`](`LocalName`) are scoped under.
229#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
230pub struct XmlNamespace<'a>(Cow<'a, str>);
231
232/// An error that can occur when parsing a [`XmlNamespace`].
233#[derive(Debug, thiserror::Error)]
234pub enum XmlNamespaceParseError {}
235
236impl FromStr for XmlNamespace<'static> {
237    type Err = XmlNamespaceParseError;
238
239    fn from_str(value: &str) -> Result<Self, Self::Err> {
240        Self::new(value.to_owned())
241    }
242}
243
244impl<'a> XmlNamespace<'a> {
245    /// Creates a new [`XmlNamespace`] from a string.
246    pub fn new<T: Into<Cow<'a, str>>>(value: T) -> Result<Self, XmlNamespaceParseError> {
247        Ok(Self(value.into()))
248    }
249
250    /// Creates a new [`XmlNamespace`] from a string without validating it, but it works in a const context.
251    ///
252    /// # Safety
253    /// 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.
254    pub const fn new_dangerous(value: &'a str) -> Self {
255        Self(Cow::Borrowed(value))
256    }
257
258    /// Converts this [`XmlNamespace`] into an owned version.
259    pub fn into_owned(self) -> XmlNamespace<'static> {
260        XmlNamespace(Cow::Owned(self.0.into_owned()))
261    }
262
263    /// Returns this [`XmlNamespace`] as a string slice.
264    pub fn as_str(&self) -> &str {
265        &self.0
266    }
267}
268
269/// # XML Prefix
270/// A namespace [`Prefix`] used to map a [`LocalName`] to a [`XmlNamespace`] within an XML document.
271#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
272pub struct Prefix<'a>(Cow<'a, str>);
273
274/// An error that can occur when parsing a [`Prefix`].
275#[derive(Debug, thiserror::Error, PartialEq, Eq)]
276pub enum PrefixParseError {
277    /// The [`Prefix`] is not a valid XML name.
278    #[error("Prefix has an invalid XML name: {0}")]
279    InvalidXmlName(#[from] name_tokens::InvalidXmlNameError),
280}
281
282impl<'a> Prefix<'a> {
283    /// Creates a new [`Prefix`] from a string.
284    pub fn new<T: Into<Cow<'a, str>>>(value: T) -> Result<Self, PrefixParseError> {
285        let value = value.into();
286
287        name_tokens::is_valid_name(&value)?;
288
289        Ok(Self(value))
290    }
291
292    /// Creates a new [`Prefix`] from a string without validating it, but it works in a const context.
293    ///
294    /// # Safety
295    /// 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.
296    pub const fn new_dangerous(value: &'a str) -> Result<Self, PrefixParseError> {
297        Ok(Self(Cow::Borrowed(value)))
298    }
299
300    /// Converts this [`Prefix`] into an owned version.
301    pub fn into_owned(self) -> Prefix<'static> {
302        Prefix(Cow::Owned(self.0.into_owned()))
303    }
304
305    /// Returns this [`Prefix`] as a string slice.
306    pub fn as_str(&self) -> &str {
307        &self.0
308    }
309
310    /// Returns this [`Prefix`] as a [`QName`] with the `xmlns` prefix. This is useful for serializing namespaces.
311    pub fn xmlns(&'a self) -> QName<'a> {
312        QName::new(
313            Prefix::new("xmlns").expect("xmlns is a valid prefix"),
314            LocalName::from(self.clone()),
315        )
316    }
317
318    /// Returns `true` if this [`Prefix`] is the default prefix.
319    pub fn is_default(&self) -> bool {
320        self.0.is_empty()
321    }
322}
323
324impl<'a> From<Prefix<'a>> for LocalName<'a> {
325    fn from(value: Prefix<'a>) -> Self {
326        LocalName(value.0)
327    }
328}
329
330impl<'a> From<Option<Prefix<'a>>> for Prefix<'a> {
331    fn from(value: Option<Prefix<'a>>) -> Self {
332        value.unwrap_or_default()
333    }
334}
335
336impl FromStr for Prefix<'static> {
337    type Err = PrefixParseError;
338
339    fn from_str(value: &str) -> Result<Self, Self::Err> {
340        Self::new(value.to_owned())
341    }
342}
343
344impl Display for Prefix<'_> {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        self.0.fmt(f)
347    }
348}
349
350/// # XML Local Name
351/// A local name of an XML element or attribute within a [`XmlNamespace`].
352///
353/// Together with a [`XmlNamespace`], it forms an [`ExpandedName`].
354#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
355pub struct LocalName<'a>(Cow<'a, str>);
356
357/// An error that can occur when parsing a [`LocalName`].
358#[derive(Debug, thiserror::Error, PartialEq, Eq)]
359pub enum LocalNameParseError {
360    /// The [`LocalName`] is not a valid XML name.
361    #[error("Local name has an invalid XML name: {0}")]
362    InvalidXmlName(#[from] name_tokens::InvalidXmlNameError),
363}
364
365impl<'a> LocalName<'a> {
366    /// Creates a new [`LocalName`] from a string.
367    pub fn new<T: Into<Cow<'a, str>>>(value: T) -> Result<Self, LocalNameParseError> {
368        let value = value.into();
369
370        name_tokens::is_valid_name(&value)?;
371
372        Ok(Self(value))
373    }
374
375    /// Creates a new [`LocalName`] from a string without validating it, but it works in a const context.
376    ///
377    /// # Safety
378    /// 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.
379    pub const fn new_dangerous(value: &'a str) -> Result<Self, PrefixParseError> {
380        Ok(Self(Cow::Borrowed(value)))
381    }
382
383    /// Converts this [`LocalName`] into an owned version.
384    pub fn into_owned(self) -> LocalName<'static> {
385        LocalName(Cow::Owned(self.0.into_owned()))
386    }
387}
388
389impl FromStr for LocalName<'_> {
390    type Err = LocalNameParseError;
391    fn from_str(s: &str) -> Result<Self, Self::Err> {
392        Self::new(s.to_owned())
393    }
394}
395
396impl Display for LocalName<'_> {
397    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398        self.0.fmt(f)
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use rstest::rstest;
405
406    use super::*;
407    use std::str::FromStr;
408
409    #[rstest]
410    #[case::basic("prefix")]
411    fn test_prefix(#[case] prefix_text: &str) {
412        let prefix = Prefix::from_str(prefix_text).unwrap();
413        assert_eq!(prefix.to_string(), prefix_text);
414        assert_eq!(prefix.into_owned().to_string(), prefix_text);
415    }
416
417    #[rstest]
418    #[case::empty("", PrefixParseError::InvalidXmlName(InvalidXmlNameError::Empty))]
419    #[case::space("invalid prefix", PrefixParseError::InvalidXmlName(InvalidXmlNameError::InvalidChar { index: 7, character: ' ' }))]
420    fn invalid_prefix_invalid_characters(
421        #[case] prefix: &str,
422        #[case] expected_error: PrefixParseError,
423    ) {
424        let error = Prefix::from_str(prefix).unwrap_err();
425        assert_eq!(error, expected_error);
426    }
427
428    #[rstest]
429    #[case::basic("localName")]
430    fn test_local_name(#[case] local_name_text: &str) {
431        let local_name = LocalName::from_str(local_name_text).unwrap();
432        assert_eq!(local_name.to_string(), local_name_text);
433        assert_eq!(local_name.into_owned().to_string(), local_name_text);
434    }
435
436    #[rstest]
437    #[case::empty("", LocalNameParseError::InvalidXmlName(InvalidXmlNameError::Empty))]
438    #[case::space("invalid localName", LocalNameParseError::InvalidXmlName(InvalidXmlNameError::InvalidChar { index: 7, character: ' ' }))]
439    fn invalid_local_name_invalid_characters(
440        #[case] local_name: &str,
441        #[case] expected_error: LocalNameParseError,
442    ) {
443        let error = LocalName::from_str(local_name).unwrap_err();
444        assert_eq!(error, expected_error);
445    }
446
447    #[rstest]
448    #[case::basic("localName", None)]
449    #[case::with_namespace("localName", Some(XmlNamespace::new("http://example.com").unwrap()))]
450    fn test_expanded_name(#[case] local_name_text: &str, #[case] namespace: Option<XmlNamespace>) {
451        let local_name = LocalName::from_str(local_name_text).unwrap();
452        let expanded_name = ExpandedName::new(local_name.clone(), namespace.clone());
453        assert_eq!(expanded_name.local_name(), &local_name);
454        assert_eq!(expanded_name.namespace(), namespace.as_ref());
455        assert_eq!(expanded_name.to_string(), local_name_text);
456        assert_eq!(expanded_name.into_owned().to_string(), local_name_text);
457    }
458
459    #[rstest]
460    #[case::basic("prefix:localName")]
461    fn test_qname(#[case] qname_text: &str) {
462        let qname = QName::from_str(qname_text).unwrap();
463        assert_eq!(qname.to_string(), qname_text);
464        assert_eq!(qname.into_owned().to_string(), qname_text);
465    }
466
467    #[rstest]
468    #[case::invalid_local_name("prefix:invalid localName", QNameParseError::InvalidLocalName(LocalNameParseError::InvalidXmlName(InvalidXmlNameError::InvalidChar { index: 7, character: ' ' })))]
469    fn invalid_qname_invalid_characters(
470        #[case] qname: &str,
471        #[case] expected_error: QNameParseError,
472    ) {
473        let error = QName::from_str(qname).unwrap_err();
474        assert_eq!(error, expected_error);
475    }
476}