action.rs

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