Skip to main content

crue_engine/
context.rs

1//! Evaluation Context - Runtime context for rule evaluation
2
3use crate::EvaluationRequest;
4use std::collections::HashMap;
5
6/// Evaluation context - provides field values for rule evaluation
7pub struct EvaluationContext {
8    /// Field values keyed by field path (e.g., "agent.requests_last_hour")
9    fields: HashMap<String, FieldValue>,
10}
11
12impl EvaluationContext {
13    /// Create context from evaluation request
14    pub fn from_request(request: &EvaluationRequest) -> Self {
15        let mut ctx = EvaluationContext {
16            fields: HashMap::new(),
17        };
18
19        // Agent fields
20        ctx.set_field("agent.id", FieldValue::String(request.agent_id.clone()));
21        ctx.set_field("agent.org", FieldValue::String(request.agent_org.clone()));
22        ctx.set_field(
23            "agent.level",
24            FieldValue::String(request.agent_level.clone()),
25        );
26        ctx.set_field(
27            "agent.requests_last_hour",
28            FieldValue::Number(request.requests_last_hour as i64),
29        );
30        ctx.set_field(
31            "agent.requests_last_24h",
32            FieldValue::Number(request.requests_last_24h as i64),
33        );
34
35        // Mission fields
36        if let Some(mission_id) = &request.mission_id {
37            ctx.set_field("mission.id", FieldValue::String(mission_id.clone()));
38        }
39        if let Some(mission_type) = &request.mission_type {
40            ctx.set_field("mission.type", FieldValue::String(mission_type.clone()));
41        }
42
43        // Request fields
44        if let Some(query_type) = &request.query_type {
45            ctx.set_field("request.query_type", FieldValue::String(query_type.clone()));
46        }
47        if let Some(justification) = &request.justification {
48            ctx.set_field(
49                "request.justification",
50                FieldValue::String(justification.clone()),
51            );
52            ctx.set_field(
53                "request.justification_length",
54                FieldValue::Number(justification.len() as i64),
55            );
56        }
57        if let Some(export_format) = &request.export_format {
58            ctx.set_field(
59                "request.export_format",
60                FieldValue::String(export_format.clone()),
61            );
62        }
63        if let Some(limit) = request.result_limit {
64            ctx.set_field("request.result_limit", FieldValue::Number(limit as i64));
65        }
66
67        // Results
68        ctx.set_field(
69            "request.results_last_query",
70            FieldValue::Number(request.results_last_query as i64),
71        );
72
73        // Geographic
74        if let Some(dept) = &request.account_department {
75            ctx.set_field("account.department", FieldValue::String(dept.clone()));
76        }
77
78        // Allowed departments
79        for (i, dept) in request.allowed_departments.iter().enumerate() {
80            ctx.set_field(
81                &format!("agent.allowed_departments[{}]", i),
82                FieldValue::Number(*dept as i64),
83            );
84        }
85
86        // Time
87        ctx.set_field(
88            "context.request_hour",
89            FieldValue::Number(request.request_hour as i64),
90        );
91        ctx.set_field(
92            "context.is_within_mission_hours",
93            FieldValue::Boolean(request.is_within_mission_hours),
94        );
95
96        ctx
97    }
98
99    /// Get field value
100    pub fn get_field(&self, path: &str) -> Option<&FieldValue> {
101        self.fields.get(path)
102    }
103
104    /// Set field value
105    pub fn set_field(&mut self, path: &str, value: FieldValue) {
106        self.fields.insert(path.to_string(), value);
107    }
108
109    /// Get all fields
110    pub fn fields(&self) -> &HashMap<String, FieldValue> {
111        &self.fields
112    }
113}
114
115/// Field value types
116#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
117pub enum FieldValue {
118    Number(i64),
119    Float(f64),
120    String(String),
121    Boolean(bool),
122}
123
124impl FieldValue {
125    /// Compare using operator
126    pub fn compare(&self, op: &str, other: &FieldValue) -> bool {
127        match (self, other) {
128            (FieldValue::Number(a), FieldValue::Number(b)) => match op {
129                ">" => a > b,
130                "<" => a < b,
131                ">=" => a >= b,
132                "<=" => a <= b,
133                "==" => a == b,
134                "!=" => a != b,
135                _ => false,
136            },
137            (FieldValue::String(a), FieldValue::String(b)) => match op {
138                "==" => a == b,
139                "!=" => a != b,
140                _ => false,
141            },
142            (FieldValue::Boolean(a), FieldValue::Boolean(b)) => match op {
143                "==" => a == b,
144                "!=" => a != b,
145                _ => false,
146            },
147            _ => false,
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_context_creation() {
158        let request = EvaluationRequest {
159            request_id: "test".to_string(),
160            agent_id: "AGENT_001".to_string(),
161            agent_org: "DGFiP".to_string(),
162            agent_level: "standard".to_string(),
163            mission_id: Some("MIS_001".to_string()),
164            mission_type: None,
165            query_type: None,
166            justification: None,
167            export_format: None,
168            result_limit: None,
169            requests_last_hour: 10,
170            requests_last_24h: 100,
171            results_last_query: 5,
172            account_department: None,
173            allowed_departments: vec![],
174            request_hour: 14,
175            is_within_mission_hours: true,
176        };
177
178        let ctx = EvaluationContext::from_request(&request);
179
180        assert_eq!(
181            ctx.get_field("agent.id"),
182            Some(&FieldValue::String("AGENT_001".to_string()))
183        );
184        assert_eq!(
185            ctx.get_field("agent.requests_last_hour"),
186            Some(&FieldValue::Number(10))
187        );
188    }
189
190    #[test]
191    fn test_field_comparison() {
192        assert!(FieldValue::Number(10).compare(">", &FieldValue::Number(5)));
193        assert!(FieldValue::Number(10).compare(">=", &FieldValue::Number(10)));
194        assert!(FieldValue::String("test".to_string())
195            .compare("==", &FieldValue::String("test".to_string())));
196    }
197}