Skip to main content

feature_flag/
predicate.rs

1//! Targeting predicate DSL — typed comparisons + boolean combinators.
2//!
3//! The DSL is deliberately small. If you need a real expression language you
4//! probably want a different crate.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::Subject;
10
11/// Comparison operator for [`Predicate::Compare`].
12#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
13#[serde(rename_all = "snake_case")]
14pub enum Comparison {
15    /// `attr == value`
16    Eq,
17    /// `attr != value`
18    Ne,
19    /// `attr > value` (numeric)
20    Gt,
21    /// `attr >= value` (numeric)
22    Gte,
23    /// `attr < value` (numeric)
24    Lt,
25    /// `attr <= value` (numeric)
26    Lte,
27    /// `value` must be a JSON array; true when `attr` is one of its elements.
28    In,
29    /// Inverse of `In`.
30    NotIn,
31    /// String prefix match.
32    StartsWith,
33    /// String suffix match.
34    EndsWith,
35}
36
37/// One node of the predicate tree. Tagged via `kind` for serde so the JSON
38/// stays readable.
39#[derive(Clone, Debug, Deserialize, Serialize)]
40#[serde(tag = "kind", rename_all = "snake_case")]
41pub enum Predicate {
42    /// Always true. Useful as a default-match.
43    Always,
44    /// Look at `subject.attrs[attr]` and compare it to `value` with `op`.
45    Compare {
46        /// Attribute key (e.g. `"country"`, `"plan"`, `"email"`).
47        attr: String,
48        /// Comparison operator.
49        op: Comparison,
50        /// Right-hand side.
51        value: Value,
52    },
53    /// All children must match.
54    AllOf {
55        /// Children.
56        matchers: Vec<Predicate>,
57    },
58    /// At least one child must match.
59    AnyOf {
60        /// Children.
61        matchers: Vec<Predicate>,
62    },
63    /// Inverts the child.
64    Not {
65        /// Child.
66        matcher: Box<Predicate>,
67    },
68}
69
70impl Predicate {
71    /// Evaluate against a subject. Missing attrs make every comparison `false`
72    /// (except `NotIn` against a list — that's still true if the value isn't
73    /// there).
74    pub fn matches(&self, subject: &Subject) -> bool {
75        match self {
76            Self::Always => true,
77            Self::AllOf { matchers } => matchers.iter().all(|p| p.matches(subject)),
78            Self::AnyOf { matchers } => matchers.iter().any(|p| p.matches(subject)),
79            Self::Not { matcher } => !matcher.matches(subject),
80            Self::Compare { attr, op, value } => compare(subject.attr(attr), *op, value),
81        }
82    }
83}
84
85fn compare(actual: Option<&Value>, op: Comparison, expected: &Value) -> bool {
86    match op {
87        Comparison::Eq => actual.is_some_and(|a| a == expected),
88        Comparison::Ne => actual.is_none_or(|a| a != expected),
89        Comparison::Gt => num_cmp(actual, expected, |a, b| a > b),
90        Comparison::Gte => num_cmp(actual, expected, |a, b| a >= b),
91        Comparison::Lt => num_cmp(actual, expected, |a, b| a < b),
92        Comparison::Lte => num_cmp(actual, expected, |a, b| a <= b),
93        Comparison::In => match (actual, expected.as_array()) {
94            (Some(a), Some(arr)) => arr.iter().any(|v| v == a),
95            _ => false,
96        },
97        Comparison::NotIn => match (actual, expected.as_array()) {
98            (Some(a), Some(arr)) => !arr.iter().any(|v| v == a),
99            (None, Some(_)) => true,
100            _ => false,
101        },
102        Comparison::StartsWith => str_pair(actual, expected, |a, b| a.starts_with(b)),
103        Comparison::EndsWith => str_pair(actual, expected, |a, b| a.ends_with(b)),
104    }
105}
106
107fn num_cmp(actual: Option<&Value>, expected: &Value, f: impl Fn(f64, f64) -> bool) -> bool {
108    match (actual.and_then(Value::as_f64), expected.as_f64()) {
109        (Some(a), Some(b)) => f(a, b),
110        _ => false,
111    }
112}
113
114fn str_pair(actual: Option<&Value>, expected: &Value, f: impl Fn(&str, &str) -> bool) -> bool {
115    match (actual.and_then(Value::as_str), expected.as_str()) {
116        (Some(a), Some(b)) => f(a, b),
117        _ => false,
118    }
119}