mockforge_intelligence/behavioral_economics/
conditions.rs1use mockforge_foundation::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[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 And,
18 Or,
20 Nor,
22}
23
24#[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,
34
35 LatencyThreshold {
37 endpoint: String,
39 threshold_ms: u64,
41 },
42
43 LoadPressure {
45 threshold_rps: f64,
47 },
48
49 PricingChange {
51 product_id: String,
53 threshold: f64,
55 },
56
57 FraudSuspicion {
59 user_id: String,
61 risk_score: f64,
63 },
64
65 CustomerSegment {
67 segment: String,
69 },
70
71 ErrorRate {
73 endpoint: String,
75 threshold: f64,
77 },
78
79 Composite {
81 operator: LogicalOp,
83 conditions: Vec<BehaviorCondition>,
85 },
86}
87
88pub struct ConditionEvaluator {
92 latency_metrics: HashMap<String, u64>,
94 load_rps: f64,
96 error_rates: HashMap<String, f64>,
98 pricing_data: HashMap<String, f64>,
100 previous_pricing_data: HashMap<String, f64>,
102 fraud_scores: HashMap<String, f64>,
104 customer_segments: HashMap<String, String>,
106}
107
108impl ConditionEvaluator {
109 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 pub fn update_latency(&mut self, endpoint: &str, latency_ms: u64) {
124 self.latency_metrics.insert(endpoint.to_string(), latency_ms);
125 }
126
127 pub fn update_load(&mut self, rps: f64) {
129 self.load_rps = rps;
130 }
131
132 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 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 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 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 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 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 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), };
186 if previous == 0.0 {
187 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 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 if !text.starts_with(parts[0]) {
247 return false;
248 }
249
250 let last = parts[parts.len() - 1];
252 if !text.ends_with(last) {
253 return false;
254 }
255
256 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 assert!(evaluator.matches_pattern("anything", "*"));
341 assert!(evaluator.matches_pattern("", "*"));
342 assert!(evaluator.matches_pattern("/foo/bar", "*/bar"));
344 assert!(evaluator.matches_pattern("/foo/bar", "/foo/*"));
346 assert!(evaluator.matches_pattern("/api/users", "/api/**"));
348 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); assert!(evaluator
358 .evaluate(&BehaviorCondition::PricingChange {
359 product_id: "prod-1".to_string(),
360 threshold: 10.0, })
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); 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); 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); 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}