Skip to main content

whitaker_common/
path.rs

1//! Shared helpers for working with `::`-delimited paths.
2
3use std::fmt;
4
5/// Represents a syntactic path composed of `::`-separated segments.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct SimplePath {
8    segments: Vec<String>,
9}
10
11impl SimplePath {
12    /// Builds a path from iterator segments.
13    ///
14    /// # Examples
15    ///
16    /// ```
17    /// use whitaker_common::path::SimplePath;
18    ///
19    /// let path = SimplePath::new(["tokio", "test"]);
20    /// assert_eq!(path.segments(), &["tokio", "test"]);
21    /// ```
22    #[must_use]
23    pub fn new<I, S>(segments: I) -> Self
24    where
25        I: IntoIterator<Item = S>,
26        S: Into<String>,
27    {
28        Self {
29            segments: segments.into_iter().map(Into::into).collect(),
30        }
31    }
32
33    /// Parses a Rust path from its textual representation.
34    ///
35    /// Empty segments produced by leading, trailing, or repeated separators are
36    /// discarded.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use whitaker_common::path::SimplePath;
42    ///
43    /// let parsed = SimplePath::from("tokio::test");
44    /// assert_eq!(parsed.segments(), &["tokio", "test"]);
45    /// let compact = SimplePath::parse("::test::");
46    /// assert_eq!(compact.segments(), &["test"]);
47    /// ```
48    #[must_use]
49    pub fn parse(path: &str) -> Self {
50        Self::new(path.split("::").filter(|segment| !segment.is_empty()))
51    }
52
53    /// Returns the path segments as a slice.
54    #[must_use]
55    #[rustfmt::skip]
56    pub fn segments(&self) -> &[String] { &self.segments }
57
58    /// Returns the final path segment when present.
59    #[must_use]
60    #[rustfmt::skip]
61    pub fn last(&self) -> Option<&str> { self.segments.last().map(String::as_str) }
62
63    /// Returns `true` when this path matches the provided sequence exactly.
64    #[must_use]
65    pub fn matches<I, S>(&self, candidate: I) -> bool
66    where
67        I: IntoIterator<Item = S>,
68        S: AsRef<str>,
69    {
70        let mut candidate_iter = candidate.into_iter();
71
72        for expected in &self.segments {
73            match candidate_iter.next() {
74                Some(candidate_segment) if expected == candidate_segment.as_ref() => continue,
75                _ => return false,
76            }
77        }
78
79        candidate_iter.next().is_none()
80    }
81
82    /// Returns `true` when the path denotes a doc comment (`doc`).
83    #[must_use]
84    #[rustfmt::skip]
85    pub fn is_doc(&self) -> bool { self.matches(["doc"]) }
86}
87
88impl From<&str> for SimplePath {
89    fn from(path: &str) -> Self {
90        Self::parse(path)
91    }
92}
93
94impl From<String> for SimplePath {
95    fn from(path: String) -> Self {
96        Self::parse(&path)
97    }
98}
99
100impl fmt::Display for SimplePath {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.write_str(&self.segments.join("::"))
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use rstest::rstest;
110    use std::collections::VecDeque;
111
112    #[rstest]
113    fn filters_empty_segments() {
114        let path = SimplePath::from("::crate::::Item::");
115        assert!(path.matches(["crate", "Item"]));
116    }
117
118    #[rstest]
119    fn matches_segments() {
120        let path = SimplePath::from("crate::module::Item");
121        assert!(path.matches(["crate", "module", "Item"]));
122        assert!(!path.matches(["crate", "module", "Other"]));
123    }
124
125    #[rstest]
126    fn last_returns_final_segment() {
127        let populated = SimplePath::from("crate::module::Item");
128        assert_eq!(populated.last(), Some("Item"));
129
130        let empty = SimplePath::new(Vec::<String>::new());
131        assert_eq!(empty.last(), None);
132    }
133
134    #[rstest]
135    fn is_doc_identifies_doc_segments() {
136        assert!(SimplePath::from("doc").is_doc());
137        assert!(!SimplePath::from("allow").is_doc());
138    }
139
140    #[rstest]
141    fn display_formats_with_separators() {
142        let path = SimplePath::from("crate::module::Item");
143        assert_eq!(path.to_string(), "crate::module::Item");
144    }
145
146    #[rstest]
147    fn from_string_parses_owned_values() {
148        let owned = String::from("test::path");
149        let path = SimplePath::from(owned);
150        assert!(path.matches(["test", "path"]));
151    }
152
153    #[rstest]
154    fn new_accepts_varied_iterators() {
155        let from_vec = SimplePath::new(vec!["a", "b"]);
156        let from_array = SimplePath::new(["a", "b"]);
157        let from_owned = SimplePath::new(vec![String::from("a"), String::from("b")]);
158
159        assert!(from_vec.matches(["a", "b"]));
160        assert!(from_array.matches(["a", "b"]));
161        assert!(from_owned.matches(["a", "b"]));
162    }
163
164    #[rstest]
165    fn new_accepts_iterator_inputs_beyond_vectors() {
166        let deque_path = SimplePath::new(VecDeque::from(["module", "Item"]));
167        assert!(deque_path.matches(["module", "Item"]));
168
169        let once_path = SimplePath::new(std::iter::once("solo"));
170        assert!(once_path.matches(["solo"]));
171    }
172}