1#![warn(missing_docs)]
2use 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
37mod name_tokens {
45 const fn is_name_start_char(c: char) -> bool {
46 matches!(
47 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 #[derive(Debug, Clone, thiserror::Error, PartialEq, Eq, PartialOrd, Ord, Hash)]
58 #[non_exhaustive]
59 pub enum InvalidXmlNameError {
60 #[error("Invalid start character")]
62 InvalidStartChar,
63 #[error("Invalid character at index {index}")]
65 InvalidChar {
66 index: usize,
68 character: char,
70 },
71 #[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#[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 pub fn new(local_name: LocalName<'a>, namespace: Option<XmlNamespace<'a>>) -> Self {
108 Self {
109 local_name,
110 namespace,
111 }
112 }
113
114 pub fn into_parts(self) -> (LocalName<'a>, Option<XmlNamespace<'a>>) {
116 (self.local_name, self.namespace)
117 }
118
119 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 pub fn local_name(&self) -> &LocalName<'_> {
129 &self.local_name
130 }
131
132 pub fn namespace(&self) -> Option<&XmlNamespace<'_>> {
134 self.namespace.as_ref()
135 }
136
137 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#[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#[derive(Debug, thiserror::Error, PartialEq, Eq)]
159pub enum QNameParseError {
160 #[error("Invalid prefix: {0}")]
162 InvalidPrefix(#[from] PrefixParseError),
163 #[error("Invalid local name: {0}")]
165 InvalidLocalName(#[from] LocalNameParseError),
166}
167
168impl<'a> QName<'a> {
169 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 pub fn into_parts(self) -> (Option<Prefix<'a>>, LocalName<'a>) {
182 (self.prefix, self.local_name)
183 }
184
185 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 pub fn prefix(&self) -> Option<&Prefix<'a>> {
195 self.prefix.as_ref()
196 }
197
198 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#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
240pub struct XmlNamespace<'a>(Cow<'a, str>);
241
242#[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 pub fn new<T: Into<Cow<'a, str>>>(value: T) -> Result<Self, XmlNamespaceParseError> {
257 Ok(Self(value.into()))
258 }
259
260 pub const fn new_dangerous(value: &'a str) -> Self {
265 Self(Cow::Borrowed(value))
266 }
267
268 pub fn into_owned(self) -> XmlNamespace<'static> {
270 XmlNamespace(Cow::Owned(self.0.into_owned()))
271 }
272
273 pub fn as_str(&self) -> &str {
275 &self.0
276 }
277}
278
279#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
282pub struct Prefix<'a>(Cow<'a, str>);
283
284#[derive(Debug, thiserror::Error, PartialEq, Eq)]
286pub enum PrefixParseError {
287 #[error("Prefix has an invalid XML name: {0}")]
289 InvalidXmlName(#[from] name_tokens::InvalidXmlNameError),
290}
291
292impl<'a> Prefix<'a> {
293 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 pub const fn new_dangerous(value: &'a str) -> Self {
307 Self(Cow::Borrowed(value))
308 }
309
310 pub fn into_owned(self) -> Prefix<'static> {
312 Prefix(Cow::Owned(self.0.into_owned()))
313 }
314
315 pub fn as_str(&self) -> &str {
317 &self.0
318 }
319
320 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 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#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
365pub struct LocalName<'a>(Cow<'a, str>);
366
367#[derive(Debug, thiserror::Error, PartialEq, Eq)]
369pub enum LocalNameParseError {
370 #[error("Local name has an invalid XML name: {0}")]
372 InvalidXmlName(#[from] name_tokens::InvalidXmlNameError),
373}
374
375impl<'a> LocalName<'a> {
376 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 pub const fn new_dangerous(value: &'a str) -> Self {
390 Self(Cow::Borrowed(value))
391 }
392
393 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}