pcm_engine/
rules.rs

1//! Rule engine for catalog management
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6/// Catalog rule
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct CatalogRule {
9    pub id: Uuid,
10    pub name: String,
11    pub rule_type: RuleType,
12    pub conditions: Vec<RuleCondition>,
13    pub logical_operator: LogicalOperator, // How to combine conditions
14    pub actions: Vec<RuleAction>,
15    pub priority: u32,
16    pub is_active: bool,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub parent_rule_id: Option<Uuid>, // For rule chaining
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub valid_for: Option<TimePeriod>,
21}
22
23/// Time period for rule validity
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TimePeriod {
26    pub start: chrono::DateTime<chrono::Utc>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub end: Option<chrono::DateTime<chrono::Utc>>,
29}
30
31/// Rule type
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
34pub enum RuleType {
35    Validation,
36    Transformation,
37    Pricing,
38    Eligibility,
39}
40
41/// Rule condition
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct RuleCondition {
44    pub field: String,
45    pub operator: RuleOperator,
46    pub value: String,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub value2: Option<String>, // For BETWEEN operator
49}
50
51/// Logical operator for combining conditions
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
54pub enum LogicalOperator {
55    And,
56    Or,
57    Not,
58}
59
60/// Rule operator
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
63pub enum RuleOperator {
64    Equals,
65    NotEquals,
66    GreaterThan,
67    LessThan,
68    GreaterThanOrEqual,
69    LessThanOrEqual,
70    Contains,
71    NotContains,
72    StartsWith,
73    EndsWith,
74    Regex,
75    In,
76    NotIn,
77    Between,
78    IsNull,
79    IsNotNull,
80}
81
82/// Rule action
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct RuleAction {
85    pub action_type: ActionType,
86    pub target: String,
87    pub value: String,
88}
89
90/// Action type
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
92#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
93pub enum ActionType {
94    Set,
95    Add,
96    Remove,
97    Validate,
98    Transform,
99}
100
101/// Evaluate a rule
102pub fn evaluate_rule(rule: &CatalogRule, context: &RuleContext) -> RuleResult {
103    if !rule.is_active {
104        return RuleResult::Skipped;
105    }
106
107    // Check time validity
108    if let Some(ref period) = rule.valid_for {
109        let now = chrono::Utc::now();
110        if now < period.start {
111            return RuleResult::NotMatched; // Rule not yet active
112        }
113        if let Some(end) = period.end {
114            if now > end {
115                return RuleResult::NotMatched; // Rule expired
116            }
117        }
118    }
119
120    // Evaluate conditions based on logical operator
121    let conditions_met = match rule.logical_operator {
122        LogicalOperator::And => rule
123            .conditions
124            .iter()
125            .all(|condition| evaluate_condition(condition, context)),
126        LogicalOperator::Or => rule
127            .conditions
128            .iter()
129            .any(|condition| evaluate_condition(condition, context)),
130        LogicalOperator::Not => !rule
131            .conditions
132            .iter()
133            .all(|condition| evaluate_condition(condition, context)),
134    };
135
136    if conditions_met {
137        RuleResult::Matched {
138            actions: rule.actions.clone(),
139        }
140    } else {
141        RuleResult::NotMatched
142    }
143}
144
145/// Rule context
146#[derive(Debug, Clone)]
147pub struct RuleContext {
148    pub data: std::collections::HashMap<String, String>,
149}
150
151/// Rule evaluation result
152#[derive(Debug, Clone)]
153pub enum RuleResult {
154    Matched { actions: Vec<RuleAction> },
155    NotMatched,
156    Skipped,
157}
158
159fn evaluate_condition(condition: &RuleCondition, context: &RuleContext) -> bool {
160    let field_value = context.data.get(&condition.field).cloned();
161
162    match condition.operator {
163        RuleOperator::IsNull => field_value.is_none(),
164        RuleOperator::IsNotNull => field_value.is_some(),
165        _ => {
166            let field_value = field_value.unwrap_or_default();
167            match condition.operator {
168                RuleOperator::Equals => field_value == condition.value,
169                RuleOperator::NotEquals => field_value != condition.value,
170                RuleOperator::GreaterThan => {
171                    if let (Ok(field_num), Ok(cond_num)) =
172                        (field_value.parse::<f64>(), condition.value.parse::<f64>())
173                    {
174                        field_num > cond_num
175                    } else {
176                        false
177                    }
178                }
179                RuleOperator::LessThan => {
180                    if let (Ok(field_num), Ok(cond_num)) =
181                        (field_value.parse::<f64>(), condition.value.parse::<f64>())
182                    {
183                        field_num < cond_num
184                    } else {
185                        false
186                    }
187                }
188                RuleOperator::GreaterThanOrEqual => {
189                    if let (Ok(field_num), Ok(cond_num)) =
190                        (field_value.parse::<f64>(), condition.value.parse::<f64>())
191                    {
192                        field_num >= cond_num
193                    } else {
194                        false
195                    }
196                }
197                RuleOperator::LessThanOrEqual => {
198                    if let (Ok(field_num), Ok(cond_num)) =
199                        (field_value.parse::<f64>(), condition.value.parse::<f64>())
200                    {
201                        field_num <= cond_num
202                    } else {
203                        false
204                    }
205                }
206                RuleOperator::Contains => field_value.contains(&condition.value),
207                RuleOperator::NotContains => !field_value.contains(&condition.value),
208                RuleOperator::StartsWith => field_value.starts_with(&condition.value),
209                RuleOperator::EndsWith => field_value.ends_with(&condition.value),
210                RuleOperator::Regex => regex::Regex::new(&condition.value)
211                    .map(|re| re.is_match(&field_value))
212                    .unwrap_or(false),
213                RuleOperator::In => {
214                    let values: Vec<&str> = condition.value.split(',').collect();
215                    values.iter().any(|v| v.trim() == field_value)
216                }
217                RuleOperator::NotIn => {
218                    let values: Vec<&str> = condition.value.split(',').collect();
219                    !values.iter().any(|v| v.trim() == field_value)
220                }
221                RuleOperator::Between => {
222                    if let Some(ref value2_str) = condition.value2 {
223                        if let (Ok(field_num), Ok(min), Ok(max)) = (
224                            field_value.parse::<f64>(),
225                            condition.value.parse::<f64>(),
226                            value2_str.parse::<f64>(),
227                        ) {
228                            field_num >= min && field_num <= max
229                        } else {
230                            false
231                        }
232                    } else {
233                        false
234                    }
235                }
236                _ => false,
237            }
238        }
239    }
240}