fast_decision/
types.rs

1//! Data structures and type definitions.
2//!
3//! This module contains all data structures used by the rule engine:
4//! - [`RuleSet`]: Top-level container for all categories
5//! - [`Category`]: Collection of rules with evaluation settings
6//! - [`Rule`]: Individual rule with conditions and action
7//! - [`Predicate`]: AST for condition evaluation (Comparison, AND, OR)
8//! - [`Comparison`]: Single field comparison operation
9//! - [`Operator`]: Comparison operators
10//!
11//! All types implement custom deserialization for optimal memory layout.
12
13use serde::{Deserialize, Deserializer, Serialize};
14use serde_json::Value;
15use std::collections::HashMap;
16
17/// Converts a dot-separated path string into a boxed slice of tokens.
18///
19/// # Performance
20///
21/// Uses `Box<[String]>` instead of `Vec<String>` to save 8 bytes per comparison
22/// (eliminates capacity field).
23///
24/// # Examples
25///
26/// ```ignore
27/// let tokens = tokenize_path("user.profile.age");
28/// assert_eq!(tokens.len(), 3);
29/// ```
30fn tokenize_path(path: &str) -> Box<[String]> {
31    path.split('.')
32        .map(|s| s.to_owned())
33        .collect::<Vec<_>>()
34        .into_boxed_slice()
35}
36
37/// Comparison operators.
38///
39/// # Memory Layout
40///
41/// Uses `#[repr(u8)]` for minimal memory footprint (1 byte per operator).
42///
43/// # Supported Operators
44///
45/// ## Comparison Operators
46/// - `$equals`: Equal
47/// - `$not-equals`: Not equal
48/// - `$greater-than`: Greater than
49/// - `$less-than`: Less than
50/// - `$greater-than-or-equals`: Greater than or equal
51/// - `$less-than-or-equals`: Less than or equal
52///
53/// ## Membership Operators
54/// - `$in`: Value is in array
55/// - `$not-in`: Value is not in array
56///
57/// ## String Operators
58/// - `$contains`: Case-sensitive substring check
59/// - `$starts-with`: String starts with value
60/// - `$ends-with`: String ends with value
61/// - `$regex`: Regular expression matching
62#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
63#[repr(u8)]
64pub enum Operator {
65    // Comparison operators
66    #[serde(rename = "$equals")]
67    Equal,
68    #[serde(rename = "$not-equals")]
69    NotEqual,
70    #[serde(rename = "$greater-than")]
71    GreaterThan,
72    #[serde(rename = "$less-than")]
73    LessThan,
74    #[serde(rename = "$greater-than-or-equals")]
75    GreaterThanOrEqual,
76    #[serde(rename = "$less-than-or-equals")]
77    LessThanOrEqual,
78
79    // Membership operators
80    #[serde(rename = "$in")]
81    In,
82    #[serde(rename = "$not-in")]
83    NotIn,
84
85    // String operators
86    #[serde(rename = "$contains")]
87    Contains,
88    #[serde(rename = "$starts-with")]
89    StartsWith,
90    #[serde(rename = "$ends-with")]
91    EndsWith,
92    #[serde(rename = "$regex")]
93    Regex,
94}
95
96/// A single field comparison operation.
97///
98/// # Fields
99///
100/// - `path_tokens`: Tokenized field path (e.g., `["user", "tier"]` for `"user.tier"`)
101/// - `op`: Comparison operator
102/// - `value`: Expected value to compare against
103///
104/// # Memory Optimization
105///
106/// Uses `Box<[String]>` for path tokens to minimize memory overhead.
107#[derive(Debug, Clone, Serialize)]
108pub struct Comparison {
109    #[serde(rename = "path")]
110    pub path_tokens: Box<[String]>,
111    pub op: Operator,
112    pub value: Value,
113}
114
115/// Abstract Syntax Tree (AST) node for condition evaluation.
116///
117/// Predicates can be nested to form complex logical expressions.
118///
119/// # Variants
120///
121/// - `Comparison`: Leaf node (single field comparison)
122/// - `And`: All child predicates must be true
123/// - `Or`: At least one child predicate must be true
124///
125/// # Examples
126///
127/// Simple comparison:
128/// ```json
129/// {"user.tier": {"$eq": "Gold"}}
130/// ```
131///
132/// Complex AND:
133/// ```json
134/// {"user.tier": {"$eq": "Gold"}, "amount": {"$gt": 100}}
135/// ```
136///
137/// Explicit OR:
138/// ```json
139/// {"$or": [{"tier": {"$eq": "Gold"}}, {"tier": {"$eq": "Platinum"}}]}
140/// ```
141#[derive(Debug, Clone, Serialize)]
142#[serde(untagged)]
143pub enum Predicate {
144    Comparison(Comparison),
145    #[serde(rename = "$and")]
146    And(Vec<Predicate>),
147    #[serde(rename = "$or")]
148    Or(Vec<Predicate>),
149}
150
151/// A category containing multiple rules with evaluation settings.
152///
153/// # Fields
154///
155/// - `stop_on_first`: If `true`, evaluation stops after the first matching rule
156/// - `rules`: List of rules (automatically sorted by priority during deserialization)
157///
158/// # Priority Sorting
159///
160/// Rules are sorted by priority (lower value = higher precedence) when deserialized.
161#[derive(Debug, Clone)]
162pub struct Category {
163    pub stop_on_first: bool,
164    pub rules: Vec<Rule>,
165}
166
167/// An individual rule with conditions and action.
168///
169/// # Fields
170///
171/// - `id`: Unique identifier for the rule
172/// - `priority`: Evaluation priority (lower = higher precedence, default: 0)
173/// - `predicate`: Condition tree (deserialized from `conditions` field)
174/// - `action`: Action identifier (informational, not evaluated by engine)
175/// - `metadata`: Optional metadata for tracing, compliance, or custom annotations
176///
177/// # JSON Format
178///
179/// ```json
180/// {
181///   "id": "Premium_User",
182///   "priority": 1,
183///   "conditions": {"user.tier": {"$equals": "Gold"}},
184///   "action": "apply_discount",
185///   "metadata": {
186///     "source": "Pricing Rules v2.3",
187///     "tags": ["premium", "discount"]
188///   }
189/// }
190/// ```
191///
192/// The `metadata` field is optional and will be included in evaluation results if present.
193#[derive(Debug, Clone, Serialize)]
194pub struct Rule {
195    pub id: String,
196    pub priority: i32,
197    #[serde(rename = "conditions")]
198    pub predicate: Predicate,
199    pub action: String,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub metadata: Option<serde_json::Map<String, Value>>,
202}
203
204impl Predicate {
205    /// Recursively deserializes a serde_json::Value into a Predicate AST.
206    fn deserialize_from_value(value: Value) -> Result<Self, String> {
207        let map = value
208            .as_object()
209            .ok_or_else(|| format!("Predicate must be a JSON object{}", ""))?;
210
211        let mut predicates = Vec::new();
212
213        for (key, val) in map {
214            match key.as_str() {
215                // Handle explicit AND/OR operators
216                "$and" | "$or" => {
217                    let arr = val
218                        .as_array()
219                        .ok_or_else(|| format!("'{}' must be an array of objects", key))?;
220
221                    if map.len() > 1 {
222                        return Err(format!(
223                            "If '{}' is present, it must be the only top-level key in the predicate",
224                            key
225                        ));
226                    }
227
228                    let children: Result<Vec<Predicate>, _> = arr
229                        .iter()
230                        .cloned()
231                        .map(Predicate::deserialize_from_value) // Recursive call
232                        .collect();
233
234                    let children = children?;
235
236                    return match key.as_str() {
237                        "$and" => Ok(Predicate::And(children)),
238                        "$or" => Ok(Predicate::Or(children)),
239                        _ => unreachable!(),
240                    };
241                }
242                // Handle field path (leaf node)
243                field_path => {
244                    // This must be an object of operators: {"path": {"$op": value}}
245                    let operators_map = val.as_object().ok_or_else(|| {
246                        format!(
247                            "Value for field path '{}' must be an object of operators",
248                            field_path
249                        )
250                    })?;
251
252                    // Flat structure of conditions (implicit AND)
253                    for (op_str, comp_value) in operators_map {
254                        let op = match op_str.as_str() {
255                            "$equals" => Operator::Equal,
256                            "$not-equals" => Operator::NotEqual,
257                            "$greater-than" => Operator::GreaterThan,
258                            "$less-than" => Operator::LessThan,
259                            "$greater-than-or-equals" => Operator::GreaterThanOrEqual,
260                            "$less-than-or-equals" => Operator::LessThanOrEqual,
261                            "$in" => Operator::In,
262                            "$not-in" => Operator::NotIn,
263                            "$contains" => Operator::Contains,
264                            "$starts-with" => Operator::StartsWith,
265                            "$ends-with" => Operator::EndsWith,
266                            "$regex" => Operator::Regex,
267                            _ => return Err(format!("Unknown operator: {}", op_str)),
268                        };
269
270                        predicates.push(Predicate::Comparison(Comparison {
271                            path_tokens: tokenize_path(field_path),
272                            op,
273                            value: comp_value.clone(),
274                        }));
275                    }
276                }
277            }
278        }
279
280        // If we reached this point, we processed a flat structure (implicit AND).
281        match predicates.len() {
282            0 => Err(format!(
283                "Rule condition must contain at least one comparison{}",
284                ""
285            )),
286            1 => Ok(predicates.pop().unwrap()), // Single condition
287            _ => Ok(Predicate::And(predicates)), // Implicit AND
288        }
289    }
290}
291
292impl Category {
293    /// Checks for rules with duplicate priorities and logs warnings.
294    ///
295    /// Duplicate priorities may result in non-deterministic evaluation order
296    /// for rules with the same priority value.
297    ///
298    /// # Arguments
299    ///
300    /// * `category_name` - Name of the category (for logging)
301    pub fn warn_duplicate_priorities(&self, category_name: &str) {
302        use std::collections::HashMap;
303        let mut priority_count: HashMap<i32, Vec<&str>> = HashMap::new();
304
305        for rule in &self.rules {
306            priority_count
307                .entry(rule.priority)
308                .or_default()
309                .push(&rule.id);
310        }
311
312        for (priority, ids) in priority_count {
313            if ids.len() > 1 {
314                log::warn!(
315                    "Category '{}': Multiple rules with priority {}: {:?}",
316                    category_name,
317                    priority,
318                    ids
319                );
320            }
321        }
322    }
323}
324
325impl<'de> Deserialize<'de> for Category {
326    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
327    where
328        D: Deserializer<'de>,
329    {
330        #[derive(Deserialize)]
331        #[serde(untagged)]
332        enum CategoryHelper {
333            WithConfig {
334                stop_on_first: bool,
335                rules: Vec<Rule>,
336            },
337            Simple(Vec<Rule>),
338        }
339
340        match CategoryHelper::deserialize(deserializer)? {
341            CategoryHelper::WithConfig {
342                stop_on_first,
343                mut rules,
344            } => {
345                rules.sort_by_key(|r| r.priority);
346                Ok(Category {
347                    stop_on_first,
348                    rules,
349                })
350            }
351            CategoryHelper::Simple(mut rules) => {
352                rules.sort_by_key(|r| r.priority);
353                Ok(Category {
354                    stop_on_first: false,
355                    rules,
356                })
357            }
358        }
359    }
360}
361
362impl<'de> Deserialize<'de> for Rule {
363    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
364    where
365        D: Deserializer<'de>,
366    {
367        #[derive(Deserialize)]
368        struct RuleHelper {
369            id: String,
370            #[serde(default)]
371            priority: i32,
372            conditions: Value,
373            action: String,
374            #[serde(default)]
375            metadata: Option<serde_json::Map<String, Value>>,
376        }
377
378        let helper = RuleHelper::deserialize(deserializer)?;
379
380        let predicate = Predicate::deserialize_from_value(helper.conditions)
381            .map_err(serde::de::Error::custom)?;
382
383        Ok(Rule {
384            id: helper.id,
385            priority: helper.priority,
386            predicate,
387            action: helper.action,
388            metadata: helper.metadata,
389        })
390    }
391}
392
393/// Top-level container for all rule categories.
394///
395/// # JSON Format
396///
397/// ```json
398/// {
399///   "categories": {
400///     "Pricing": {
401///       "stop_on_first": true,
402///       "rules": [...]
403///     },
404///     "Fraud": {
405///       "stop_on_first": false,
406///       "rules": [...]
407///     }
408///   }
409/// }
410/// ```
411///
412/// # Performance
413///
414/// Uses `HashMap` for O(1) category lookup by name.
415#[derive(Debug, Deserialize, Clone)]
416pub struct RuleSet {
417    pub categories: HashMap<String, Category>,
418}