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 execution 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`]: MongoDB-style comparison operators
10//!
11//! All types implement custom deserialization for optimal memory layout.
12
13use serde::{Deserialize, Deserializer};
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/// MongoDB-style 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/// - `$eq`: Equal
46/// - `$ne`: Not equal
47/// - `$gt`: Greater than
48/// - `$lt`: Less than
49/// - `$gte`: Greater than or equal
50/// - `$lte`: Less than or equal
51#[derive(Debug, Deserialize, Clone, Copy)]
52#[repr(u8)]
53pub enum Operator {
54    #[serde(rename = "$eq")]
55    Equal,
56    #[serde(rename = "$ne")]
57    NotEqual,
58    #[serde(rename = "$gt")]
59    GreaterThan,
60    #[serde(rename = "$lt")]
61    LessThan,
62    #[serde(rename = "$gte")]
63    GreaterThanOrEqual,
64    #[serde(rename = "$lte")]
65    LessThanOrEqual,
66}
67
68/// A single field comparison operation.
69///
70/// # Fields
71///
72/// - `path_tokens`: Tokenized field path (e.g., `["user", "tier"]` for `"user.tier"`)
73/// - `op`: Comparison operator
74/// - `value`: Expected value to compare against
75///
76/// # Memory Optimization
77///
78/// Uses `Box<[String]>` for path tokens to minimize memory overhead.
79#[derive(Debug, Clone)]
80pub struct Comparison {
81    pub path_tokens: Box<[String]>,
82    pub op: Operator,
83    pub value: Value,
84}
85
86/// Abstract Syntax Tree (AST) node for condition evaluation.
87///
88/// Predicates can be nested to form complex logical expressions.
89///
90/// # Variants
91///
92/// - `Comparison`: Leaf node (single field comparison)
93/// - `And`: All child predicates must be true
94/// - `Or`: At least one child predicate must be true
95///
96/// # Examples
97///
98/// Simple comparison:
99/// ```json
100/// {"user.tier": {"$eq": "Gold"}}
101/// ```
102///
103/// Complex AND:
104/// ```json
105/// {"user.tier": {"$eq": "Gold"}, "amount": {"$gt": 100}}
106/// ```
107///
108/// Explicit OR:
109/// ```json
110/// {"$or": [{"tier": {"$eq": "Gold"}}, {"tier": {"$eq": "Platinum"}}]}
111/// ```
112#[derive(Debug, Clone)]
113pub enum Predicate {
114    Comparison(Comparison),
115    And(Vec<Predicate>),
116    Or(Vec<Predicate>),
117}
118
119/// A category containing multiple rules with execution settings.
120///
121/// # Fields
122///
123/// - `stop_on_first`: If `true`, execution stops after the first matching rule
124/// - `rules`: List of rules (automatically sorted by priority during deserialization)
125///
126/// # Priority Sorting
127///
128/// Rules are sorted by priority (lower value = higher precedence) when deserialized.
129#[derive(Debug, Clone)]
130pub struct Category {
131    pub stop_on_first: bool,
132    pub rules: Vec<Rule>,
133}
134
135/// An individual rule with conditions and action.
136///
137/// # Fields
138///
139/// - `id`: Unique identifier for the rule
140/// - `priority`: Execution priority (lower = higher precedence, default: 0)
141/// - `predicate`: Condition tree (deserialized from `conditions` field)
142/// - `action`: Action identifier (informational, not executed by engine)
143///
144/// # JSON Format
145///
146/// ```json
147/// {
148///   "id": "Premium_User",
149///   "priority": 1,
150///   "conditions": {"user.tier": {"$eq": "Gold"}},
151///   "action": "apply_discount"
152/// }
153/// ```
154#[derive(Debug, Clone)]
155pub struct Rule {
156    pub id: String,
157    pub priority: i32,
158    pub predicate: Predicate,
159    pub action: String,
160}
161
162impl Predicate {
163    /// Recursively deserializes a serde_json::Value into a Predicate AST.
164    fn deserialize_from_value(value: Value) -> Result<Self, String> {
165        let map = value
166            .as_object()
167            .ok_or_else(|| format!("Predicate must be a JSON object{}", ""))?;
168
169        let mut predicates = Vec::new();
170
171        for (key, val) in map {
172            match key.as_str() {
173                // Handle explicit AND/OR operators
174                "$and" | "$or" => {
175                    let arr = val
176                        .as_array()
177                        .ok_or_else(|| format!("'{}' must be an array of objects", key))?;
178
179                    if map.len() > 1 {
180                        return Err(format!(
181                            "If '{}' is present, it must be the only top-level key in the predicate",
182                            key
183                        ));
184                    }
185
186                    let children: Result<Vec<Predicate>, _> = arr
187                        .iter()
188                        .cloned()
189                        .map(Predicate::deserialize_from_value) // Recursive call
190                        .collect();
191
192                    let children = children?;
193
194                    return match key.as_str() {
195                        "$and" => Ok(Predicate::And(children)),
196                        "$or" => Ok(Predicate::Or(children)),
197                        _ => unreachable!(),
198                    };
199                }
200                // Handle field path (leaf node)
201                field_path => {
202                    // This must be an object of operators: {"path": {"$op": value}}
203                    let operators_map = val.as_object().ok_or_else(|| {
204                        format!(
205                            "Value for field path '{}' must be an object of operators",
206                            field_path
207                        )
208                    })?;
209
210                    // Flat structure of conditions (implicit AND)
211                    for (op_str, comp_value) in operators_map {
212                        let op = match op_str.as_str() {
213                            "$eq" => Operator::Equal,
214                            "$ne" => Operator::NotEqual,
215                            "$gt" => Operator::GreaterThan,
216                            "$lt" => Operator::LessThan,
217                            "$gte" => Operator::GreaterThanOrEqual,
218                            "$lte" => Operator::LessThanOrEqual,
219                            _ => return Err(format!("Unknown operator: {}", op_str)),
220                        };
221
222                        predicates.push(Predicate::Comparison(Comparison {
223                            path_tokens: tokenize_path(field_path),
224                            op,
225                            value: comp_value.clone(),
226                        }));
227                    }
228                }
229            }
230        }
231
232        // If we reached this point, we processed a flat structure (implicit AND).
233        match predicates.len() {
234            0 => Err(format!(
235                "Rule condition must contain at least one comparison{}",
236                ""
237            )),
238            1 => Ok(predicates.pop().unwrap()), // Single condition
239            _ => Ok(Predicate::And(predicates)), // Implicit AND
240        }
241    }
242}
243
244impl Category {
245    /// Checks for rules with duplicate priorities and logs warnings.
246    ///
247    /// Duplicate priorities may result in non-deterministic execution order
248    /// for rules with the same priority value.
249    ///
250    /// # Arguments
251    ///
252    /// * `category_name` - Name of the category (for logging)
253    pub fn warn_duplicate_priorities(&self, category_name: &str) {
254        use std::collections::HashMap;
255        let mut priority_count: HashMap<i32, Vec<&str>> = HashMap::new();
256
257        for rule in &self.rules {
258            priority_count
259                .entry(rule.priority)
260                .or_default()
261                .push(&rule.id);
262        }
263
264        for (priority, ids) in priority_count {
265            if ids.len() > 1 {
266                log::warn!(
267                    "Category '{}': Multiple rules with priority {}: {:?}",
268                    category_name,
269                    priority,
270                    ids
271                );
272            }
273        }
274    }
275}
276
277impl<'de> Deserialize<'de> for Category {
278    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
279    where
280        D: Deserializer<'de>,
281    {
282        #[derive(Deserialize)]
283        #[serde(untagged)]
284        enum CategoryHelper {
285            WithConfig {
286                stop_on_first: bool,
287                rules: Vec<Rule>,
288            },
289            Simple(Vec<Rule>),
290        }
291
292        match CategoryHelper::deserialize(deserializer)? {
293            CategoryHelper::WithConfig {
294                stop_on_first,
295                mut rules,
296            } => {
297                rules.sort_by_key(|r| r.priority);
298                Ok(Category {
299                    stop_on_first,
300                    rules,
301                })
302            }
303            CategoryHelper::Simple(mut rules) => {
304                rules.sort_by_key(|r| r.priority);
305                Ok(Category {
306                    stop_on_first: false,
307                    rules,
308                })
309            }
310        }
311    }
312}
313
314impl<'de> Deserialize<'de> for Rule {
315    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
316    where
317        D: Deserializer<'de>,
318    {
319        #[derive(Deserialize)]
320        struct RuleHelper {
321            id: String,
322            #[serde(default)]
323            priority: i32,
324            conditions: Value,
325            action: String,
326        }
327
328        let helper = RuleHelper::deserialize(deserializer)?;
329
330        let predicate = Predicate::deserialize_from_value(helper.conditions)
331            .map_err(serde::de::Error::custom)?;
332
333        Ok(Rule {
334            id: helper.id,
335            priority: helper.priority,
336            predicate,
337            action: helper.action,
338        })
339    }
340}
341
342/// Top-level container for all rule categories.
343///
344/// # JSON Format
345///
346/// ```json
347/// {
348///   "categories": {
349///     "Pricing": {
350///       "stop_on_first": true,
351///       "rules": [...]
352///     },
353///     "Fraud": {
354///       "stop_on_first": false,
355///       "rules": [...]
356///     }
357///   }
358/// }
359/// ```
360///
361/// # Performance
362///
363/// Uses `HashMap` for O(1) category lookup by name.
364#[derive(Debug, Deserialize, Clone)]
365pub struct RuleSet {
366    pub categories: HashMap<String, Category>,
367}