tytanic_core/test/
id.rs

1//! Test identifiers.
2
3use std::borrow::{Borrow, Cow};
4use std::fmt::{self, Debug, Display};
5use std::ops::Deref;
6use std::path::{Component, Path, PathBuf};
7use std::str::FromStr;
8
9use ecow::EcoString;
10use thiserror::Error;
11
12/// A test id, this is the relative path from the test root directory, down to
13/// the folder containing the test script.
14///
15/// Each part of the path must be a simple id containing only ASCII
16/// alpha-numeric characters, dashes `-` or underscores `_` and start with an
17/// alphabetic character. This restriction may be lifted in the future.
18#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
19pub struct Id(EcoString);
20
21impl Id {
22    /// The test component separator.
23    pub const SEPARATOR: &'static str = "/";
24}
25
26impl Id {
27    /// Turns this string into an id.
28    ///
29    /// All components must start at least one ascii alphabetic letter and
30    /// contain only ascii alphanumeric characters, underscores and minuses.
31    ///
32    /// # Examples
33    /// ```
34    /// # use tytanic_core::test::Id;
35    /// let id = Id::new("a/b/c")?;
36    /// # Ok::<_, Box<dyn std::error::Error>>(())
37    /// ```
38    ///
39    /// # Errors
40    /// Returns an error if a component wasn't valid.
41    pub fn new<S: Into<EcoString>>(string: S) -> Result<Self, ParseIdError> {
42        let id = string.into();
43        Self::validate(&id)?;
44
45        Ok(Self(id))
46    }
47
48    /// Turns this path into an id, this follows the same rules as
49    /// [`Id::new`] with the additional constraint that paths must valid
50    /// UTF-8.
51    ///
52    /// # Examples
53    /// ```
54    /// # use tytanic_core::test::Id;
55    /// let id = Id::new_from_path("a/b/c")?;
56    /// # Ok::<_, Box<dyn std::error::Error>>(())
57    /// ```
58    ///
59    /// # Errors
60    /// Returns an error if a component wasn't valid.
61    pub fn new_from_path<P: AsRef<Path>>(path: P) -> Result<Self, ParseIdError> {
62        fn inner(path: &Path) -> Result<Id, ParseIdError> {
63            let mut id = String::new();
64
65            for component in path.components() {
66                match component {
67                    Component::Normal(comp) => {
68                        if let Some(comp) = comp.to_str() {
69                            Id::validate_component(comp)?;
70
71                            if !id.is_empty() {
72                                id.push_str(Id::SEPARATOR);
73                            }
74
75                            id.push_str(comp);
76                        } else {
77                            return Err(ParseIdError::InvalidFragment);
78                        }
79                    }
80                    _ => return Err(ParseIdError::InvalidFragment),
81                }
82            }
83
84            Ok(Id(id.into()))
85        }
86
87        inner(path.as_ref())
88    }
89
90    /// Turns this string into an id without validating it.
91    ///
92    /// # Safety
93    /// The caller must ensure that the given string is a valid id.
94    pub unsafe fn new_unchecked(string: EcoString) -> Self {
95        debug_assert!(Self::is_valid(&string));
96        Self(string)
97    }
98}
99
100impl Id {
101    /// Whether the given string is a valid id.
102    ///
103    /// # Examples
104    /// ```
105    /// # use tytanic_core::test::Id;
106    /// assert!( Id::is_valid("a/b/c"));
107    /// assert!( Id::is_valid("a/b"));
108    /// assert!( Id::is_valid("a"));
109    /// assert!(!Id::is_valid("a//b"));  // empty component
110    /// assert!(!Id::is_valid("a/"));    // empty component
111    /// ```
112    pub fn is_valid<S: AsRef<str>>(string: S) -> bool {
113        Self::validate(string).is_ok()
114    }
115
116    fn validate<S: AsRef<str>>(string: S) -> Result<(), ParseIdError> {
117        for fragment in string.as_ref().split(Self::SEPARATOR) {
118            Self::validate_component(fragment)?;
119        }
120
121        Ok(())
122    }
123
124    /// Whether the given string is a valid id component.
125    ///
126    /// # Examples
127    /// ```
128    /// # use tytanic_core::test::Id;
129    /// assert!( Id::is_component_valid("a"));
130    /// assert!( Id::is_component_valid("a1"));
131    /// assert!(!Id::is_component_valid("1a"));  // invalid char
132    /// assert!(!Id::is_component_valid("a "));  // invalid char
133    /// ```
134    pub fn is_component_valid<S: AsRef<str>>(component: S) -> bool {
135        Self::validate_component(component).is_ok()
136    }
137
138    // TODO(tinger): this seems to be the culprit of the 100% doc tests
139    fn validate_component<S: AsRef<str>>(component: S) -> Result<(), ParseIdError> {
140        let component = component.as_ref();
141
142        if component.is_empty() {
143            return Err(ParseIdError::Empty);
144        }
145
146        let mut chars = component.chars().peekable();
147        if !chars.next().unwrap().is_ascii_alphabetic() {
148            return Err(ParseIdError::InvalidFragment);
149        }
150
151        if chars.peek().is_some()
152            && !chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
153        {
154            return Err(ParseIdError::InvalidFragment);
155        }
156
157        Ok(())
158    }
159}
160
161impl Id {
162    /// The full id as a `str`, this string is never empty.
163    pub fn as_str(&self) -> &str {
164        self.0.as_str()
165    }
166
167    /// Clones the inner [`EcoString`].
168    pub fn to_inner(&self) -> EcoString {
169        self.0.clone()
170    }
171
172    /// The name of this test, the last component of this id. This string is
173    /// never empty.
174    ///
175    /// # Examples
176    /// ```
177    /// # use tytanic_core::test::Id;
178    /// let id = Id::new("a/b/c")?;
179    /// assert_eq!(id.name(), "c");
180    /// # Ok::<_, Box<dyn std::error::Error>>(())
181    /// ```
182    pub fn name(&self) -> &str {
183        self.components()
184            .next_back()
185            .expect("id is always non-empty")
186    }
187
188    /// The module containing the, all but the last component of this id. This
189    /// string may be empty.
190    ///
191    /// # Examples
192    /// ```
193    /// # use tytanic_core::test::Id;
194    /// let id = Id::new("a/b/c")?;
195    /// assert_eq!(id.module(), "a/b");
196    /// # Ok::<_, Box<dyn std::error::Error>>(())
197    /// ```
198    pub fn module(&self) -> &str {
199        let mut c = self.components();
200        _ = c.next_back().expect("id is always non-empty");
201        c.rest
202    }
203
204    /// The ancestors of this id, this corresponds to the ancestors of the
205    /// test's path.
206    ///
207    /// # Examples
208    /// ```
209    /// # use tytanic_core::test::Id;
210    /// let id = Id::new("a/b/c")?;
211    /// let mut ancestors = id.ancestors();
212    /// assert_eq!(ancestors.next(), Some("a/b/c"));
213    /// assert_eq!(ancestors.next(), Some("a/b"));
214    /// assert_eq!(ancestors.next(), Some("a"));
215    /// assert_eq!(ancestors.next(), None);
216    /// # Ok::<_, Box<dyn std::error::Error>>(())
217    /// ```
218    pub fn ancestors(&self) -> Ancestors<'_> {
219        Ancestors { rest: &self.0 }
220    }
221
222    /// The components of this id, this corresponds to the components of the
223    /// test's path.
224    ///
225    /// # Examples
226    /// ```
227    /// # use tytanic_core::test::Id;
228    /// let id = Id::new("a/b/c")?;
229    /// let mut components = id.components();
230    /// assert_eq!(components.next(), Some("a"));
231    /// assert_eq!(components.next(), Some("b"));
232    /// assert_eq!(components.next(), Some("c"));
233    /// assert_eq!(components.next(), None);
234    /// # Ok::<_, Box<dyn std::error::Error>>(())
235    /// ```
236    pub fn components(&self) -> Components<'_> {
237        Components { rest: &self.0 }
238    }
239
240    /// Turns this id into a path relative to the test directory root.
241    pub fn to_path(&self) -> Cow<'_, Path> {
242        let s = self.as_str();
243
244        if Self::SEPARATOR == std::path::MAIN_SEPARATOR_STR {
245            Cow::Borrowed(Path::new(s))
246        } else {
247            Cow::Owned(PathBuf::from(
248                s.replace(Self::SEPARATOR, std::path::MAIN_SEPARATOR_STR),
249            ))
250        }
251    }
252}
253
254impl Id {
255    /// Adds the given component to this Id without checking if it is valid.
256    ///
257    /// # Safety
258    /// The caller must ensure that the given component is valid.
259    pub unsafe fn push_component_unchecked<S: AsRef<str>>(&mut self, component: S) {
260        let comp = component.as_ref();
261        self.0.push_str(Self::SEPARATOR);
262        self.0.push_str(comp);
263    }
264
265    /// Tries to add the given component to this id.
266    pub fn push_component<S: AsRef<str>>(&mut self, component: S) -> Result<(), ParseIdError> {
267        let comp = component.as_ref();
268        Self::validate_component(comp)?;
269
270        // SAFETY: we validated above
271        unsafe {
272            self.push_component_unchecked(component);
273        }
274
275        Ok(())
276    }
277
278    /// Tries to add the given component to this id.
279    pub fn push_path_component<P: AsRef<Path>>(
280        &mut self,
281        component: P,
282    ) -> Result<(), ParseIdError> {
283        self.push_component(
284            component
285                .as_ref()
286                .to_str()
287                .ok_or(ParseIdError::InvalidFragment)?,
288        )
289    }
290}
291
292impl Deref for Id {
293    type Target = str;
294
295    fn deref(&self) -> &Self::Target {
296        self.0.as_str()
297    }
298}
299
300impl AsRef<str> for Id {
301    fn as_ref(&self) -> &str {
302        self.0.as_str()
303    }
304}
305
306impl Borrow<str> for Id {
307    fn borrow(&self) -> &str {
308        self.0.as_str()
309    }
310}
311
312impl FromStr for Id {
313    type Err = ParseIdError;
314
315    fn from_str(s: &str) -> Result<Self, Self::Err> {
316        Self::new(s)
317    }
318}
319
320impl Debug for Id {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        Debug::fmt(self.as_str(), f)
323    }
324}
325
326impl Display for Id {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        Display::fmt(self.as_str(), f)
329    }
330}
331
332impl PartialEq<str> for Id {
333    fn eq(&self, other: &str) -> bool {
334        self.as_str() == other
335    }
336}
337
338impl PartialEq<Id> for str {
339    fn eq(&self, other: &Id) -> bool {
340        self == other.as_str()
341    }
342}
343
344impl PartialEq<String> for Id {
345    fn eq(&self, other: &String) -> bool {
346        self.as_str() == other
347    }
348}
349
350impl PartialEq<Id> for String {
351    fn eq(&self, other: &Id) -> bool {
352        self == other.as_str()
353    }
354}
355
356impl PartialEq<EcoString> for Id {
357    fn eq(&self, other: &EcoString) -> bool {
358        self.as_str() == other
359    }
360}
361
362impl PartialEq<Id> for EcoString {
363    fn eq(&self, other: &Id) -> bool {
364        self == other.as_str()
365    }
366}
367
368/// Returned by [`Id::ancestors`].
369#[derive(Debug)]
370pub struct Ancestors<'id> {
371    rest: &'id str,
372}
373
374impl<'id> Iterator for Ancestors<'id> {
375    type Item = &'id str;
376
377    fn next(&mut self) -> Option<Self::Item> {
378        let ret = self.rest;
379        self.rest = self
380            .rest
381            .rsplit_once(Id::SEPARATOR)
382            .map(|(rest, _)| rest)
383            .unwrap_or("");
384
385        if ret.is_empty() {
386            return None;
387        }
388
389        Some(ret)
390    }
391}
392
393/// Returned by [`Id::components`].
394#[derive(Debug)]
395pub struct Components<'id> {
396    rest: &'id str,
397}
398
399impl<'id> Iterator for Components<'id> {
400    type Item = &'id str;
401
402    fn next(&mut self) -> Option<Self::Item> {
403        if self.rest.is_empty() {
404            return None;
405        }
406
407        let (c, rest) = self
408            .rest
409            .split_once(Id::SEPARATOR)
410            .unwrap_or((self.rest, ""));
411        self.rest = rest;
412
413        Some(c)
414    }
415}
416
417impl<'id> DoubleEndedIterator for Components<'id> {
418    fn next_back(&mut self) -> Option<Self::Item> {
419        if self.rest.is_empty() {
420            return None;
421        }
422
423        let (rest, c) = self
424            .rest
425            .rsplit_once(Id::SEPARATOR)
426            .unwrap_or(("", self.rest));
427        self.rest = rest;
428
429        Some(c)
430    }
431}
432
433/// Returned by [`Id::new`][new] and [`Id::new_from_path`][new_from_path].
434///
435/// [new]: super::Id::new
436/// [new_from_path]: super::Id::new_from_path
437#[derive(Debug, Error)]
438pub enum ParseIdError {
439    /// An id contained an invalid fragment.
440    #[error("id contained an invalid fragment")]
441    InvalidFragment,
442
443    /// An id contained empty or no fragments.
444    #[error("id contained empty or no fragments")]
445    Empty,
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[test]
453    fn test_ancestors() {
454        assert_eq!(
455            Id::new("a/b/c").unwrap().ancestors().collect::<Vec<_>>(),
456            ["a/b/c", "a/b", "a"]
457        );
458    }
459
460    #[test]
461    fn test_components() {
462        assert_eq!(
463            Id::new("a/b/c").unwrap().components().collect::<Vec<_>>(),
464            ["a", "b", "c"]
465        );
466        assert_eq!(
467            Id::new("a/b/c")
468                .unwrap()
469                .components()
470                .rev()
471                .collect::<Vec<_>>(),
472            ["c", "b", "a"]
473        );
474    }
475
476    #[test]
477    fn test_name() {
478        let tests = [("a/b/c", "c"), ("a/b", "b"), ("a", "a")];
479
480        for (id, name) in tests {
481            assert_eq!(Id(id.into()).name(), name);
482        }
483    }
484
485    #[test]
486    fn test_module() {
487        let tests = [("a/b/c", "a/b"), ("a/b", "a"), ("a", "")];
488
489        for (id, name) in tests {
490            assert_eq!(Id(id.into()).module(), name);
491        }
492    }
493
494    #[test]
495    fn test_str_invalid() {
496        assert!(Id::new("/a").is_err());
497        assert!(Id::new("a/").is_err());
498        assert!(Id::new("a//b").is_err());
499
500        assert!(Id::new("a ").is_err());
501        assert!(Id::new("1a").is_err());
502        assert!(Id::new("").is_err());
503    }
504}