Skip to main content

structfs_core_store/
path_pattern.rs

1//! Component-level path patterns for matching, masking, and subscriptions.
2
3use crate::Path;
4
5/// A pattern over paths, matched **component-wise** — never byte-wise.
6///
7/// A `Prefix` pattern for `config/gate/accounts` matches
8/// `config/gate/accounts/personal` but *not* `config/gate/accounts_other`,
9/// which a naive string `starts_with` would incorrectly match.
10///
11/// # Examples
12///
13/// ```rust
14/// use structfs_core_store::{path, PathPattern};
15///
16/// let exact = PathPattern::exact(path!("gate/defaults/model"));
17/// assert!(exact.matches(&path!("gate/defaults/model")));
18/// assert!(!exact.matches(&path!("gate/defaults/model/extra")));
19///
20/// let prefix = PathPattern::prefix(path!("gate/accounts"));
21/// assert!(prefix.matches(&path!("gate/accounts")));
22/// assert!(prefix.matches(&path!("gate/accounts/personal/key")));
23/// assert!(!prefix.matches(&path!("gate/accounts_other")));
24///
25/// // Match `gate/accounts/{anything...}/provider`
26/// let ps = PathPattern::prefix_suffix(path!("gate/accounts"), path!("provider"));
27/// assert!(ps.matches(&path!("gate/accounts/personal/provider")));
28/// assert!(!ps.matches(&path!("gate/accounts/personal/model")));
29/// ```
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub enum PathPattern {
32    /// Matches exactly one path.
33    Exact(Path),
34    /// Matches the path itself and everything under it.
35    Prefix(Path),
36    /// Matches paths that start with the prefix and end with the suffix,
37    /// with at least the suffix's components after the prefix. The middle
38    /// may be empty: `prefix_suffix(a, c)` matches `a/c` and `a/b/c`.
39    PrefixSuffix(Path, Path),
40}
41
42impl PathPattern {
43    /// Pattern matching exactly `path`.
44    pub fn exact(path: Path) -> Self {
45        PathPattern::Exact(path)
46    }
47
48    /// Pattern matching `path` and all its descendants.
49    pub fn prefix(path: Path) -> Self {
50        PathPattern::Prefix(path)
51    }
52
53    /// Pattern matching paths under `prefix` that end with `suffix`.
54    pub fn prefix_suffix(prefix: Path, suffix: Path) -> Self {
55        PathPattern::PrefixSuffix(prefix, suffix)
56    }
57
58    /// Check whether a path matches this pattern (component-wise).
59    pub fn matches(&self, path: &Path) -> bool {
60        match self {
61            PathPattern::Exact(p) => p == path,
62            PathPattern::Prefix(prefix) => path.has_prefix(prefix),
63            PathPattern::PrefixSuffix(prefix, suffix) => match path.strip_prefix(prefix) {
64                Some(rest) => {
65                    rest.len() >= suffix.len()
66                        && rest.slice(rest.len() - suffix.len(), rest.len()) == *suffix
67                }
68                None => false,
69            },
70        }
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::path;
78
79    #[test]
80    fn exact_matches_only_itself() {
81        let p = PathPattern::exact(path!("a/b"));
82        assert!(p.matches(&path!("a/b")));
83        assert!(!p.matches(&path!("a")));
84        assert!(!p.matches(&path!("a/b/c")));
85    }
86
87    #[test]
88    fn prefix_is_component_wise() {
89        let p = PathPattern::prefix(path!("gate/api_key"));
90        assert!(p.matches(&path!("gate/api_key")));
91        assert!(p.matches(&path!("gate/api_key/inner")));
92        // The byte-prefix bug: "gate/api_key_other" starts with "gate/api_key"
93        // as a string, but must not match component-wise.
94        assert!(!p.matches(&path!("gate/api_key_other")));
95    }
96
97    #[test]
98    fn empty_prefix_matches_everything() {
99        let p = PathPattern::prefix(path!(""));
100        assert!(p.matches(&path!("")));
101        assert!(p.matches(&path!("anything/at/all")));
102    }
103
104    #[test]
105    fn prefix_suffix_middle_may_be_empty() {
106        let p = PathPattern::prefix_suffix(path!("accounts"), path!("provider"));
107        assert!(p.matches(&path!("accounts/provider")));
108        assert!(p.matches(&path!("accounts/personal/provider")));
109        assert!(p.matches(&path!("accounts/a/b/provider")));
110        assert!(!p.matches(&path!("accounts")));
111        assert!(!p.matches(&path!("accounts/personal/model")));
112        assert!(!p.matches(&path!("other/personal/provider")));
113    }
114
115    #[test]
116    fn prefix_suffix_component_wise() {
117        let p = PathPattern::prefix_suffix(path!("a"), path!("key"));
118        assert!(!p.matches(&path!("a/x/key_other")));
119        assert!(p.matches(&path!("a/x/key")));
120    }
121}