Skip to main content

rustdv_sim/
path.rs

1//! `RustdvPath` — a component's position in the tree, as segments.
2//!
3//! The path is **derived by the walk and never stored on a component** (D7).
4//! Making it a type rather than a `String` buys three things:
5//!
6//! 1. **It cannot be fabricated.** A `RustdvPath` is produced only by the
7//!    framework's walk ([`RustdvPath::root`] then [`RustdvPath::child`]), so a
8//!    user cannot hand-type `"env.loga"` where a path is expected. That is the
9//!    guarantee D7 and D62 exist to protect: the old
10//!    `Logger::new("env.loga")` kept compiling — and kept addressing the wrong
11//!    subtree — after a rename.
12//! 2. **Prefix matching is correct by construction.** Comparing segment lists
13//!    cannot confuse `ab` with a child of `a`, which the string form had to
14//!    special-case (D64).
15//! 3. **Depth is a count, not a scan** — and D13's config precedence is scaled
16//!    by the setter's depth.
17//!
18//! The rendered dotted form is cached alongside the segments, so `Display` and
19//! `as_str()` are free and every log line prints exactly as it always has.
20
21use std::fmt;
22
23#[derive(Clone, Debug, Default, PartialEq, Eq)]
24pub struct RustdvPath {
25    segments: Vec<String>,
26    /// The dotted rendering, kept in step with `segments`. Built once per node
27    /// during the walk rather than formatted at every log call.
28    rendered: String,
29}
30
31impl RustdvPath {
32    /// The root of a tree — the test's registered name (D49).
33    pub fn root(name: &str) -> RustdvPath {
34        RustdvPath {
35            segments: vec![name.to_string()],
36            rendered: name.to_string(),
37        }
38    }
39
40    /// An empty path, for contexts with no position (a free `#[rustdv::test]`
41    /// function, which has no component tree).
42    pub fn empty() -> RustdvPath {
43        RustdvPath::default()
44    }
45
46    /// This path extended by one segment — how the walk descends (D9).
47    pub fn child(&self, name: &str) -> RustdvPath {
48        let mut segments = self.segments.clone();
49        segments.push(name.to_string());
50        let rendered = if self.rendered.is_empty() {
51            name.to_string()
52        } else {
53            format!("{}.{}", self.rendered, name)
54        };
55        RustdvPath { segments, rendered }
56    }
57
58    /// The dotted form, e.g. `RandomTest.env.scoreboard`.
59    pub fn as_str(&self) -> &str {
60        &self.rendered
61    }
62
63    pub fn segments(&self) -> &[String] {
64        &self.segments
65    }
66
67    /// How deep this node sits. The root is depth 1; an empty path is 0.
68    pub fn depth(&self) -> usize {
69        self.segments.len()
70    }
71
72    pub fn is_empty(&self) -> bool {
73        self.segments.is_empty()
74    }
75
76    /// Is this path at or below `prefix`? Segment-wise, so `a.b` is under `a`
77    /// and `ab` is not — without the string special-case D64 needed.
78    pub fn is_under(&self, prefix: &RustdvPath) -> bool {
79        prefix.segments.len() <= self.segments.len()
80            && self.segments[..prefix.segments.len()] == prefix.segments[..]
81    }
82}
83
84impl fmt::Display for RustdvPath {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(&self.rendered)
87    }
88}
89
90/// Segment-wise "is `path` at or below `prefix`?" for the string form, used by
91/// the log module's per-subtree handlers where prefixes arrive as text.
92///
93/// This replaces `path == prefix || path.starts_with(&format!("{prefix}."))` —
94/// same answer, no allocation, and the dot rule falls out of the comparison
95/// instead of being bolted on.
96pub fn str_is_under(path: &str, prefix: &str) -> bool {
97    if prefix.is_empty() {
98        return true;
99    }
100    let mut have = path.split('.');
101    for want in prefix.split('.') {
102        match have.next() {
103            Some(seg) if seg == want => continue,
104            _ => return false,
105        }
106    }
107    true
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn child_extends_and_renders() {
116        let root = RustdvPath::root("RandomTest");
117        let env = root.child("env");
118        let sb = env.child("scoreboard");
119        assert_eq!(sb.as_str(), "RandomTest.env.scoreboard");
120        assert_eq!(sb.depth(), 3);
121        assert_eq!(sb.segments(), ["RandomTest", "env", "scoreboard"]);
122    }
123
124    #[test]
125    fn under_is_segment_wise() {
126        let a = RustdvPath::root("a");
127        let ab = a.child("b");
128        assert!(ab.is_under(&a));
129        assert!(a.is_under(&a));
130        assert!(!a.is_under(&ab));
131        // the bug the string form had to special-case
132        assert!(!RustdvPath::root("ab").is_under(&a));
133    }
134
135    #[test]
136    fn str_under_matches_on_segments() {
137        assert!(str_is_under("a.b", "a"));
138        assert!(str_is_under("a", "a"));
139        assert!(!str_is_under("ab", "a")); // not a child of `a`
140        assert!(!str_is_under("a", "a.b"));
141        assert!(str_is_under("anything", ""));
142    }
143
144    #[test]
145    fn empty_path_has_no_segments() {
146        let e = RustdvPath::empty();
147        assert!(e.is_empty());
148        assert_eq!(e.depth(), 0);
149        assert_eq!(e.as_str(), "");
150        assert_eq!(e.child("top").as_str(), "top");
151    }
152}