1use crate::model::{NodeId, TestCase};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeSet;
6
7#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum Expr {
11 Word(String),
12 Not(Box<Expr>),
13 And(Box<Expr>, Box<Expr>),
14 Or(Box<Expr>, Box<Expr>),
15}
16
17impl Expr {
18 pub fn parse(text: &str) -> Result<Self, String> {
19 let tokens = tokenize(text);
20 let mut position = 0;
21 let expr = parse_or(&tokens, &mut position)?;
22 if position != tokens.len() {
23 return Err(format!(
24 "unexpected {:?} in expression {text:?}",
25 tokens[position]
26 ));
27 }
28 Ok(expr)
29 }
30
31 pub fn eval(&self, word: &dyn Fn(&str) -> bool) -> bool {
32 match self {
33 Self::Word(value) => word(value),
34 Self::Not(inner) => !inner.eval(word),
35 Self::And(left, right) => left.eval(word) && right.eval(word),
36 Self::Or(left, right) => left.eval(word) || right.eval(word),
37 }
38 }
39}
40
41fn tokenize(text: &str) -> Vec<String> {
42 let mut tokens = Vec::new();
43 let mut current = String::new();
44 for character in text.chars() {
45 if character.is_whitespace() || matches!(character, '(' | ')') {
46 if !current.is_empty() {
47 tokens.push(std::mem::take(&mut current));
48 }
49 if !character.is_whitespace() {
50 tokens.push(character.to_string());
51 }
52 } else {
53 current.push(character);
54 }
55 }
56 if !current.is_empty() {
57 tokens.push(current);
58 }
59 tokens
60}
61
62fn parse_or(tokens: &[String], position: &mut usize) -> Result<Expr, String> {
63 let mut left = parse_and(tokens, position)?;
64 while tokens.get(*position).is_some_and(|token| token == "or") {
65 *position += 1;
66 left = Expr::Or(Box::new(left), Box::new(parse_and(tokens, position)?));
67 }
68 Ok(left)
69}
70
71fn parse_and(tokens: &[String], position: &mut usize) -> Result<Expr, String> {
72 let mut left = parse_not(tokens, position)?;
73 while tokens.get(*position).is_some_and(|token| token == "and") {
74 *position += 1;
75 left = Expr::And(Box::new(left), Box::new(parse_not(tokens, position)?));
76 }
77 Ok(left)
78}
79
80fn parse_not(tokens: &[String], position: &mut usize) -> Result<Expr, String> {
81 match tokens.get(*position).map(String::as_str) {
82 Some("not") => {
83 *position += 1;
84 Ok(Expr::Not(Box::new(parse_not(tokens, position)?)))
85 }
86 Some("(") => {
87 *position += 1;
88 let inner = parse_or(tokens, position)?;
89 if tokens.get(*position).map(String::as_str) != Some(")") {
90 return Err("missing ) in expression".into());
91 }
92 *position += 1;
93 Ok(inner)
94 }
95 Some(")" | "and" | "or") | None => Err("expected a word in expression".into()),
96 Some(word) => {
97 *position += 1;
98 Ok(Expr::Word(word.into()))
99 }
100 }
101}
102
103#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
106#[serde(transparent)]
107pub struct NodeIdPattern(String);
108
109impl NodeIdPattern {
110 pub fn parse(text: &str) -> Result<Self, String> {
111 let text = text.trim().replace('\\', "/");
112 if text.is_empty() {
113 return Err("empty node ID pattern".into());
114 }
115 Ok(Self(text))
116 }
117
118 pub fn matches(&self, node: &NodeId) -> bool {
119 [node.to_string(), node.local()].iter().any(|candidate| {
120 candidate.strip_prefix(self.0.as_str()).is_some_and(|rest| {
121 rest.is_empty() || rest.starts_with("::") || rest.starts_with('[')
122 })
123 })
124 }
125}
126
127#[derive(Clone, Debug, Default, Serialize, Deserialize)]
128pub struct Selector {
129 pub nodes: Vec<NodeIdPattern>,
131 pub keyword: Option<Expr>,
133 pub marks: Option<Expr>,
135 pub only: Option<BTreeSet<String>>,
137}
138
139impl Selector {
140 pub fn matches(&self, case: &TestCase) -> bool {
141 let node = case.node.to_string();
142 (self.nodes.is_empty() || self.nodes.iter().any(|pattern| pattern.matches(&case.node)))
143 && self.keyword.as_ref().is_none_or(|expr| {
144 let haystack = node.to_lowercase();
145 expr.eval(&|word| haystack.contains(&word.to_lowercase()))
146 }) && self
147 .marks
148 .as_ref()
149 .is_none_or(|expr| expr.eval(&|word| case.marks.labels.contains(word)))
150 && self.only.as_ref().is_none_or(|only| only.contains(&node))
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn expressions_follow_not_and_or_precedence() {
160 let expr = Expr::parse("a or b and not c").unwrap();
161 let truth = |set: &[&str]| expr.eval(&|word| set.contains(&word));
162 assert!(truth(&["a"]));
163 assert!(truth(&["b"]));
164 assert!(!truth(&["b", "c"]));
165 assert!(
166 !Expr::parse("(a or b) and c")
167 .unwrap()
168 .eval(&|word| word == "a")
169 );
170 for bad in ["", "a and", "(a", "a b", "not"] {
171 assert!(Expr::parse(bad).is_err(), "{bad:?}");
172 }
173 }
174
175 #[test]
176 fn node_patterns_match_whole_segments() {
177 let node: NodeId = "m::events/a.txt::a.1::flag".parse().unwrap();
178 for pattern in [
179 "events/a.txt",
180 "m::events/a.txt::a.1",
181 "events/a.txt::a.1::flag",
182 ] {
183 assert!(
184 NodeIdPattern::parse(pattern).unwrap().matches(&node),
185 "{pattern}"
186 );
187 }
188 for pattern in ["events/a", "events/a.txt::a.10", "other::events/a.txt"] {
189 assert!(
190 !NodeIdPattern::parse(pattern).unwrap().matches(&node),
191 "{pattern}"
192 );
193 }
194 }
195}