Skip to main content

crue_engine/
engine.rs

1//! CRUE Engine Core
2
3use crate::{EvaluationRequest, EvaluationResult, decision::Decision};
4use crate::context::EvaluationContext;
5use crate::rules::RuleRegistry;
6use std::time::Instant;
7use tracing::{info, warn, error};
8
9/// CRUE Engine - main rule evaluation engine
10pub struct CrueEngine {
11    rule_registry: RuleRegistry,
12    strict_mode: bool,
13}
14
15impl CrueEngine {
16    /// Create new CRUE engine
17    pub fn new() -> Self {
18        CrueEngine {
19            rule_registry: RuleRegistry::new(),
20            strict_mode: true,
21        }
22    }
23    
24    /// Load rules from registry
25    pub fn load_rules(&mut self, registry: RuleRegistry) {
26        self.rule_registry = registry;
27    }
28    
29    /// Evaluate request against all rules
30    pub fn evaluate(&self, request: &EvaluationRequest) -> EvaluationResult {
31        let start = Instant::now();
32        
33        info!("Evaluating request: {}", request.request_id);
34        
35        // Create evaluation context
36        let ctx = EvaluationContext::from_request(request);
37        
38        // Get active rules
39        let rules = self.rule_registry.get_active_rules();
40        
41        // Evaluate each rule in order (first-match-wins)
42        for rule in rules {
43            // Check rule validity period
44            if !rule.is_valid_now() {
45                continue;
46            }
47            
48            // Evaluate conditions
49            match rule.evaluate(&ctx) {
50                Ok(true) => {
51                    // Rule matched - apply action
52                    let result = rule.apply_action(&ctx);
53                    
54                    let evaluation_time = start.elapsed().as_millis() as u64;
55                    
56                    info!(
57                        "Request {}: {} by rule {} ({}ms)",
58                        request.request_id,
59                        format!("{:?}", result.decision),
60                        rule.id,
61                        evaluation_time
62                    );
63                    
64                    return EvaluationResult {
65                        request_id: request.request_id.clone(),
66                        decision: result.decision,
67                        error_code: result.error_code,
68                        message: result.message,
69                        rule_id: Some(rule.id.clone()),
70                        rule_version: Some(rule.version.clone()),
71                        evaluated_at: chrono::Utc::now().to_rfc3339(),
72                        evaluation_time_ms: evaluation_time,
73                    };
74                }
75                Ok(false) => {
76                    // Rule didn't match, continue to next
77                }
78                Err(e) => {
79                    // Error evaluating rule
80                    if self.strict_mode {
81                        error!("Error evaluating rule {}: {}", rule.id, e);
82                        // In strict mode, block on error
83                        return EvaluationResult {
84                            request_id: request.request_id.clone(),
85                            decision: Decision::Block,
86                            error_code: Some("ENGINE_ERROR".to_string()),
87                            message: Some(format!("Rule evaluation error: {}", e)),
88                            rule_id: Some(rule.id.clone()),
89                            rule_version: Some(rule.version.clone()),
90                            evaluated_at: chrono::Utc::now().to_rfc3339(),
91                            evaluation_time_ms: start.elapsed().as_millis() as u64,
92                        };
93                    } else {
94                        warn!("Non-strict mode: continuing after error in rule {}", rule.id);
95                    }
96                }
97            }
98        }
99        
100        // No rules matched - allow by default
101        EvaluationResult {
102            request_id: request.request_id.clone(),
103            decision: Decision::Allow,
104            evaluated_at: chrono::Utc::now().to_rfc3339(),
105            evaluation_time_ms: start.elapsed().as_millis() as u64,
106            ..Default::default()
107        }
108    }
109    
110    /// Set strict mode
111    pub fn set_strict_mode(&mut self, strict: bool) {
112        self.strict_mode = strict;
113    }
114    
115    /// Get rule count
116    pub fn rule_count(&self) -> usize {
117        self.rule_registry.len()
118    }
119}
120
121impl Default for CrueEngine {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    
131    #[test]
132    fn test_engine_default() {
133        let engine = CrueEngine::new();
134        assert_eq!(engine.rule_count(), 0);
135    }
136    
137    #[test]
138    fn test_evaluate_default_allow() {
139        let engine = CrueEngine::new();
140        
141        let request = EvaluationRequest {
142            request_id: "test_001".to_string(),
143            agent_id: "AGENT_001".to_string(),
144            agent_org: "DGFiP".to_string(),
145            agent_level: "standard".to_string(),
146            mission_id: None,
147            mission_type: None,
148            query_type: None,
149            justification: None,
150            export_format: None,
151            result_limit: None,
152            requests_last_hour: 0,
153            requests_last_24h: 0,
154            results_last_query: 0,
155            account_department: None,
156            allowed_departments: vec![],
157            request_hour: 12,
158            is_within_mission_hours: true,
159        };
160        
161        let result = engine.evaluate(&request);
162        assert_eq!(result.decision, Decision::Allow);
163    }
164}