json_rules_engine/
condition.rs

1use crate::{status::Status, Constraint};
2#[cfg(feature = "eval")]
3use rhai::{serde::to_dynamic, Engine, Scope};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
8#[serde(untagged)]
9pub enum Condition {
10    And {
11        and: Vec<Condition>,
12    },
13    Or {
14        or: Vec<Condition>,
15    },
16    Not {
17        not: Box<Condition>,
18    },
19    AtLeast {
20        should_minimum_meet: usize,
21        conditions: Vec<Condition>,
22    },
23    Condition {
24        field: String,
25        #[serde(flatten)]
26        constraint: Constraint,
27        path: Option<String>,
28    },
29    #[cfg(feature = "eval")]
30    Eval {
31        expr: String,
32    },
33}
34
35impl Condition {
36    /// Starting at this node, recursively check (depth-first) any child nodes and
37    /// aggregate the results
38    pub fn check_value(
39        &self,
40        info: &Value,
41        #[cfg(feature = "eval")] rhai_engine: &Engine,
42    ) -> ConditionResult {
43        match *self {
44            Condition::And { ref and } => {
45                let mut status = Status::Met;
46                let children = and
47                    .iter()
48                    .map(|c| {
49                        c.check_value(
50                            info,
51                            #[cfg(feature = "eval")]
52                            rhai_engine,
53                        )
54                    })
55                    .inspect(|r| status = status & r.status)
56                    .collect::<Vec<_>>();
57
58                ConditionResult {
59                    name: "And".into(),
60                    status,
61                    children,
62                }
63            }
64            Condition::Not { not: ref c } => {
65                let res = c.check_value(
66                    info,
67                    #[cfg(feature = "eval")]
68                    rhai_engine,
69                );
70
71                ConditionResult {
72                    name: "Not".into(),
73                    status: !res.status,
74                    children: res.children,
75                }
76            }
77            Condition::Or { ref or } => {
78                let mut status = Status::NotMet;
79                let children = or
80                    .iter()
81                    .map(|c| {
82                        c.check_value(
83                            info,
84                            #[cfg(feature = "eval")]
85                            rhai_engine,
86                        )
87                    })
88                    .inspect(|r| status = status | r.status)
89                    .collect::<Vec<_>>();
90
91                ConditionResult {
92                    name: "Or".into(),
93                    status,
94                    children,
95                }
96            }
97            Condition::AtLeast {
98                should_minimum_meet,
99                ref conditions,
100            } => {
101                let mut met_count = 0;
102                let children = conditions
103                    .iter()
104                    .map(|c| {
105                        c.check_value(
106                            info,
107                            #[cfg(feature = "eval")]
108                            rhai_engine,
109                        )
110                    })
111                    .inspect(|r| {
112                        if r.status == Status::Met {
113                            met_count += 1;
114                        }
115                    })
116                    .collect::<Vec<_>>();
117
118                let status = if met_count >= should_minimum_meet {
119                    Status::Met
120                } else {
121                    Status::NotMet
122                };
123
124                ConditionResult {
125                    name: format!(
126                        "At least meet {} of {}",
127                        should_minimum_meet,
128                        conditions.len()
129                    ),
130                    status,
131                    children,
132                }
133            }
134            #[allow(unused_variables)]
135            Condition::Condition {
136                ref field,
137                ref constraint,
138                ref path,
139            } => {
140                let node_path = if field.starts_with('/') {
141                    field.to_owned()
142                } else {
143                    format!("/{}", field)
144                };
145
146                let mut status = Status::Unknown;
147
148                #[allow(unused_mut)]
149                if let Some(mut node) = info.pointer(&node_path).cloned() {
150                    #[cfg(feature = "path")]
151                    {
152                        if let Some(p) = path {
153                            let x = jsonpath_lib::select(&node, p)
154                                .unwrap()
155                                .into_iter()
156                                .cloned()
157                                .collect();
158                            node = Value::Array(x);
159                        }
160                    }
161
162                    status = constraint.check_value(&node);
163                }
164
165                ConditionResult {
166                    name: field.to_owned(),
167                    status,
168                    children: Vec::new(),
169                }
170            }
171            #[cfg(feature = "eval")]
172            Condition::Eval { ref expr } => {
173                let mut scope = Scope::new();
174                if let Ok(val) = to_dynamic(info) {
175                    scope.push_dynamic("facts", val);
176                }
177                let status = if rhai_engine
178                    .eval_with_scope::<bool>(&mut scope, expr)
179                    .unwrap_or(false)
180                {
181                    Status::Met
182                } else {
183                    Status::NotMet
184                };
185
186                ConditionResult {
187                    name: "Eval".to_owned(),
188                    status,
189                    children: Vec::new(),
190                }
191            }
192        }
193    }
194}
195
196/// Result of checking a rules tree.
197#[derive(Debug, Serialize, Deserialize)]
198pub struct ConditionResult {
199    /// Human-friendly description of the rule
200    pub name: String,
201    /// top-level status of this result
202    pub status: Status,
203    /// Results of any sub-rules
204    pub children: Vec<ConditionResult>,
205}
206
207/// Creates a `Rule` where all child `Rule`s must be `Met`
208///
209/// * If any are `NotMet`, the result will be `NotMet`
210/// * If the results contain only `Met` and `Unknown`, the result will be `Unknown`
211/// * Only results in `Met` if all children are `Met`
212pub fn and(and: Vec<Condition>) -> Condition {
213    Condition::And { and }
214}
215
216/// Creates a `Rule` where any child `Rule` must be `Met`
217///
218/// * If any are `Met`, the result will be `Met`
219/// * If the results contain only `NotMet` and `Unknown`, the result will be `Unknown`
220/// * Only results in `NotMet` if all children are `NotMet`
221pub fn or(or: Vec<Condition>) -> Condition {
222    Condition::Or { or }
223}
224
225/// Creates a `Rule` where `n` child `Rule`s must be `Met`
226///
227/// * If `>= n` are `Met`, the result will be `Met`, otherwise it'll be `NotMet`
228pub fn at_least(
229    should_minimum_meet: usize,
230    conditions: Vec<Condition>,
231) -> Condition {
232    Condition::AtLeast {
233        should_minimum_meet,
234        conditions,
235    }
236}
237
238/// Creates a rule for string comparison
239pub fn string_equals(field: &str, val: &str) -> Condition {
240    Condition::Condition {
241        field: field.into(),
242        constraint: Constraint::StringEquals(val.into()),
243        path: None,
244    }
245}
246
247pub fn string_not_equals(field: &str, val: &str) -> Condition {
248    Condition::Condition {
249        field: field.into(),
250        constraint: Constraint::StringNotEquals(val.into()),
251        path: None,
252    }
253}
254
255pub fn string_contains(field: &str, val: &str) -> Condition {
256    Condition::Condition {
257        field: field.into(),
258        constraint: Constraint::StringContains(val.into()),
259        path: None,
260    }
261}
262
263pub fn string_contains_all(field: &str, val: Vec<&str>) -> Condition {
264    Condition::Condition {
265        field: field.into(),
266        constraint: Constraint::StringContainsAll(
267            val.into_iter().map(ToOwned::to_owned).collect(),
268        ),
269        path: None,
270    }
271}
272
273pub fn string_contains_any(field: &str, val: Vec<&str>) -> Condition {
274    Condition::Condition {
275        field: field.into(),
276        constraint: Constraint::StringContainsAny(
277            val.into_iter().map(ToOwned::to_owned).collect(),
278        ),
279        path: None,
280    }
281}
282
283pub fn string_does_not_contain(field: &str, val: &str) -> Condition {
284    Condition::Condition {
285        field: field.into(),
286        constraint: Constraint::StringDoesNotContain(val.into()),
287        path: None,
288    }
289}
290
291pub fn string_does_not_contain_any(field: &str, val: Vec<&str>) -> Condition {
292    Condition::Condition {
293        field: field.into(),
294        constraint: Constraint::StringDoesNotContainAny(
295            val.into_iter().map(ToOwned::to_owned).collect(),
296        ),
297        path: None,
298    }
299}
300
301pub fn string_in(field: &str, val: Vec<&str>) -> Condition {
302    Condition::Condition {
303        field: field.into(),
304        constraint: Constraint::StringIn(
305            val.into_iter().map(ToOwned::to_owned).collect(),
306        ),
307        path: None,
308    }
309}
310
311pub fn string_not_in(field: &str, val: Vec<&str>) -> Condition {
312    Condition::Condition {
313        field: field.into(),
314        constraint: Constraint::StringNotIn(
315            val.into_iter().map(ToOwned::to_owned).collect(),
316        ),
317        path: None,
318    }
319}
320
321/// Creates a rule for int comparison.
322pub fn int_equals(field: &str, val: i64) -> Condition {
323    Condition::Condition {
324        field: field.into(),
325        constraint: Constraint::IntEquals(val),
326        path: None,
327    }
328}
329
330pub fn int_not_equals(field: &str, val: i64) -> Condition {
331    Condition::Condition {
332        field: field.into(),
333        constraint: Constraint::IntNotEquals(val),
334        path: None,
335    }
336}
337
338pub fn int_contains(field: &str, val: i64) -> Condition {
339    Condition::Condition {
340        field: field.into(),
341        constraint: Constraint::IntContains(val),
342        path: None,
343    }
344}
345
346pub fn int_contains_all(field: &str, val: Vec<i64>) -> Condition {
347    Condition::Condition {
348        field: field.into(),
349        constraint: Constraint::IntContainsAll(val),
350        path: None,
351    }
352}
353
354pub fn int_contains_any(field: &str, val: Vec<i64>) -> Condition {
355    Condition::Condition {
356        field: field.into(),
357        constraint: Constraint::IntContainsAny(val),
358        path: None,
359    }
360}
361
362pub fn int_does_not_contain(field: &str, val: i64) -> Condition {
363    Condition::Condition {
364        field: field.into(),
365        constraint: Constraint::IntDoesNotContain(val),
366        path: None,
367    }
368}
369
370pub fn int_does_not_contain_any(field: &str, val: Vec<i64>) -> Condition {
371    Condition::Condition {
372        field: field.into(),
373        constraint: Constraint::IntDoesNotContainAny(val),
374        path: None,
375    }
376}
377
378pub fn int_in(field: &str, val: Vec<i64>) -> Condition {
379    Condition::Condition {
380        field: field.into(),
381        constraint: Constraint::IntIn(val),
382        path: None,
383    }
384}
385
386pub fn int_not_in(field: &str, val: Vec<i64>) -> Condition {
387    Condition::Condition {
388        field: field.into(),
389        constraint: Constraint::IntNotIn(val),
390        path: None,
391    }
392}
393
394pub fn int_in_range(field: &str, start: i64, end: i64) -> Condition {
395    Condition::Condition {
396        field: field.into(),
397        constraint: Constraint::IntInRange(start, end),
398        path: None,
399    }
400}
401
402pub fn int_not_in_range(field: &str, start: i64, end: i64) -> Condition {
403    Condition::Condition {
404        field: field.into(),
405        constraint: Constraint::IntNotInRange(start, end),
406        path: None,
407    }
408}
409
410pub fn int_less_than(field: &str, val: i64) -> Condition {
411    Condition::Condition {
412        field: field.into(),
413        constraint: Constraint::IntLessThan(val),
414        path: None,
415    }
416}
417
418pub fn int_less_than_inclusive(field: &str, val: i64) -> Condition {
419    Condition::Condition {
420        field: field.into(),
421        constraint: Constraint::IntLessThanInclusive(val),
422        path: None,
423    }
424}
425
426pub fn int_greater_than(field: &str, val: i64) -> Condition {
427    Condition::Condition {
428        field: field.into(),
429        constraint: Constraint::IntGreaterThan(val),
430        path: None,
431    }
432}
433
434pub fn int_greater_than_inclusive(field: &str, val: i64) -> Condition {
435    Condition::Condition {
436        field: field.into(),
437        constraint: Constraint::IntGreaterThanInclusive(val),
438        path: None,
439    }
440}
441
442/// Creates a rule for float comparison.
443pub fn float_equals(field: &str, val: f64) -> Condition {
444    Condition::Condition {
445        field: field.into(),
446        constraint: Constraint::FloatEquals(val),
447        path: None,
448    }
449}
450
451pub fn float_not_equals(field: &str, val: f64) -> Condition {
452    Condition::Condition {
453        field: field.into(),
454        constraint: Constraint::FloatNotEquals(val),
455        path: None,
456    }
457}
458
459pub fn float_contains(field: &str, val: f64) -> Condition {
460    Condition::Condition {
461        field: field.into(),
462        constraint: Constraint::FloatContains(val),
463        path: None,
464    }
465}
466
467pub fn float_does_not_contain(field: &str, val: f64) -> Condition {
468    Condition::Condition {
469        field: field.into(),
470        constraint: Constraint::FloatDoesNotContain(val),
471        path: None,
472    }
473}
474
475pub fn float_in(field: &str, val: Vec<f64>) -> Condition {
476    Condition::Condition {
477        field: field.into(),
478        constraint: Constraint::FloatIn(val),
479        path: None,
480    }
481}
482
483pub fn float_not_in(field: &str, val: Vec<f64>) -> Condition {
484    Condition::Condition {
485        field: field.into(),
486        constraint: Constraint::FloatNotIn(val),
487        path: None,
488    }
489}
490
491pub fn float_in_range(field: &str, start: f64, end: f64) -> Condition {
492    Condition::Condition {
493        field: field.into(),
494        constraint: Constraint::FloatInRange(start, end),
495        path: None,
496    }
497}
498
499pub fn float_not_in_range(field: &str, start: f64, end: f64) -> Condition {
500    Condition::Condition {
501        field: field.into(),
502        constraint: Constraint::FloatNotInRange(start, end),
503        path: None,
504    }
505}
506
507pub fn float_less_than(field: &str, val: f64) -> Condition {
508    Condition::Condition {
509        field: field.into(),
510        constraint: Constraint::FloatLessThan(val),
511        path: None,
512    }
513}
514
515pub fn float_less_than_inclusive(field: &str, val: f64) -> Condition {
516    Condition::Condition {
517        field: field.into(),
518        constraint: Constraint::FloatLessThanInclusive(val),
519        path: None,
520    }
521}
522
523pub fn float_greater_than(field: &str, val: f64) -> Condition {
524    Condition::Condition {
525        field: field.into(),
526        constraint: Constraint::FloatGreaterThan(val),
527        path: None,
528    }
529}
530
531pub fn float_greater_than_inclusive(field: &str, val: f64) -> Condition {
532    Condition::Condition {
533        field: field.into(),
534        constraint: Constraint::FloatGreaterThanInclusive(val),
535        path: None,
536    }
537}
538
539/// Creates a rule for boolean comparison.
540pub fn bool_equals(field: &str, val: bool) -> Condition {
541    Condition::Condition {
542        field: field.into(),
543        constraint: Constraint::BoolEquals(val),
544        path: None,
545    }
546}
547
548#[cfg(not(feature = "eval"))]
549#[cfg(test)]
550mod tests {
551    use super::{
552        and, at_least, bool_equals, int_equals, int_in_range, or, string_equals,
553    };
554    use crate::status::Status;
555    use serde_json::{json, Value};
556
557    fn get_test_data() -> Value {
558        json!({
559            "foo": 1,
560            "bar": "bar",
561            "baz": true
562        })
563    }
564
565    #[test]
566    fn and_rules() {
567        let map = get_test_data();
568        // Met & Met == Met
569        let mut root =
570            and(vec![int_equals("foo", 1), string_equals("bar", "bar")]);
571        let mut res = root.check_value(&map);
572
573        assert_eq!(res.status, Status::Met);
574
575        // Met & NotMet == NotMet
576        root = and(vec![int_equals("foo", 2), string_equals("bar", "bar")]);
577        res = root.check_value(&map);
578
579        assert_eq!(res.status, Status::NotMet);
580
581        // Met & Unknown == Unknown
582        root = and(vec![int_equals("quux", 2), string_equals("bar", "bar")]);
583        res = root.check_value(&map);
584
585        assert_eq!(res.status, Status::Unknown);
586
587        // NotMet & Unknown == NotMet
588        root = and(vec![int_equals("quux", 2), string_equals("bar", "baz")]);
589        res = root.check_value(&map);
590
591        assert_eq!(res.status, Status::NotMet);
592
593        // Unknown & Unknown == Unknown
594        root = and(vec![int_equals("quux", 2), string_equals("fizz", "bar")]);
595        res = root.check_value(&map);
596
597        assert_eq!(res.status, Status::Unknown);
598    }
599
600    #[test]
601    fn or_rules() {
602        let map = get_test_data();
603        // Met | Met == Met
604        let mut root =
605            or(vec![int_equals("foo", 1), string_equals("bar", "bar")]);
606        let mut res = root.check_value(&map);
607
608        assert_eq!(res.status, Status::Met);
609
610        // Met | NotMet == Met
611        root = or(vec![int_equals("foo", 2), string_equals("bar", "bar")]);
612        res = root.check_value(&map);
613
614        assert_eq!(res.status, Status::Met);
615
616        // Met | Unknown == Met
617        root = or(vec![int_equals("quux", 2), string_equals("bar", "bar")]);
618        res = root.check_value(&map);
619
620        assert_eq!(res.status, Status::Met);
621
622        // NotMet | Unknown == Unknown
623        root = or(vec![int_equals("quux", 2), string_equals("bar", "baz")]);
624        res = root.check_value(&map);
625
626        assert_eq!(res.status, Status::Unknown);
627
628        // Unknown | Unknown == Unknown
629        root = or(vec![int_equals("quux", 2), string_equals("fizz", "bar")]);
630        res = root.check_value(&map);
631
632        assert_eq!(res.status, Status::Unknown);
633    }
634
635    #[test]
636    fn n_of_rules() {
637        let map = get_test_data();
638        // 2 Met, 1 NotMet == Met
639        let mut root = at_least(
640            2,
641            vec![
642                int_equals("foo", 1),
643                string_equals("bar", "bar"),
644                bool_equals("baz", false),
645            ],
646        );
647        let mut res = root.check_value(&map);
648
649        assert_eq!(res.status, Status::Met);
650
651        // 1 Met, 1 NotMet, 1 Unknown == NotMet
652        root = at_least(
653            2,
654            vec![
655                int_equals("foo", 1),
656                string_equals("quux", "bar"),
657                bool_equals("baz", false),
658            ],
659        );
660        res = root.check_value(&map);
661
662        assert_eq!(res.status, Status::NotMet);
663
664        // 2 NotMet, 1 Unknown == Unknown
665        root = at_least(
666            2,
667            vec![
668                int_equals("foo", 2),
669                string_equals("quux", "baz"),
670                bool_equals("baz", false),
671            ],
672        );
673        res = root.check_value(&map);
674
675        assert_eq!(res.status, Status::NotMet);
676    }
677
678    #[test]
679    fn string_equals_rule() {
680        let map = get_test_data();
681        let mut rule = string_equals("bar", "bar");
682        let mut res = rule.check_value(&map);
683        assert_eq!(res.status, Status::Met);
684
685        rule = string_equals("bar", "baz");
686        res = rule.check_value(&map);
687        assert_eq!(res.status, Status::NotMet);
688    }
689
690    #[test]
691    fn int_equals_rule() {
692        let map = get_test_data();
693        let mut rule = int_equals("foo", 1);
694        let mut res = rule.check_value(&map);
695        assert_eq!(res.status, Status::Met);
696
697        rule = int_equals("foo", 2);
698        res = rule.check_value(&map);
699        assert_eq!(res.status, Status::NotMet);
700
701        // Values not convertible to int should be NotMet
702        rule = int_equals("bar", 2);
703        res = rule.check_value(&map);
704        assert_eq!(res.status, Status::NotMet);
705    }
706
707    #[test]
708    fn int_range_rule() {
709        let map = get_test_data();
710        let mut rule = int_in_range("foo", 1, 3);
711        let mut res = rule.check_value(&map);
712        assert_eq!(res.status, Status::Met);
713
714        rule = int_in_range("foo", 2, 3);
715        res = rule.check_value(&map);
716        assert_eq!(res.status, Status::NotMet);
717
718        // Values not convertible to int should be NotMet
719        rule = int_in_range("bar", 1, 3);
720        res = rule.check_value(&map);
721        assert_eq!(res.status, Status::NotMet);
722    }
723
724    #[test]
725    fn boolean_rule() {
726        let mut map = get_test_data();
727        let mut rule = bool_equals("baz", true);
728        let mut res = rule.check_value(&map);
729        assert_eq!(res.status, Status::Met);
730
731        rule = bool_equals("baz", false);
732        res = rule.check_value(&map);
733        assert_eq!(res.status, Status::NotMet);
734
735        rule = bool_equals("bar", true);
736        res = rule.check_value(&map);
737        assert_eq!(res.status, Status::NotMet);
738
739        rule = bool_equals("bar", false);
740        res = rule.check_value(&map);
741        assert_eq!(res.status, Status::NotMet);
742
743        map["quux".to_owned()] = json!("tRuE");
744        rule = bool_equals("quux", true);
745        res = rule.check_value(&map);
746        assert_eq!(res.status, Status::NotMet);
747    }
748}