qname_impl/
lib.rs

1//! This crate implements the macro for `qname` and should not be used directly.
2
3use std::{fmt::Display, str::FromStr};
4
5use proc_macro2::TokenStream;
6use quote::quote;
7use syn::LitStr;
8
9#[doc(hidden)]
10pub fn qname(item: TokenStream) -> Result<TokenStream, syn::Error> {
11    // Implement your proc-macro logic here. :)
12    let s: LitStr = syn::parse2(item)?;
13    let _qname: QName = s
14        .value()
15        .parse()
16        .map_err(|_| syn::Error::new(s.span(), "Invalid QName"))?;
17
18    Ok(quote! {
19        ::qname::QName::new_unchecked(#s)
20    })
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub struct QName {
25    pub(crate) namespace: Option<String>,
26    pub(crate) local_part: String,
27    pub(crate) prefixed_name: String,
28}
29
30impl Display for QName {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.write_str(&self.prefixed_name)
33    }
34}
35
36#[derive(Debug, Clone, Copy)]
37pub struct Error(ErrorInner);
38
39#[derive(Debug, Clone, Copy)]
40enum ErrorInner {
41    Empty,
42    Start(char),
43    Continue(char),
44}
45
46impl std::error::Error for Error {}
47
48impl Display for Error {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self.0 {
51            ErrorInner::Empty => write!(f, "Invalid QName: Cannot be empty"),
52            ErrorInner::Start(c) => write!(f, "Invalid QName: First char cannot be {c:?}"),
53            ErrorInner::Continue(c) => write!(f, "Invalid QName: Cannot contain {c:?}"),
54        }
55    }
56}
57
58impl FromStr for QName {
59    type Err = Error;
60
61    fn from_str(s: &str) -> Result<Self, Self::Err> {
62        QName::new(s)
63    }
64}
65
66impl TryFrom<&'_ str> for QName {
67    type Error = Error;
68
69    fn try_from(s: &str) -> Result<Self, Error> {
70        QName::new(s)
71    }
72}
73
74impl QName {
75    /// Attempt to parse a string as a qualified name.
76    pub fn new(name: &str) -> Result<QName, Error> {
77        if let Some(first_err) = first_qname_error(name) {
78            return Err(first_err);
79        }
80
81        Ok(match name.split_once(":") {
82            Some((ns, local)) => Self {
83                namespace: Some(ns.to_string()),
84                local_part: local.to_string(),
85                prefixed_name: format!("{ns}:{local}"),
86            },
87            None => Self {
88                namespace: None,
89                local_part: name.to_string(),
90                prefixed_name: name.to_string(),
91            },
92        })
93    }
94
95    /// Create a qname from a known-valid qualified name.
96    ///
97    /// ## Panics
98    ///
99    /// This function panics if the given name is not valid.
100    pub fn new_unchecked(name: &str) -> QName {
101        if let Some(err) = first_qname_error(name) {
102            panic!("Input '{name}' is not a valid QName: {err}.");
103        }
104
105        match name.split_once(":") {
106            Some((ns, local)) => Self {
107                namespace: Some(ns.to_string()),
108                local_part: local.to_string(),
109                prefixed_name: format!("{ns}:{local}"),
110            },
111            None => Self {
112                namespace: None,
113                local_part: name.to_string(),
114                prefixed_name: name.to_string(),
115            },
116        }
117    }
118
119    pub fn namespace(&self) -> Option<&str> {
120        self.namespace.as_deref()
121    }
122
123    pub fn local_part(&self) -> &str {
124        &self.local_part
125    }
126
127    pub fn prefixed_name(&self) -> &str {
128        &self.prefixed_name
129    }
130}
131
132pub fn is_valid_qname(input: &str) -> bool {
133    first_qname_error(input).is_some()
134}
135
136fn first_qname_error(input: &str) -> Option<Error> {
137    fn is_name_start_char(ch: char) -> bool {
138        matches!(ch, ':'
139            | 'A'..='Z'
140            | '_'
141            | 'a'..='z'
142            | '\u{C0}'..='\u{D6}'
143            | '\u{D8}'..='\u{F6}'
144            | '\u{F8}'..='\u{2FF}'
145            | '\u{370}'..='\u{37D}'
146            | '\u{37F}'..='\u{1FFF}'
147            | '\u{200C}'..='\u{200D}'
148            | '\u{2070}'..='\u{218F}'
149            | '\u{2C00}'..='\u{2FEF}'
150            | '\u{3001}'..='\u{D7FF}'
151            | '\u{F900}'..='\u{FDCF}'
152            | '\u{FDF0}'..='\u{FFFD}'
153            | '\u{10000}'..='\u{EFFFF}')
154    }
155
156    fn is_name_char(ch: char) -> bool {
157        if is_name_start_char(ch) {
158            return true;
159        }
160
161        match ch {
162            '-' | '.' | '0'..='9' => return true,
163            _ => {}
164        }
165
166        match ch as u32 {
167            0xb7 | 0x0300..=0x036F | 0x203F..=0x2040 => true,
168            _ => false,
169        }
170    }
171
172    let mut chars = input.chars();
173    match chars.next() {
174        None => return Some(Error(ErrorInner::Empty)),
175        Some(ch) => {
176            if !is_name_start_char(ch) {
177                return Some(Error(ErrorInner::Start(ch)));
178            }
179        }
180    };
181
182    chars
183        .find(|&c| !is_name_char(c))
184        .map(|ch| Error(ErrorInner::Continue(ch)))
185}