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}