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