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_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 pub fn local_name(&self) -> &LocalName<'_> {
124 &self.local_name
125 }
126
127 pub fn namespace(&self) -> Option<&XmlNamespace<'_>> {
129 self.namespace.as_ref()
130 }
131
132 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#[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#[derive(Debug, thiserror::Error, PartialEq, Eq)]
154pub enum QNameParseError {
155 #[error("Invalid prefix: {0}")]
157 InvalidPrefix(#[from] PrefixParseError),
158 #[error("Invalid local name: {0}")]
160 InvalidLocalName(#[from] LocalNameParseError),
161}
162
163impl<'a> QName<'a> {
164 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 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 pub fn prefix(&self) -> Option<&Prefix<'a>> {
185 self.prefix.as_ref()
186 }
187
188 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#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
230pub struct XmlNamespace<'a>(Cow<'a, str>);
231
232#[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 pub fn new<T: Into<Cow<'a, str>>>(value: T) -> Result<Self, XmlNamespaceParseError> {
247 Ok(Self(value.into()))
248 }
249
250 pub const fn new_dangerous(value: &'a str) -> Self {
255 Self(Cow::Borrowed(value))
256 }
257
258 pub fn into_owned(self) -> XmlNamespace<'static> {
260 XmlNamespace(Cow::Owned(self.0.into_owned()))
261 }
262
263 pub fn as_str(&self) -> &str {
265 &self.0
266 }
267}
268
269#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
272pub struct Prefix<'a>(Cow<'a, str>);
273
274#[derive(Debug, thiserror::Error, PartialEq, Eq)]
276pub enum PrefixParseError {
277 #[error("Prefix has an invalid XML name: {0}")]
279 InvalidXmlName(#[from] name_tokens::InvalidXmlNameError),
280}
281
282impl<'a> Prefix<'a> {
283 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 pub const fn new_dangerous(value: &'a str) -> Result<Self, PrefixParseError> {
297 Ok(Self(Cow::Borrowed(value)))
298 }
299
300 pub fn into_owned(self) -> Prefix<'static> {
302 Prefix(Cow::Owned(self.0.into_owned()))
303 }
304
305 pub fn as_str(&self) -> &str {
307 &self.0
308 }
309
310 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 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#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
355pub struct LocalName<'a>(Cow<'a, str>);
356
357#[derive(Debug, thiserror::Error, PartialEq, Eq)]
359pub enum LocalNameParseError {
360 #[error("Local name has an invalid XML name: {0}")]
362 InvalidXmlName(#[from] name_tokens::InvalidXmlNameError),
363}
364
365impl<'a> LocalName<'a> {
366 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 pub const fn new_dangerous(value: &'a str) -> Result<Self, PrefixParseError> {
380 Ok(Self(Cow::Borrowed(value)))
381 }
382
383 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}