Skip to main content

mockforge_intelligence/behavioral_economics/
conditions.rs

1//! Behavior condition definitions and evaluation
2//!
3//! Defines conditions that can be evaluated to determine if a behavior rule
4//! should trigger. Conditions can be simple (latency threshold) or composite
5//! (multiple conditions with logical operators).
6
7use mockforge_foundation::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Logical operator for composite conditions
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14#[serde(rename_all = "snake_case")]
15pub enum LogicalOp {
16    /// All conditions must be true (AND)
17    And,
18    /// At least one condition must be true (OR)
19    Or,
20    /// All conditions must be false (NOR)
21    Nor,
22}
23
24/// Behavior condition
25///
26/// Conditions are evaluated to determine if a behavior rule should trigger.
27/// Conditions can be simple (single check) or composite (multiple conditions).
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30#[serde(tag = "type", rename_all = "snake_case")]
31pub enum BehaviorCondition {
32    /// Always true (for testing or default behavior)
33    Always,
34
35    /// Latency threshold condition
36    LatencyThreshold {
37        /// Endpoint pattern to match
38        endpoint: String,
39        /// Threshold in milliseconds
40        threshold_ms: u64,
41    },
42
43    /// Load pressure condition
44    LoadPressure {
45        /// Threshold in requests per second
46        threshold_rps: f64,
47    },
48
49    /// Pricing change condition
50    PricingChange {
51        /// Product ID pattern
52        product_id: String,
53        /// Threshold change percentage
54        threshold: f64,
55    },
56
57    /// Fraud suspicion condition
58    FraudSuspicion {
59        /// User ID pattern
60        user_id: String,
61        /// Risk score threshold (0.0 to 1.0)
62        risk_score: f64,
63    },
64
65    /// Customer segment condition
66    CustomerSegment {
67        /// Segment name
68        segment: String,
69    },
70
71    /// Error rate condition
72    ErrorRate {
73        /// Endpoint pattern
74        endpoint: String,
75        /// Error rate threshold (0.0 to 1.0)
76        threshold: f64,
77    },
78
79    /// Composite condition (multiple conditions with logical operator)
80    Composite {
81        /// Logical operator
82        operator: LogicalOp,
83        /// List of conditions
84        conditions: Vec<BehaviorCondition>,
85    },
86}
87
88/// Condition evaluator
89///
90/// Evaluates behavior conditions based on current system state.
91pub struct ConditionEvaluator {
92    /// Current latency metrics (endpoint -> latency_ms)
93    latency_metrics: HashMap<String, u64>,
94    /// Current load metrics (requests per second)
95    load_rps: f64,
96    /// Current error rates (endpoint -> error_rate)
97    error_rates: HashMap<String, f64>,
98    /// Current pricing data (product_id -> price)
99    pricing_data: HashMap<String, f64>,
100    /// Previous pricing data for detecting changes (product_id -> price)
101    previous_pricing_data: HashMap<String, f64>,
102    /// Current fraud scores (user_id -> risk_score)
103    fraud_scores: HashMap<String, f64>,
104    /// Current customer segments (user_id -> segment)
105    customer_segments: HashMap<String, String>,
106}
107
108impl ConditionEvaluator {
109    /// Create a new condition evaluator
110    pub fn new() -> Self {
111        Self {
112            latency_metrics: HashMap::new(),
113            load_rps: 0.0,
114            error_rates: HashMap::new(),
115            pricing_data: HashMap::new(),
116            previous_pricing_data: HashMap::new(),
117            fraud_scores: HashMap::new(),
118            customer_segments: HashMap::new(),
119        }
120    }
121
122    /// Update latency metric for an endpoint
123    pub fn update_latency(&mut self, endpoint: &str, latency_ms: u64) {
124        self.latency_metrics.insert(endpoint.to_string(), latency_ms);
125    }
126
127    /// Update load metric
128    pub fn update_load(&mut self, rps: f64) {
129        self.load_rps = rps;
130    }
131
132    /// Update error rate for an endpoint
133    pub fn update_error_rate(&mut self, endpoint: &str, error_rate: f64) {
134        self.error_rates.insert(endpoint.to_string(), error_rate);
135    }
136
137    /// Update pricing data, preserving the previous price for change detection
138    pub fn update_pricing(&mut self, product_id: &str, price: f64) {
139        if let Some(old_price) = self.pricing_data.get(product_id) {
140            self.previous_pricing_data.insert(product_id.to_string(), *old_price);
141        }
142        self.pricing_data.insert(product_id.to_string(), price);
143    }
144
145    /// Update fraud score
146    pub fn update_fraud_score(&mut self, user_id: &str, risk_score: f64) {
147        self.fraud_scores.insert(user_id.to_string(), risk_score);
148    }
149
150    /// Update customer segment
151    pub fn update_customer_segment(&mut self, user_id: &str, segment: String) {
152        self.customer_segments.insert(user_id.to_string(), segment);
153    }
154
155    /// Evaluate a condition
156    pub fn evaluate(&self, condition: &BehaviorCondition) -> Result<bool> {
157        match condition {
158            BehaviorCondition::Always => Ok(true),
159
160            BehaviorCondition::LatencyThreshold {
161                endpoint,
162                threshold_ms,
163            } => {
164                // Simple pattern matching (supports wildcards)
165                let matches = self.latency_metrics.iter().any(|(ep, latency)| {
166                    self.matches_pattern(ep, endpoint) && *latency > *threshold_ms
167                });
168                Ok(matches)
169            }
170
171            BehaviorCondition::LoadPressure { threshold_rps } => Ok(self.load_rps > *threshold_rps),
172
173            BehaviorCondition::PricingChange {
174                product_id,
175                threshold,
176            } => {
177                // Check if price change percentage exceeds threshold
178                let current = match self.pricing_data.get(product_id) {
179                    Some(price) => *price,
180                    None => return Ok(false),
181                };
182                let previous = match self.previous_pricing_data.get(product_id) {
183                    Some(price) => *price,
184                    None => return Ok(false), // No history yet
185                };
186                if previous == 0.0 {
187                    // Avoid division by zero; any change from zero is significant
188                    return Ok(current != 0.0);
189                }
190                let pct_change = ((current - previous) / previous).abs() * 100.0;
191                Ok(pct_change > *threshold)
192            }
193
194            BehaviorCondition::FraudSuspicion {
195                user_id,
196                risk_score,
197            } => {
198                let score = self.fraud_scores.get(user_id).copied().unwrap_or(0.0);
199                Ok(score > *risk_score)
200            }
201
202            BehaviorCondition::CustomerSegment { segment } => {
203                Ok(self.customer_segments.values().any(|s| s == segment))
204            }
205
206            BehaviorCondition::ErrorRate {
207                endpoint,
208                threshold,
209            } => {
210                let matches = self
211                    .error_rates
212                    .iter()
213                    .any(|(ep, rate)| self.matches_pattern(ep, endpoint) && *rate > *threshold);
214                Ok(matches)
215            }
216
217            BehaviorCondition::Composite {
218                operator,
219                conditions,
220            } => {
221                let results: Result<Vec<bool>> =
222                    conditions.iter().map(|c| self.evaluate(c)).collect();
223                let results = results?;
224
225                match operator {
226                    LogicalOp::And => Ok(results.iter().all(|&r| r)),
227                    LogicalOp::Or => Ok(results.iter().any(|&r| r)),
228                    LogicalOp::Nor => Ok(!results.iter().any(|&r| r)),
229                }
230            }
231        }
232    }
233
234    /// Glob-style pattern matching (supports multiple `*` wildcards)
235    ///
236    /// Each `*` matches zero or more characters greedily. The pattern is split
237    /// on `*` and the resulting literal parts are matched left-to-right.
238    fn matches_pattern(&self, text: &str, pattern: &str) -> bool {
239        if !pattern.contains('*') {
240            return text == pattern;
241        }
242
243        let parts: Vec<&str> = pattern.split('*').collect();
244
245        // First part must be a prefix
246        if !text.starts_with(parts[0]) {
247            return false;
248        }
249
250        // Last part must be a suffix (checked separately to avoid overlap)
251        let last = parts[parts.len() - 1];
252        if !text.ends_with(last) {
253            return false;
254        }
255
256        // Walk through the middle parts in order
257        let mut cursor = parts[0].len();
258        let end = text.len() - last.len();
259
260        for &part in &parts[1..parts.len() - 1] {
261            if part.is_empty() {
262                continue;
263            }
264            match text[cursor..end].find(part) {
265                Some(pos) => cursor += pos + part.len(),
266                None => return false,
267            }
268        }
269
270        cursor <= end
271    }
272}
273
274impl Default for ConditionEvaluator {
275    fn default() -> Self {
276        Self::new()
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_always_condition() {
286        let evaluator = ConditionEvaluator::new();
287        assert!(evaluator.evaluate(&BehaviorCondition::Always).unwrap());
288    }
289
290    #[test]
291    fn test_latency_threshold_condition() {
292        let mut evaluator = ConditionEvaluator::new();
293        evaluator.update_latency("/api/checkout", 500);
294        assert!(evaluator
295            .evaluate(&BehaviorCondition::LatencyThreshold {
296                endpoint: "/api/checkout".to_string(),
297                threshold_ms: 400,
298            })
299            .unwrap());
300    }
301
302    #[test]
303    fn test_load_pressure_condition() {
304        let mut evaluator = ConditionEvaluator::new();
305        evaluator.update_load(150.0);
306        assert!(evaluator
307            .evaluate(&BehaviorCondition::LoadPressure {
308                threshold_rps: 100.0
309            })
310            .unwrap());
311    }
312
313    #[test]
314    fn test_single_wildcard_pattern() {
315        let evaluator = ConditionEvaluator::new();
316        assert!(evaluator.matches_pattern("/api/users", "/api/*"));
317        assert!(evaluator.matches_pattern("/api/users/123", "/api/*"));
318        assert!(!evaluator.matches_pattern("/other/path", "/api/*"));
319    }
320
321    #[test]
322    fn test_multi_wildcard_pattern() {
323        let evaluator = ConditionEvaluator::new();
324        assert!(evaluator.matches_pattern("/api/v1/users/123", "/api/*/users/*"));
325        assert!(evaluator.matches_pattern("/api/v2/users/456", "/api/*/users/*"));
326        assert!(!evaluator.matches_pattern("/api/v1/orders/123", "/api/*/users/*"));
327    }
328
329    #[test]
330    fn test_no_wildcard_pattern() {
331        let evaluator = ConditionEvaluator::new();
332        assert!(evaluator.matches_pattern("/api/users", "/api/users"));
333        assert!(!evaluator.matches_pattern("/api/users/123", "/api/users"));
334    }
335
336    #[test]
337    fn test_wildcard_edge_cases() {
338        let evaluator = ConditionEvaluator::new();
339        // Pattern of just a wildcard matches anything
340        assert!(evaluator.matches_pattern("anything", "*"));
341        assert!(evaluator.matches_pattern("", "*"));
342        // Wildcard at start
343        assert!(evaluator.matches_pattern("/foo/bar", "*/bar"));
344        // Wildcard at end
345        assert!(evaluator.matches_pattern("/foo/bar", "/foo/*"));
346        // Adjacent wildcards
347        assert!(evaluator.matches_pattern("/api/users", "/api/**"));
348        // Empty text with non-trivial pattern
349        assert!(!evaluator.matches_pattern("", "/api/*"));
350    }
351
352    #[test]
353    fn test_pricing_change_above_threshold() {
354        let mut evaluator = ConditionEvaluator::new();
355        evaluator.update_pricing("prod-1", 100.0);
356        evaluator.update_pricing("prod-1", 125.0); // 25% change
357        assert!(evaluator
358            .evaluate(&BehaviorCondition::PricingChange {
359                product_id: "prod-1".to_string(),
360                threshold: 10.0, // 10% threshold
361            })
362            .unwrap());
363    }
364
365    #[test]
366    fn test_pricing_change_below_threshold() {
367        let mut evaluator = ConditionEvaluator::new();
368        evaluator.update_pricing("prod-1", 100.0);
369        evaluator.update_pricing("prod-1", 103.0); // 3% change
370        assert!(!evaluator
371            .evaluate(&BehaviorCondition::PricingChange {
372                product_id: "prod-1".to_string(),
373                threshold: 10.0,
374            })
375            .unwrap());
376    }
377
378    #[test]
379    fn test_pricing_change_no_history() {
380        let mut evaluator = ConditionEvaluator::new();
381        evaluator.update_pricing("prod-1", 100.0); // First price, no previous
382        assert!(!evaluator
383            .evaluate(&BehaviorCondition::PricingChange {
384                product_id: "prod-1".to_string(),
385                threshold: 10.0,
386            })
387            .unwrap());
388    }
389
390    #[test]
391    fn test_pricing_change_zero_price() {
392        let mut evaluator = ConditionEvaluator::new();
393        evaluator.update_pricing("prod-1", 0.0);
394        evaluator.update_pricing("prod-1", 50.0); // Change from zero
395        assert!(evaluator
396            .evaluate(&BehaviorCondition::PricingChange {
397                product_id: "prod-1".to_string(),
398                threshold: 10.0,
399            })
400            .unwrap());
401    }
402
403    #[test]
404    fn test_composite_and_condition() {
405        let mut evaluator = ConditionEvaluator::new();
406        evaluator.update_latency("/api/checkout", 500);
407        evaluator.update_load(150.0);
408
409        let condition = BehaviorCondition::Composite {
410            operator: LogicalOp::And,
411            conditions: vec![
412                BehaviorCondition::LatencyThreshold {
413                    endpoint: "/api/checkout".to_string(),
414                    threshold_ms: 400,
415                },
416                BehaviorCondition::LoadPressure {
417                    threshold_rps: 100.0,
418                },
419            ],
420        };
421
422        assert!(evaluator.evaluate(&condition).unwrap());
423    }
424}