json_rules_engine_fork/
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
321pub fn string_is_subset(field: &str, val: Vec<&str>) -> Condition {
322    Condition::Condition {
323        field: field.into(),
324        constraint: Constraint::StringIsSubset(
325            val.into_iter().map(ToOwned::to_owned).collect(),
326        ),
327        path: None,
328    }
329}
330
331pub fn string_is_substring(field: &str, val: &str) -> Condition {
332    Condition::Condition {
333        field: field.into(),
334        constraint: Constraint::StringIsSubstring(val.into()),
335        path: None,
336    }
337}
338
339pub fn string_has_substring(field: &str, val: &str) -> Condition {
340    Condition::Condition {
341        field: field.into(),
342        constraint: Constraint::StringHasSubstring(val.into()),
343        path: None,
344    }
345}
346
347/// Creates a rule for int comparison.
348pub fn int_equals(field: &str, val: i64) -> Condition {
349    Condition::Condition {
350        field: field.into(),
351        constraint: Constraint::IntEquals(val),
352        path: None,
353    }
354}
355
356pub fn int_not_equals(field: &str, val: i64) -> Condition {
357    Condition::Condition {
358        field: field.into(),
359        constraint: Constraint::IntNotEquals(val),
360        path: None,
361    }
362}
363
364pub fn int_contains(field: &str, val: i64) -> Condition {
365    Condition::Condition {
366        field: field.into(),
367        constraint: Constraint::IntContains(val),
368        path: None,
369    }
370}
371
372pub fn int_contains_all(field: &str, val: Vec<i64>) -> Condition {
373    Condition::Condition {
374        field: field.into(),
375        constraint: Constraint::IntContainsAll(val),
376        path: None,
377    }
378}
379
380pub fn int_contains_any(field: &str, val: Vec<i64>) -> Condition {
381    Condition::Condition {
382        field: field.into(),
383        constraint: Constraint::IntContainsAny(val),
384        path: None,
385    }
386}
387
388pub fn int_does_not_contain(field: &str, val: i64) -> Condition {
389    Condition::Condition {
390        field: field.into(),
391        constraint: Constraint::IntDoesNotContain(val),
392        path: None,
393    }
394}
395
396pub fn int_does_not_contain_any(field: &str, val: Vec<i64>) -> Condition {
397    Condition::Condition {
398        field: field.into(),
399        constraint: Constraint::IntDoesNotContainAny(val),
400        path: None,
401    }
402}
403
404pub fn int_in(field: &str, val: Vec<i64>) -> Condition {
405    Condition::Condition {
406        field: field.into(),
407        constraint: Constraint::IntIn(val),
408        path: None,
409    }
410}
411
412pub fn int_not_in(field: &str, val: Vec<i64>) -> Condition {
413    Condition::Condition {
414        field: field.into(),
415        constraint: Constraint::IntNotIn(val),
416        path: None,
417    }
418}
419
420pub fn int_in_range(field: &str, start: i64, end: i64) -> Condition {
421    Condition::Condition {
422        field: field.into(),
423        constraint: Constraint::IntInRange(start, end),
424        path: None,
425    }
426}
427
428pub fn int_not_in_range(field: &str, start: i64, end: i64) -> Condition {
429    Condition::Condition {
430        field: field.into(),
431        constraint: Constraint::IntNotInRange(start, end),
432        path: None,
433    }
434}
435
436pub fn int_less_than(field: &str, val: i64) -> Condition {
437    Condition::Condition {
438        field: field.into(),
439        constraint: Constraint::IntLessThan(val),
440        path: None,
441    }
442}
443
444pub fn int_less_than_inclusive(field: &str, val: i64) -> Condition {
445    Condition::Condition {
446        field: field.into(),
447        constraint: Constraint::IntLessThanInclusive(val),
448        path: None,
449    }
450}
451
452pub fn int_greater_than(field: &str, val: i64) -> Condition {
453    Condition::Condition {
454        field: field.into(),
455        constraint: Constraint::IntGreaterThan(val),
456        path: None,
457    }
458}
459
460pub fn int_greater_than_inclusive(field: &str, val: i64) -> Condition {
461    Condition::Condition {
462        field: field.into(),
463        constraint: Constraint::IntGreaterThanInclusive(val),
464        path: None,
465    }
466}
467
468/// Creates a rule for float comparison.
469pub fn float_equals(field: &str, val: f64) -> Condition {
470    Condition::Condition {
471        field: field.into(),
472        constraint: Constraint::FloatEquals(val),
473        path: None,
474    }
475}
476
477pub fn float_not_equals(field: &str, val: f64) -> Condition {
478    Condition::Condition {
479        field: field.into(),
480        constraint: Constraint::FloatNotEquals(val),
481        path: None,
482    }
483}
484
485pub fn float_contains(field: &str, val: f64) -> Condition {
486    Condition::Condition {
487        field: field.into(),
488        constraint: Constraint::FloatContains(val),
489        path: None,
490    }
491}
492
493pub fn float_does_not_contain(field: &str, val: f64) -> Condition {
494    Condition::Condition {
495        field: field.into(),
496        constraint: Constraint::FloatDoesNotContain(val),
497        path: None,
498    }
499}
500
501pub fn float_in(field: &str, val: Vec<f64>) -> Condition {
502    Condition::Condition {
503        field: field.into(),
504        constraint: Constraint::FloatIn(val),
505        path: None,
506    }
507}
508
509pub fn float_not_in(field: &str, val: Vec<f64>) -> Condition {
510    Condition::Condition {
511        field: field.into(),
512        constraint: Constraint::FloatNotIn(val),
513        path: None,
514    }
515}
516
517pub fn float_in_range(field: &str, start: f64, end: f64) -> Condition {
518    Condition::Condition {
519        field: field.into(),
520        constraint: Constraint::FloatInRange(start, end),
521        path: None,
522    }
523}
524
525pub fn float_not_in_range(field: &str, start: f64, end: f64) -> Condition {
526    Condition::Condition {
527        field: field.into(),
528        constraint: Constraint::FloatNotInRange(start, end),
529        path: None,
530    }
531}
532
533pub fn float_less_than(field: &str, val: f64) -> Condition {
534    Condition::Condition {
535        field: field.into(),
536        constraint: Constraint::FloatLessThan(val),
537        path: None,
538    }
539}
540
541pub fn float_less_than_inclusive(field: &str, val: f64) -> Condition {
542    Condition::Condition {
543        field: field.into(),
544        constraint: Constraint::FloatLessThanInclusive(val),
545        path: None,
546    }
547}
548
549pub fn float_greater_than(field: &str, val: f64) -> Condition {
550    Condition::Condition {
551        field: field.into(),
552        constraint: Constraint::FloatGreaterThan(val),
553        path: None,
554    }
555}
556
557pub fn float_greater_than_inclusive(field: &str, val: f64) -> Condition {
558    Condition::Condition {
559        field: field.into(),
560        constraint: Constraint::FloatGreaterThanInclusive(val),
561        path: None,
562    }
563}
564
565/// Creates a rule for boolean comparison.
566pub fn bool_equals(field: &str, val: bool) -> Condition {
567    Condition::Condition {
568        field: field.into(),
569        constraint: Constraint::BoolEquals(val),
570        path: None,
571    }
572}
573
574#[cfg(not(feature = "eval"))]
575#[cfg(test)]
576mod tests {
577    use super::{
578        and, at_least, bool_equals, int_equals, int_in_range, or, string_equals, string_is_subset
579    };
580    use crate::{status::Status, string_is_substring, string_has_substring};
581    use serde_json::{json, Value};
582
583    fn get_test_data() -> Value {
584        json!({
585            "foo": 1,
586            "bar": "bar",
587            "baz": true
588        })
589    }
590
591    #[test]
592    fn and_rules() {
593        let map = get_test_data();
594        // Met & Met == Met
595        let mut root =
596            and(vec![int_equals("foo", 1), string_equals("bar", "bar")]);
597        let mut res = root.check_value(&map);
598
599        assert_eq!(res.status, Status::Met);
600
601        // Met & NotMet == NotMet
602        root = and(vec![int_equals("foo", 2), string_equals("bar", "bar")]);
603        res = root.check_value(&map);
604
605        assert_eq!(res.status, Status::NotMet);
606
607        // Met & Unknown == Unknown
608        root = and(vec![int_equals("quux", 2), string_equals("bar", "bar")]);
609        res = root.check_value(&map);
610
611        assert_eq!(res.status, Status::Unknown);
612
613        // NotMet & Unknown == NotMet
614        root = and(vec![int_equals("quux", 2), string_equals("bar", "baz")]);
615        res = root.check_value(&map);
616
617        assert_eq!(res.status, Status::NotMet);
618
619        // Unknown & Unknown == Unknown
620        root = and(vec![int_equals("quux", 2), string_equals("fizz", "bar")]);
621        res = root.check_value(&map);
622
623        assert_eq!(res.status, Status::Unknown);
624    }
625
626    #[test]
627    fn or_rules() {
628        let map = get_test_data();
629        // Met | Met == Met
630        let mut root =
631            or(vec![int_equals("foo", 1), string_equals("bar", "bar")]);
632        let mut res = root.check_value(&map);
633
634        assert_eq!(res.status, Status::Met);
635
636        // Met | NotMet == Met
637        root = or(vec![int_equals("foo", 2), string_equals("bar", "bar")]);
638        res = root.check_value(&map);
639
640        assert_eq!(res.status, Status::Met);
641
642        // Met | Unknown == Met
643        root = or(vec![int_equals("quux", 2), string_equals("bar", "bar")]);
644        res = root.check_value(&map);
645
646        assert_eq!(res.status, Status::Met);
647
648        // NotMet | Unknown == Unknown
649        root = or(vec![int_equals("quux", 2), string_equals("bar", "baz")]);
650        res = root.check_value(&map);
651
652        assert_eq!(res.status, Status::Unknown);
653
654        // Unknown | Unknown == Unknown
655        root = or(vec![int_equals("quux", 2), string_equals("fizz", "bar")]);
656        res = root.check_value(&map);
657
658        assert_eq!(res.status, Status::Unknown);
659    }
660
661    #[test]
662    fn n_of_rules() {
663        let map = get_test_data();
664        // 2 Met, 1 NotMet == Met
665        let mut root = at_least(
666            2,
667            vec![
668                int_equals("foo", 1),
669                string_equals("bar", "bar"),
670                bool_equals("baz", false),
671            ],
672        );
673        let mut res = root.check_value(&map);
674
675        assert_eq!(res.status, Status::Met);
676
677        // 1 Met, 1 NotMet, 1 Unknown == NotMet
678        root = at_least(
679            2,
680            vec![
681                int_equals("foo", 1),
682                string_equals("quux", "bar"),
683                bool_equals("baz", false),
684            ],
685        );
686        res = root.check_value(&map);
687
688        assert_eq!(res.status, Status::NotMet);
689
690        // 2 NotMet, 1 Unknown == Unknown
691        root = at_least(
692            2,
693            vec![
694                int_equals("foo", 2),
695                string_equals("quux", "baz"),
696                bool_equals("baz", false),
697            ],
698        );
699        res = root.check_value(&map);
700
701        assert_eq!(res.status, Status::NotMet);
702    }
703
704    #[test]
705    fn string_equals_rule() {
706        let map = get_test_data();
707        let mut rule = string_equals("bar", "bar");
708        let mut res = rule.check_value(&map);
709        assert_eq!(res.status, Status::Met);
710
711        rule = string_equals("bar", "baz");
712        res = rule.check_value(&map);
713        assert_eq!(res.status, Status::NotMet);
714    }
715
716    #[test]
717    fn string_is_subset_rule() {
718        let map = json!({ "foo": ["a", "b"] });
719        let rule = string_is_subset("foo", vec!["a", "c", "b"]);
720        let res = rule.check_value(&map);
721        assert_eq!(res.status, Status::Met);
722        let rule = string_is_subset("foo", vec!["a", "b"]);
723        let res = rule.check_value(&map);
724        assert_eq!(res.status, Status::Met);
725        let rule = string_is_subset("foo", vec!["a", "c"]);
726        let res = rule.check_value(&map);
727        assert_eq!(res.status, Status::NotMet);
728        let rule = string_is_subset("foo", vec![]);
729        let res = rule.check_value(&map);
730        assert_eq!(res.status, Status::NotMet);
731
732        // empty facts
733        let map = json!({ "foo": [] });
734        let rule = string_is_subset("foo", vec!["a", "c", "b"]);
735        let res = rule.check_value(&map);
736        assert_eq!(res.status, Status::Met);
737    }
738
739    #[test]
740    fn string_is_substring_rule() {
741        let map = json!({ "foo": "abc" });
742        let rule = string_is_substring("foo", "abcd");
743        let res = rule.check_value(&map);
744        assert_eq!(res.status, Status::Met);
745
746        let map = json!({ "foo": "abc" });
747        let rule = string_is_substring("foo", "ab");
748        let res = rule.check_value(&map);
749        assert_eq!(res.status, Status::NotMet);
750    }
751
752    #[test]
753    fn string_has_substring_rule() {
754        let map = json!({ "foo": "abc" });
755        let rule = string_has_substring("foo", "ab");
756        let res = rule.check_value(&map);
757        assert_eq!(res.status, Status::Met);
758
759        let map = json!({ "foo": "abc" });
760        let rule = string_has_substring("foo", "abcd");
761        let res = rule.check_value(&map);
762        assert_eq!(res.status, Status::NotMet);
763    }
764
765    #[test]
766    fn int_equals_rule() {
767        let map = get_test_data();
768        let mut rule = int_equals("foo", 1);
769        let mut res = rule.check_value(&map);
770        assert_eq!(res.status, Status::Met);
771
772        rule = int_equals("foo", 2);
773        res = rule.check_value(&map);
774        assert_eq!(res.status, Status::NotMet);
775
776        // Values not convertible to int should be NotMet
777        rule = int_equals("bar", 2);
778        res = rule.check_value(&map);
779        assert_eq!(res.status, Status::NotMet);
780    }
781
782    #[test]
783    fn int_range_rule() {
784        let map = get_test_data();
785        let mut rule = int_in_range("foo", 1, 3);
786        let mut res = rule.check_value(&map);
787        assert_eq!(res.status, Status::Met);
788
789        rule = int_in_range("foo", 2, 3);
790        res = rule.check_value(&map);
791        assert_eq!(res.status, Status::NotMet);
792
793        // Values not convertible to int should be NotMet
794        rule = int_in_range("bar", 1, 3);
795        res = rule.check_value(&map);
796        assert_eq!(res.status, Status::NotMet);
797    }
798
799    #[test]
800    fn boolean_rule() {
801        let mut map = get_test_data();
802        let mut rule = bool_equals("baz", true);
803        let mut res = rule.check_value(&map);
804        assert_eq!(res.status, Status::Met);
805
806        rule = bool_equals("baz", false);
807        res = rule.check_value(&map);
808        assert_eq!(res.status, Status::NotMet);
809
810        rule = bool_equals("bar", true);
811        res = rule.check_value(&map);
812        assert_eq!(res.status, Status::NotMet);
813
814        rule = bool_equals("bar", false);
815        res = rule.check_value(&map);
816        assert_eq!(res.status, Status::NotMet);
817
818        map["quux".to_owned()] = json!("tRuE");
819        rule = bool_equals("quux", true);
820        res = rule.check_value(&map);
821        assert_eq!(res.status, Status::NotMet);
822    }
823}