1use 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 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 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 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}