use crate::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum LogicalOp {
And,
Or,
Nor,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum BehaviorCondition {
Always,
LatencyThreshold {
endpoint: String,
threshold_ms: u64,
},
LoadPressure {
threshold_rps: f64,
},
PricingChange {
product_id: String,
threshold: f64,
},
FraudSuspicion {
user_id: String,
risk_score: f64,
},
CustomerSegment {
segment: String,
},
ErrorRate {
endpoint: String,
threshold: f64,
},
Composite {
operator: LogicalOp,
conditions: Vec<BehaviorCondition>,
},
}
pub struct ConditionEvaluator {
latency_metrics: HashMap<String, u64>,
load_rps: f64,
error_rates: HashMap<String, f64>,
pricing_data: HashMap<String, f64>,
previous_pricing_data: HashMap<String, f64>,
fraud_scores: HashMap<String, f64>,
customer_segments: HashMap<String, String>,
}
impl ConditionEvaluator {
pub fn new() -> Self {
Self {
latency_metrics: HashMap::new(),
load_rps: 0.0,
error_rates: HashMap::new(),
pricing_data: HashMap::new(),
previous_pricing_data: HashMap::new(),
fraud_scores: HashMap::new(),
customer_segments: HashMap::new(),
}
}
pub fn update_latency(&mut self, endpoint: &str, latency_ms: u64) {
self.latency_metrics.insert(endpoint.to_string(), latency_ms);
}
pub fn update_load(&mut self, rps: f64) {
self.load_rps = rps;
}
pub fn update_error_rate(&mut self, endpoint: &str, error_rate: f64) {
self.error_rates.insert(endpoint.to_string(), error_rate);
}
pub fn update_pricing(&mut self, product_id: &str, price: f64) {
if let Some(old_price) = self.pricing_data.get(product_id) {
self.previous_pricing_data.insert(product_id.to_string(), *old_price);
}
self.pricing_data.insert(product_id.to_string(), price);
}
pub fn update_fraud_score(&mut self, user_id: &str, risk_score: f64) {
self.fraud_scores.insert(user_id.to_string(), risk_score);
}
pub fn update_customer_segment(&mut self, user_id: &str, segment: String) {
self.customer_segments.insert(user_id.to_string(), segment);
}
pub fn evaluate(&self, condition: &BehaviorCondition) -> Result<bool> {
match condition {
BehaviorCondition::Always => Ok(true),
BehaviorCondition::LatencyThreshold {
endpoint,
threshold_ms,
} => {
let matches = self.latency_metrics.iter().any(|(ep, latency)| {
self.matches_pattern(ep, endpoint) && *latency > *threshold_ms
});
Ok(matches)
}
BehaviorCondition::LoadPressure { threshold_rps } => Ok(self.load_rps > *threshold_rps),
BehaviorCondition::PricingChange {
product_id,
threshold,
} => {
let current = match self.pricing_data.get(product_id) {
Some(price) => *price,
None => return Ok(false),
};
let previous = match self.previous_pricing_data.get(product_id) {
Some(price) => *price,
None => return Ok(false), };
if previous == 0.0 {
return Ok(current != 0.0);
}
let pct_change = ((current - previous) / previous).abs() * 100.0;
Ok(pct_change > *threshold)
}
BehaviorCondition::FraudSuspicion {
user_id,
risk_score,
} => {
let score = self.fraud_scores.get(user_id).copied().unwrap_or(0.0);
Ok(score > *risk_score)
}
BehaviorCondition::CustomerSegment { segment } => {
Ok(self.customer_segments.values().any(|s| s == segment))
}
BehaviorCondition::ErrorRate {
endpoint,
threshold,
} => {
let matches = self
.error_rates
.iter()
.any(|(ep, rate)| self.matches_pattern(ep, endpoint) && *rate > *threshold);
Ok(matches)
}
BehaviorCondition::Composite {
operator,
conditions,
} => {
let results: Result<Vec<bool>> =
conditions.iter().map(|c| self.evaluate(c)).collect();
let results = results?;
match operator {
LogicalOp::And => Ok(results.iter().all(|&r| r)),
LogicalOp::Or => Ok(results.iter().any(|&r| r)),
LogicalOp::Nor => Ok(!results.iter().any(|&r| r)),
}
}
}
}
fn matches_pattern(&self, text: &str, pattern: &str) -> bool {
if !pattern.contains('*') {
return text == pattern;
}
let parts: Vec<&str> = pattern.split('*').collect();
if !text.starts_with(parts[0]) {
return false;
}
let last = parts[parts.len() - 1];
if !text.ends_with(last) {
return false;
}
let mut cursor = parts[0].len();
let end = text.len() - last.len();
for &part in &parts[1..parts.len() - 1] {
if part.is_empty() {
continue;
}
match text[cursor..end].find(part) {
Some(pos) => cursor += pos + part.len(),
None => return false,
}
}
cursor <= end
}
}
impl Default for ConditionEvaluator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_always_condition() {
let evaluator = ConditionEvaluator::new();
assert!(evaluator.evaluate(&BehaviorCondition::Always).unwrap());
}
#[test]
fn test_latency_threshold_condition() {
let mut evaluator = ConditionEvaluator::new();
evaluator.update_latency("/api/checkout", 500);
assert!(evaluator
.evaluate(&BehaviorCondition::LatencyThreshold {
endpoint: "/api/checkout".to_string(),
threshold_ms: 400,
})
.unwrap());
}
#[test]
fn test_load_pressure_condition() {
let mut evaluator = ConditionEvaluator::new();
evaluator.update_load(150.0);
assert!(evaluator
.evaluate(&BehaviorCondition::LoadPressure {
threshold_rps: 100.0
})
.unwrap());
}
#[test]
fn test_single_wildcard_pattern() {
let evaluator = ConditionEvaluator::new();
assert!(evaluator.matches_pattern("/api/users", "/api/*"));
assert!(evaluator.matches_pattern("/api/users/123", "/api/*"));
assert!(!evaluator.matches_pattern("/other/path", "/api/*"));
}
#[test]
fn test_multi_wildcard_pattern() {
let evaluator = ConditionEvaluator::new();
assert!(evaluator.matches_pattern("/api/v1/users/123", "/api/*/users/*"));
assert!(evaluator.matches_pattern("/api/v2/users/456", "/api/*/users/*"));
assert!(!evaluator.matches_pattern("/api/v1/orders/123", "/api/*/users/*"));
}
#[test]
fn test_no_wildcard_pattern() {
let evaluator = ConditionEvaluator::new();
assert!(evaluator.matches_pattern("/api/users", "/api/users"));
assert!(!evaluator.matches_pattern("/api/users/123", "/api/users"));
}
#[test]
fn test_wildcard_edge_cases() {
let evaluator = ConditionEvaluator::new();
assert!(evaluator.matches_pattern("anything", "*"));
assert!(evaluator.matches_pattern("", "*"));
assert!(evaluator.matches_pattern("/foo/bar", "*/bar"));
assert!(evaluator.matches_pattern("/foo/bar", "/foo/*"));
assert!(evaluator.matches_pattern("/api/users", "/api/**"));
assert!(!evaluator.matches_pattern("", "/api/*"));
}
#[test]
fn test_pricing_change_above_threshold() {
let mut evaluator = ConditionEvaluator::new();
evaluator.update_pricing("prod-1", 100.0);
evaluator.update_pricing("prod-1", 125.0); assert!(evaluator
.evaluate(&BehaviorCondition::PricingChange {
product_id: "prod-1".to_string(),
threshold: 10.0, })
.unwrap());
}
#[test]
fn test_pricing_change_below_threshold() {
let mut evaluator = ConditionEvaluator::new();
evaluator.update_pricing("prod-1", 100.0);
evaluator.update_pricing("prod-1", 103.0); assert!(!evaluator
.evaluate(&BehaviorCondition::PricingChange {
product_id: "prod-1".to_string(),
threshold: 10.0,
})
.unwrap());
}
#[test]
fn test_pricing_change_no_history() {
let mut evaluator = ConditionEvaluator::new();
evaluator.update_pricing("prod-1", 100.0); assert!(!evaluator
.evaluate(&BehaviorCondition::PricingChange {
product_id: "prod-1".to_string(),
threshold: 10.0,
})
.unwrap());
}
#[test]
fn test_pricing_change_zero_price() {
let mut evaluator = ConditionEvaluator::new();
evaluator.update_pricing("prod-1", 0.0);
evaluator.update_pricing("prod-1", 50.0); assert!(evaluator
.evaluate(&BehaviorCondition::PricingChange {
product_id: "prod-1".to_string(),
threshold: 10.0,
})
.unwrap());
}
#[test]
fn test_composite_and_condition() {
let mut evaluator = ConditionEvaluator::new();
evaluator.update_latency("/api/checkout", 500);
evaluator.update_load(150.0);
let condition = BehaviorCondition::Composite {
operator: LogicalOp::And,
conditions: vec![
BehaviorCondition::LatencyThreshold {
endpoint: "/api/checkout".to_string(),
threshold_ms: 400,
},
BehaviorCondition::LoadPressure {
threshold_rps: 100.0,
},
],
};
assert!(evaluator.evaluate(&condition).unwrap());
}
}