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