1use crate::{EvaluationRequest, EvaluationResult, decision::Decision};
4use crate::context::EvaluationContext;
5use crate::rules::RuleRegistry;
6use std::time::Instant;
7use tracing::{info, warn, error};
8
9pub struct CrueEngine {
11 rule_registry: RuleRegistry,
12 strict_mode: bool,
13}
14
15impl CrueEngine {
16 pub fn new() -> Self {
18 CrueEngine {
19 rule_registry: RuleRegistry::new(),
20 strict_mode: true,
21 }
22 }
23
24 pub fn load_rules(&mut self, registry: RuleRegistry) {
26 self.rule_registry = registry;
27 }
28
29 pub fn evaluate(&self, request: &EvaluationRequest) -> EvaluationResult {
31 let start = Instant::now();
32
33 info!("Evaluating request: {}", request.request_id);
34
35 let ctx = EvaluationContext::from_request(request);
37
38 let rules = self.rule_registry.get_active_rules();
40
41 for rule in rules {
43 if !rule.is_valid_now() {
45 continue;
46 }
47
48 match rule.evaluate(&ctx) {
50 Ok(true) => {
51 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 }
78 Err(e) => {
79 if self.strict_mode {
81 error!("Error evaluating rule {}: {}", rule.id, e);
82 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 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 pub fn set_strict_mode(&mut self, strict: bool) {
112 self.strict_mode = strict;
113 }
114
115 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}