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}