action.rs

  1use crate::SharedString;
  2use anyhow::{anyhow, Result};
  3use collections::{HashMap, HashSet};
  4use std::any::Any;
  5
  6pub trait Action: Any + Send + Sync {
  7    fn eq(&self, action: &dyn Action) -> bool;
  8    fn boxed_clone(&self) -> Box<dyn Action>;
  9    fn as_any(&self) -> &dyn Any;
 10}
 11
 12#[derive(Clone, Debug, Default, Eq, PartialEq)]
 13pub struct DispatchContext {
 14    set: HashSet<SharedString>,
 15    map: HashMap<SharedString, SharedString>,
 16}
 17
 18impl<'a> TryFrom<&'a str> for DispatchContext {
 19    type Error = anyhow::Error;
 20
 21    fn try_from(value: &'a str) -> Result<Self> {
 22        Self::parse(value)
 23    }
 24}
 25
 26impl DispatchContext {
 27    pub fn parse(source: &str) -> Result<Self> {
 28        let mut context = Self::default();
 29        let source = skip_whitespace(source);
 30        Self::parse_expr(&source, &mut context)?;
 31        Ok(context)
 32    }
 33
 34    fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
 35        if source.is_empty() {
 36            return Ok(());
 37        }
 38
 39        let key = source
 40            .chars()
 41            .take_while(|c| is_identifier_char(*c))
 42            .collect::<String>();
 43        source = skip_whitespace(&source[key.len()..]);
 44        if let Some(suffix) = source.strip_prefix('=') {
 45            source = skip_whitespace(suffix);
 46            let value = source
 47                .chars()
 48                .take_while(|c| is_identifier_char(*c))
 49                .collect::<String>();
 50            source = skip_whitespace(&source[value.len()..]);
 51            context.set(key, value);
 52        } else {
 53            context.insert(key);
 54        }
 55
 56        Self::parse_expr(source, context)
 57    }
 58
 59    pub fn is_empty(&self) -> bool {
 60        self.set.is_empty() && self.map.is_empty()
 61    }
 62
 63    pub fn clear(&mut self) {
 64        self.set.clear();
 65        self.map.clear();
 66    }
 67
 68    pub fn extend(&mut self, other: &Self) {
 69        for v in &other.set {
 70            self.set.insert(v.clone());
 71        }
 72        for (k, v) in &other.map {
 73            self.map.insert(k.clone(), v.clone());
 74        }
 75    }
 76
 77    pub fn insert<I: Into<SharedString>>(&mut self, identifier: I) {
 78        self.set.insert(identifier.into());
 79    }
 80
 81    pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
 82        self.map.insert(key.into(), value.into());
 83    }
 84}
 85
 86#[derive(Clone, Debug, Eq, PartialEq, Hash)]
 87pub enum DispatchContextPredicate {
 88    Identifier(SharedString),
 89    Equal(SharedString, SharedString),
 90    NotEqual(SharedString, SharedString),
 91    Child(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
 92    Not(Box<DispatchContextPredicate>),
 93    And(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
 94    Or(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
 95}
 96
 97impl DispatchContextPredicate {
 98    pub fn parse(source: &str) -> Result<Self> {
 99        let source = skip_whitespace(source);
100        let (predicate, rest) = Self::parse_expr(source, 0)?;
101        if let Some(next) = rest.chars().next() {
102            Err(anyhow!("unexpected character {next:?}"))
103        } else {
104            Ok(predicate)
105        }
106    }
107
108    pub fn eval(&self, contexts: &[&DispatchContext]) -> bool {
109        let Some(context) = contexts.last() else {
110            return false;
111        };
112        match self {
113            Self::Identifier(name) => context.set.contains(&name),
114            Self::Equal(left, right) => context
115                .map
116                .get(&left)
117                .map(|value| value == right)
118                .unwrap_or(false),
119            Self::NotEqual(left, right) => context
120                .map
121                .get(&left)
122                .map(|value| value != right)
123                .unwrap_or(true),
124            Self::Not(pred) => !pred.eval(contexts),
125            Self::Child(parent, child) => {
126                parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts)
127            }
128            Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
129            Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
130        }
131    }
132
133    fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
134        type Op = fn(
135            DispatchContextPredicate,
136            DispatchContextPredicate,
137        ) -> Result<DispatchContextPredicate>;
138
139        let (mut predicate, rest) = Self::parse_primary(source)?;
140        source = rest;
141
142        'parse: loop {
143            for (operator, precedence, constructor) in [
144                (">", PRECEDENCE_CHILD, Self::new_child as Op),
145                ("&&", PRECEDENCE_AND, Self::new_and as Op),
146                ("||", PRECEDENCE_OR, Self::new_or as Op),
147                ("==", PRECEDENCE_EQ, Self::new_eq as Op),
148                ("!=", PRECEDENCE_EQ, Self::new_neq as Op),
149            ] {
150                if source.starts_with(operator) && precedence >= min_precedence {
151                    source = skip_whitespace(&source[operator.len()..]);
152                    let (right, rest) = Self::parse_expr(source, precedence + 1)?;
153                    predicate = constructor(predicate, right)?;
154                    source = rest;
155                    continue 'parse;
156                }
157            }
158            break;
159        }
160
161        Ok((predicate, source))
162    }
163
164    fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
165        let next = source
166            .chars()
167            .next()
168            .ok_or_else(|| anyhow!("unexpected eof"))?;
169        match next {
170            '(' => {
171                source = skip_whitespace(&source[1..]);
172                let (predicate, rest) = Self::parse_expr(source, 0)?;
173                if rest.starts_with(')') {
174                    source = skip_whitespace(&rest[1..]);
175                    Ok((predicate, source))
176                } else {
177                    Err(anyhow!("expected a ')'"))
178                }
179            }
180            '!' => {
181                let source = skip_whitespace(&source[1..]);
182                let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
183                Ok((DispatchContextPredicate::Not(Box::new(predicate)), source))
184            }
185            _ if is_identifier_char(next) => {
186                let len = source
187                    .find(|c: char| !is_identifier_char(c))
188                    .unwrap_or(source.len());
189                let (identifier, rest) = source.split_at(len);
190                source = skip_whitespace(rest);
191                Ok((
192                    DispatchContextPredicate::Identifier(identifier.to_string().into()),
193                    source,
194                ))
195            }
196            _ => Err(anyhow!("unexpected character {next:?}")),
197        }
198    }
199
200    fn new_or(self, other: Self) -> Result<Self> {
201        Ok(Self::Or(Box::new(self), Box::new(other)))
202    }
203
204    fn new_and(self, other: Self) -> Result<Self> {
205        Ok(Self::And(Box::new(self), Box::new(other)))
206    }
207
208    fn new_child(self, other: Self) -> Result<Self> {
209        Ok(Self::Child(Box::new(self), Box::new(other)))
210    }
211
212    fn new_eq(self, other: Self) -> Result<Self> {
213        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
214            Ok(Self::Equal(left, right))
215        } else {
216            Err(anyhow!("operands must be identifiers"))
217        }
218    }
219
220    fn new_neq(self, other: Self) -> Result<Self> {
221        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
222            Ok(Self::NotEqual(left, right))
223        } else {
224            Err(anyhow!("operands must be identifiers"))
225        }
226    }
227}
228
229const PRECEDENCE_CHILD: u32 = 1;
230const PRECEDENCE_OR: u32 = 2;
231const PRECEDENCE_AND: u32 = 3;
232const PRECEDENCE_EQ: u32 = 4;
233const PRECEDENCE_NOT: u32 = 5;
234
235fn is_identifier_char(c: char) -> bool {
236    c.is_alphanumeric() || c == '_' || c == '-'
237}
238
239fn skip_whitespace(source: &str) -> &str {
240    let len = source
241        .find(|c: char| !c.is_whitespace())
242        .unwrap_or(source.len());
243    &source[len..]
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use DispatchContextPredicate::*;
250
251    #[test]
252    fn test_parse_context() {
253        let mut expected = DispatchContext::default();
254        expected.set("foo", "bar");
255        expected.insert("baz");
256        assert_eq!(DispatchContext::parse("baz foo=bar").unwrap(), expected);
257        assert_eq!(DispatchContext::parse("foo = bar baz").unwrap(), expected);
258        assert_eq!(
259            DispatchContext::parse("  baz foo   =   bar baz").unwrap(),
260            expected
261        );
262        assert_eq!(DispatchContext::parse(" foo = bar baz").unwrap(), expected);
263    }
264
265    #[test]
266    fn test_parse_identifiers() {
267        // Identifiers
268        assert_eq!(
269            DispatchContextPredicate::parse("abc12").unwrap(),
270            Identifier("abc12".into())
271        );
272        assert_eq!(
273            DispatchContextPredicate::parse("_1a").unwrap(),
274            Identifier("_1a".into())
275        );
276    }
277
278    #[test]
279    fn test_parse_negations() {
280        assert_eq!(
281            DispatchContextPredicate::parse("!abc").unwrap(),
282            Not(Box::new(Identifier("abc".into())))
283        );
284        assert_eq!(
285            DispatchContextPredicate::parse(" ! ! abc").unwrap(),
286            Not(Box::new(Not(Box::new(Identifier("abc".into())))))
287        );
288    }
289
290    #[test]
291    fn test_parse_equality_operators() {
292        assert_eq!(
293            DispatchContextPredicate::parse("a == b").unwrap(),
294            Equal("a".into(), "b".into())
295        );
296        assert_eq!(
297            DispatchContextPredicate::parse("c!=d").unwrap(),
298            NotEqual("c".into(), "d".into())
299        );
300        assert_eq!(
301            DispatchContextPredicate::parse("c == !d")
302                .unwrap_err()
303                .to_string(),
304            "operands must be identifiers"
305        );
306    }
307
308    #[test]
309    fn test_parse_boolean_operators() {
310        assert_eq!(
311            DispatchContextPredicate::parse("a || b").unwrap(),
312            Or(
313                Box::new(Identifier("a".into())),
314                Box::new(Identifier("b".into()))
315            )
316        );
317        assert_eq!(
318            DispatchContextPredicate::parse("a || !b && c").unwrap(),
319            Or(
320                Box::new(Identifier("a".into())),
321                Box::new(And(
322                    Box::new(Not(Box::new(Identifier("b".into())))),
323                    Box::new(Identifier("c".into()))
324                ))
325            )
326        );
327        assert_eq!(
328            DispatchContextPredicate::parse("a && b || c&&d").unwrap(),
329            Or(
330                Box::new(And(
331                    Box::new(Identifier("a".into())),
332                    Box::new(Identifier("b".into()))
333                )),
334                Box::new(And(
335                    Box::new(Identifier("c".into())),
336                    Box::new(Identifier("d".into()))
337                ))
338            )
339        );
340        assert_eq!(
341            DispatchContextPredicate::parse("a == b && c || d == e && f").unwrap(),
342            Or(
343                Box::new(And(
344                    Box::new(Equal("a".into(), "b".into())),
345                    Box::new(Identifier("c".into()))
346                )),
347                Box::new(And(
348                    Box::new(Equal("d".into(), "e".into())),
349                    Box::new(Identifier("f".into()))
350                ))
351            )
352        );
353        assert_eq!(
354            DispatchContextPredicate::parse("a && b && c && d").unwrap(),
355            And(
356                Box::new(And(
357                    Box::new(And(
358                        Box::new(Identifier("a".into())),
359                        Box::new(Identifier("b".into()))
360                    )),
361                    Box::new(Identifier("c".into())),
362                )),
363                Box::new(Identifier("d".into()))
364            ),
365        );
366    }
367
368    #[test]
369    fn test_parse_parenthesized_expressions() {
370        assert_eq!(
371            DispatchContextPredicate::parse("a && (b == c || d != e)").unwrap(),
372            And(
373                Box::new(Identifier("a".into())),
374                Box::new(Or(
375                    Box::new(Equal("b".into(), "c".into())),
376                    Box::new(NotEqual("d".into(), "e".into())),
377                )),
378            ),
379        );
380        assert_eq!(
381            DispatchContextPredicate::parse(" ( a || b ) ").unwrap(),
382            Or(
383                Box::new(Identifier("a".into())),
384                Box::new(Identifier("b".into())),
385            )
386        );
387    }
388}