yosh_plugin_api/
pattern.rs1#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct CommandPattern {
9 pub tokens: Vec<String>,
10 pub has_glob_suffix: bool,
11}
12
13impl CommandPattern {
14 pub fn parse(s: &str) -> Result<Self, String> {
22 let trimmed = s.trim();
23 if trimmed.is_empty() {
24 return Err("empty pattern".to_string());
25 }
26
27 let (body, has_glob_suffix) = if let Some(stripped) = trimmed.strip_suffix(":*") {
28 (stripped.trim_end(), true)
29 } else {
30 (trimmed, false)
31 };
32
33 if body.is_empty() {
34 return Err("pattern has `:*` but no tokens".to_string());
35 }
36
37 if body.contains(":*") {
38 return Err(
39 "`:*` may only appear as a trailing suffix on the whole pattern".to_string(),
40 );
41 }
42
43 let tokens: Vec<String> = body.split_whitespace().map(|t| t.to_string()).collect();
44
45 Ok(CommandPattern {
46 tokens,
47 has_glob_suffix,
48 })
49 }
50
51 pub fn matches<S: AsRef<str>>(&self, argv: &[S]) -> bool {
57 if self.has_glob_suffix {
58 argv.len() >= self.tokens.len()
59 && self
60 .tokens
61 .iter()
62 .zip(argv)
63 .all(|(p, a)| p.as_str() == a.as_ref())
64 } else {
65 argv.len() == self.tokens.len()
66 && self
67 .tokens
68 .iter()
69 .zip(argv)
70 .all(|(p, a)| p.as_str() == a.as_ref())
71 }
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn parse_glob_suffix_separates_tokens() {
81 let p = CommandPattern::parse("git log:*").unwrap();
82 assert_eq!(p.tokens, vec!["git".to_string(), "log".to_string()]);
83 assert!(p.has_glob_suffix);
84 }
85
86 #[test]
87 fn parse_no_suffix_is_exact() {
88 let p = CommandPattern::parse("git log").unwrap();
89 assert_eq!(p.tokens, vec!["git".to_string(), "log".to_string()]);
90 assert!(!p.has_glob_suffix);
91 }
92
93 #[test]
94 fn parse_empty_string_errors() {
95 assert!(CommandPattern::parse("").is_err());
96 assert!(CommandPattern::parse(" ").is_err());
97 }
98
99 #[test]
100 fn parse_lone_glob_suffix_errors() {
101 assert!(CommandPattern::parse(":*").is_err());
102 assert!(CommandPattern::parse(" :*").is_err());
103 }
104
105 #[test]
106 fn match_glob_suffix_zero_extra() {
107 let p = CommandPattern::parse("git:*").unwrap();
108 assert!(p.matches(&["git".to_string()]));
109 }
110
111 #[test]
112 fn match_glob_suffix_many_extra() {
113 let p = CommandPattern::parse("git:*").unwrap();
114 assert!(p.matches(&["git".to_string(), "log".to_string(), "-p".to_string(),]));
115 }
116
117 #[test]
118 fn match_exact_requires_equal_length() {
119 let p = CommandPattern::parse("git status").unwrap();
120 assert!(p.matches(&["git".to_string(), "status".to_string()]));
121 assert!(!p.matches(&[
122 "git".to_string(),
123 "status".to_string(),
124 "--porcelain".to_string()
125 ]));
126 assert!(!p.matches(&["git".to_string()]));
127 }
128
129 #[test]
130 fn match_literal_compare() {
131 let p = CommandPattern::parse("git:*").unwrap();
132 assert!(!p.matches(&["/usr/bin/git".to_string(), "status".to_string()]));
133 }
134
135 #[test]
136 fn parse_mid_string_glob_suffix_errors() {
137 assert!(CommandPattern::parse("git:*:*").is_err());
138 assert!(CommandPattern::parse("git:* status:*").is_err());
139 assert!(CommandPattern::parse("foo:* bar").is_err());
140 }
141
142 #[test]
143 fn match_glob_suffix_subcommand_lock() {
144 let p = CommandPattern::parse("git status:*").unwrap();
145 assert!(p.matches(&["git".to_string(), "status".to_string()]));
146 assert!(p.matches(&[
147 "git".to_string(),
148 "status".to_string(),
149 "--porcelain".to_string()
150 ]));
151 assert!(!p.matches(&["git".to_string(), "log".to_string()]));
152 }
153
154 #[test]
155 fn matches_accepts_str_slice_argv() {
156 let p = CommandPattern::parse("/bin/echo:*").unwrap();
160 let argv: &[&str] = &["/bin/echo", "a", "b"];
161 assert!(p.matches(argv));
162 }
163}