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}