tytanic_core/test/
id.rs

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