xml_string/names/ncname.rs
1//! [`NCName`].
2//!
3//! [`NCName`]: https://www.w3.org/TR/2009/REC-xml-names-20091208/#NT-NCName
4
5use core::convert::TryFrom;
6
7use crate::names::chars;
8use crate::names::error::{NameError, TargetNameType};
9use crate::names::{Eqname, Name, Nmtoken, Qname};
10
11/// String slice for [`NCName`].
12///
13/// [`NCName`]: https://www.w3.org/TR/2009/REC-xml-names-20091208/#NT-NCName
14#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
15#[repr(transparent)]
16pub struct Ncname(str);
17
18#[allow(clippy::len_without_is_empty)]
19impl Ncname {
20 /// Creates a new `&Ncname`.
21 ///
22 /// # Failures
23 ///
24 /// Fails if the given string is not a valid [`NCName`].
25 ///
26 /// # Examples
27 ///
28 /// ```
29 /// # use xml_string::names::Ncname;
30 /// let name = Ncname::from_str("hello")?;
31 /// assert_eq!(name, "hello");
32 ///
33 /// assert!(Ncname::from_str("").is_err(), "Empty string is not an NCName");
34 /// assert!(Ncname::from_str("foo bar").is_err(), "Whitespace is not allowed");
35 /// assert!(Ncname::from_str("foo:bar").is_err(), "Colon is not allowed");
36 /// assert!(Ncname::from_str("0foo").is_err(), "ASCII digit at the beginning is not allowed");
37 /// # Ok::<_, xml_string::names::NameError>(())
38 /// ```
39 ///
40 /// [`NCName`]: https://www.w3.org/TR/2009/REC-xml-names-20091208/#NT-NCName
41 // `FromStr` can be implemented only for types with static lifetime.
42 #[allow(clippy::should_implement_trait)]
43 #[inline]
44 pub fn from_str(s: &str) -> Result<&Self, NameError> {
45 <&Self>::try_from(s)
46 }
47
48 /// Creates a new `&Ncname` without validation.
49 ///
50 /// # Safety
51 ///
52 /// The given string should be a valid [`NCName`].
53 ///
54 /// # Examples
55 ///
56 /// ```
57 /// # use xml_string::names::Ncname;
58 /// let name = unsafe {
59 /// Ncname::new_unchecked("hello")
60 /// };
61 /// assert_eq!(name, "hello");
62 /// ```
63 ///
64 /// [`NCName`]: https://www.w3.org/TR/2009/REC-xml-names-20091208/#NT-NCName
65 #[inline]
66 #[must_use]
67 pub unsafe fn new_unchecked(s: &str) -> &Self {
68 &*(s as *const str as *const Self)
69 }
70
71 /// Validates the given string.
72 fn validate(s: &str) -> Result<(), NameError> {
73 let mut chars = s.char_indices();
74
75 // Check the first character.
76 if !chars
77 .next()
78 .map_or(false, |(_, c)| chars::is_ncname_start(c))
79 {
80 return Err(NameError::new(TargetNameType::Ncname, 0));
81 }
82
83 // Check the following characters.
84 if let Some((i, _)) = chars.find(|(_, c)| !chars::is_ncname_continue(*c)) {
85 return Err(NameError::new(TargetNameType::Ncname, i));
86 }
87
88 Ok(())
89 }
90
91 /// Returns the string as `&str`.
92 ///
93 /// # Examples
94 ///
95 /// ```
96 /// # use xml_string::names::Ncname;
97 /// let name = Ncname::from_str("hello")?;
98 /// assert_eq!(name, "hello");
99 ///
100 /// let s: &str = name.as_str();
101 /// assert_eq!(s, "hello");
102 /// # Ok::<_, xml_string::names::NameError>(())
103 /// ```
104 #[inline]
105 #[must_use]
106 pub fn as_str(&self) -> &str {
107 &self.0
108 }
109
110 /// Returns the length of the string in bytes.
111 ///
112 /// # Examples
113 ///
114 /// ```
115 /// # use xml_string::names::Ncname;
116 /// let name = Ncname::from_str("foo")?;
117 /// assert_eq!(name.len(), 3);
118 /// # Ok::<_, xml_string::names::NameError>(())
119 /// ```
120 #[inline]
121 #[must_use]
122 pub fn len(&self) -> usize {
123 self.0.len()
124 }
125
126 /// Parses the leading `Ncname` and returns the value and the rest input.
127 ///
128 /// # Exmaples
129 ///
130 /// ```
131 /// # use xml_string::names::Ncname;
132 /// let input = "hello:world";
133 /// let expected = Ncname::from_str("hello").expect("valid NCName");
134 /// assert_eq!(
135 /// Ncname::parse_next(input),
136 /// Ok((expected, ":world"))
137 /// );
138 /// # Ok::<_, xml_string::names::NameError>(())
139 /// ```
140 ///
141 /// ```
142 /// # use xml_string::names::Ncname;
143 /// let input = "012";
144 /// assert!(Ncname::parse_next(input).is_err());
145 /// # Ok::<_, xml_string::names::NameError>(())
146 /// ```
147 pub fn parse_next(s: &str) -> Result<(&Self, &str), NameError> {
148 match Self::from_str(s) {
149 Ok(v) => Ok((v, &s[s.len()..])),
150 Err(e) if e.valid_up_to() == 0 => Err(e),
151 Err(e) => {
152 let valid_up_to = e.valid_up_to();
153 let v = unsafe {
154 let valid = &s[..valid_up_to];
155 debug_assert!(Self::validate(valid).is_ok());
156 // This is safe because the substring is valid.
157 Self::new_unchecked(valid)
158 };
159 Ok((v, &s[valid_up_to..]))
160 }
161 }
162 }
163
164 /// Converts a `Box<Ncname>` into a `Box<str>` without copying or allocating.
165 ///
166 /// # Examples
167 ///
168 /// ```
169 /// # use xml_string::names::Ncname;
170 /// let name = Ncname::from_str("ncname")?;
171 /// let boxed_name: Box<Ncname> = name.into();
172 /// assert_eq!(&*boxed_name, name);
173 /// let boxed_str: Box<str> = boxed_name.into_boxed_str();
174 /// assert_eq!(&*boxed_str, name.as_str());
175 /// # Ok::<_, xml_string::names::NameError>(())
176 /// ```
177 #[cfg(feature = "alloc")]
178 pub fn into_boxed_str(self: alloc::boxed::Box<Self>) -> Box<str> {
179 unsafe {
180 // This is safe because `Ncname` has the same memory layout as `str`
181 // (thanks to `#[repr(transparent)]`).
182 alloc::boxed::Box::<str>::from_raw(alloc::boxed::Box::<Self>::into_raw(self) as *mut str)
183 }
184 }
185}
186
187impl_traits_for_custom_string_slice!(Ncname);
188
189impl AsRef<Nmtoken> for Ncname {
190 #[inline]
191 fn as_ref(&self) -> &Nmtoken {
192 unsafe {
193 debug_assert!(
194 Nmtoken::from_str(self.as_str()).is_ok(),
195 "NCName {:?} must be a valid Nmtoken",
196 self.as_str()
197 );
198 // This is safe because an NCName is also a valid Nmtoken.
199 Nmtoken::new_unchecked(self.as_str())
200 }
201 }
202}
203
204impl AsRef<Name> for Ncname {
205 #[inline]
206 fn as_ref(&self) -> &Name {
207 unsafe {
208 debug_assert!(
209 Name::from_str(self.as_str()).is_ok(),
210 "An NCName is also a Name"
211 );
212 Name::new_unchecked(self.as_str())
213 }
214 }
215}
216
217impl AsRef<Qname> for Ncname {
218 #[inline]
219 fn as_ref(&self) -> &Qname {
220 unsafe {
221 debug_assert!(
222 Qname::from_str(self.as_str()).is_ok(),
223 "An NCName is also a Qname"
224 );
225 Qname::new_unchecked(self.as_str())
226 }
227 }
228}
229
230impl AsRef<Eqname> for Ncname {
231 #[inline]
232 fn as_ref(&self) -> &Eqname {
233 unsafe {
234 debug_assert!(
235 Eqname::from_str(self.as_str()).is_ok(),
236 "An NCName is also a Eqname"
237 );
238 Eqname::new_unchecked(self.as_str())
239 }
240 }
241}
242
243impl<'a> TryFrom<&'a str> for &'a Ncname {
244 type Error = NameError;
245
246 fn try_from(s: &'a str) -> Result<Self, Self::Error> {
247 Ncname::validate(s)?;
248 Ok(unsafe {
249 // This is safe because the string is validated.
250 Ncname::new_unchecked(s)
251 })
252 }
253}
254
255impl<'a> TryFrom<&'a Name> for &'a Ncname {
256 type Error = NameError;
257
258 fn try_from(s: &'a Name) -> Result<Self, Self::Error> {
259 if let Some(colon_pos) = s.as_str().find(':') {
260 return Err(NameError::new(TargetNameType::Ncname, colon_pos));
261 }
262
263 unsafe {
264 debug_assert!(
265 Ncname::validate(s.as_str()).is_ok(),
266 "Name {:?} without colons is also a valid NCName",
267 s.as_str()
268 );
269 Ok(Ncname::new_unchecked(s.as_str()))
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 fn ensure_eq(s: &str) {
279 assert_eq!(
280 Ncname::from_str(s).expect("Should not fail"),
281 s,
282 "String: {:?}",
283 s
284 );
285 }
286
287 fn ensure_error_at(s: &str, valid_up_to: usize) {
288 let err = Ncname::from_str(s).expect_err("Should fail");
289 assert_eq!(err.valid_up_to(), valid_up_to, "String: {:?}", s);
290 }
291
292 #[test]
293 fn ncname_str_valid() {
294 ensure_eq("hello");
295 ensure_eq("abc123");
296 }
297
298 #[test]
299 fn ncname_str_invalid() {
300 ensure_error_at("", 0);
301 ensure_error_at("-foo", 0);
302 ensure_error_at("0foo", 0);
303 ensure_error_at("foo bar", 3);
304 ensure_error_at("foo/bar", 3);
305
306 ensure_error_at("foo:bar", 3);
307 ensure_error_at(":foo", 0);
308 ensure_error_at("foo:", 3);
309 }
310}