1use std::collections::HashMap;
7
8use crate::error::BuildError;
9use crate::validation::{ContentItem, FieldValue};
10
11pub struct ExpressionEvaluator;
13
14impl ExpressionEvaluator {
15 pub fn evaluate(expression: &str, item: &ContentItem) -> Result<bool, BuildError> {
17 let mut context = HashMap::new();
19
20 context.insert("id".to_string(), FieldValue::String(item.id.clone()));
22 context.insert("title".to_string(), FieldValue::String(item.title.clone()));
23 context.insert(
24 "content".to_string(),
25 FieldValue::String(item.content.clone()),
26 );
27
28 for (key, value) in &item.metadata {
30 context.insert(key.clone(), value.clone());
31 }
32
33 Self::evaluate_expression(expression, &context)
35 }
36
37 fn evaluate_expression(
39 expr: &str,
40 context: &HashMap<String, FieldValue>,
41 ) -> Result<bool, BuildError> {
42 let expr = expr.trim();
43
44 if expr.contains(" or ") {
46 let parts: Vec<&str> = expr.split(" or ").collect();
47 for part in parts {
48 if Self::evaluate_expression(part.trim(), context)? {
49 return Ok(true);
50 }
51 }
52 return Ok(false);
53 }
54
55 if expr.contains(" and ") {
57 let parts: Vec<&str> = expr.split(" and ").collect();
58 for part in parts {
59 if !Self::evaluate_expression(part.trim(), context)? {
60 return Ok(false);
61 }
62 }
63 return Ok(true);
64 }
65
66 if let Some(inner_expr) = expr.strip_prefix("not ") {
68 return Ok(!Self::evaluate_expression(inner_expr, context)?);
69 }
70
71 if expr.contains(" == ") {
73 let parts: Vec<&str> = expr.split(" == ").collect();
74 if parts.len() != 2 {
75 return Err(BuildError::ValidationError(format!(
76 "Invalid comparison: {}",
77 expr
78 )));
79 }
80 let left = Self::get_value(parts[0].trim(), context)?;
81 let right = Self::parse_literal(parts[1].trim())?;
82 return Ok(Self::values_equal(&left, &right));
83 }
84
85 if expr.contains(" != ") {
86 let parts: Vec<&str> = expr.split(" != ").collect();
87 if parts.len() != 2 {
88 return Err(BuildError::ValidationError(format!(
89 "Invalid comparison: {}",
90 expr
91 )));
92 }
93 let left = Self::get_value(parts[0].trim(), context)?;
94 let right = Self::parse_literal(parts[1].trim())?;
95 return Ok(!Self::values_equal(&left, &right));
96 }
97
98 if expr.contains(" in ") {
100 let parts: Vec<&str> = expr.split(" in ").collect();
101 if parts.len() != 2 {
102 return Err(BuildError::ValidationError(format!(
103 "Invalid 'in' expression: {}",
104 expr
105 )));
106 }
107 let left = Self::get_value(parts[0].trim(), context)?;
108 let right_expr = parts[1].trim();
109
110 if right_expr.starts_with('[') && right_expr.ends_with(']') {
112 let list_content = &right_expr[1..right_expr.len() - 1];
113 let items: Vec<&str> = list_content.split(',').map(|s| s.trim()).collect();
114
115 for item in items {
116 let item_value = Self::parse_literal(item)?;
117 if Self::values_equal(&left, &item_value) {
118 return Ok(true);
119 }
120 }
121 return Ok(false);
122 }
123 }
124
125 if let Ok(value) = Self::get_value(expr, context) {
127 return Ok(Self::is_truthy(&value));
128 }
129
130 Err(BuildError::ValidationError(format!(
131 "Could not evaluate expression: {}",
132 expr
133 )))
134 }
135
136 fn get_value(
138 name: &str,
139 context: &HashMap<String, FieldValue>,
140 ) -> Result<FieldValue, BuildError> {
141 context
142 .get(name)
143 .cloned()
144 .ok_or_else(|| BuildError::ValidationError(format!("Unknown variable: {}", name)))
145 }
146
147 fn parse_literal(literal: &str) -> Result<FieldValue, BuildError> {
149 let literal = literal.trim();
150
151 if (literal.starts_with('\'') && literal.ends_with('\''))
153 || (literal.starts_with('"') && literal.ends_with('"'))
154 {
155 let content = &literal[1..literal.len() - 1];
156 return Ok(FieldValue::String(content.to_string()));
157 }
158
159 if literal == "true" {
161 return Ok(FieldValue::Boolean(true));
162 }
163 if literal == "false" {
164 return Ok(FieldValue::Boolean(false));
165 }
166
167 if let Ok(int_val) = literal.parse::<i64>() {
169 return Ok(FieldValue::Integer(int_val));
170 }
171 if let Ok(float_val) = literal.parse::<f64>() {
172 return Ok(FieldValue::Float(float_val));
173 }
174
175 Ok(FieldValue::String(literal.to_string()))
177 }
178
179 fn values_equal(left: &FieldValue, right: &FieldValue) -> bool {
181 match (left, right) {
182 (FieldValue::String(a), FieldValue::String(b)) => a == b,
183 (FieldValue::Integer(a), FieldValue::Integer(b)) => a == b,
184 (FieldValue::Float(a), FieldValue::Float(b)) => (a - b).abs() < f64::EPSILON,
185 (FieldValue::Boolean(a), FieldValue::Boolean(b)) => a == b,
186 (FieldValue::Integer(a), FieldValue::Float(b)) => (*a as f64 - b).abs() < f64::EPSILON,
187 (FieldValue::Float(a), FieldValue::Integer(b)) => (a - *b as f64).abs() < f64::EPSILON,
188 _ => false,
189 }
190 }
191
192 fn is_truthy(value: &FieldValue) -> bool {
194 match value {
195 FieldValue::String(s) => !s.is_empty(),
196 FieldValue::Integer(i) => *i != 0,
197 FieldValue::Float(f) => *f != 0.0,
198 FieldValue::Boolean(b) => *b,
199 FieldValue::Array(arr) => !arr.is_empty(),
200 FieldValue::Object(obj) => !obj.is_empty(),
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::validation::ItemLocation;
209
210 fn create_test_item() -> ContentItem {
211 let mut metadata = HashMap::new();
212 metadata.insert("status".to_string(), FieldValue::String("open".to_string()));
213 metadata.insert(
214 "priority".to_string(),
215 FieldValue::String("high".to_string()),
216 );
217
218 ContentItem {
219 id: "TEST-001".to_string(),
220 title: "Test Item".to_string(),
221 content: "Test content".to_string(),
222 metadata,
223 constraints: vec![],
224 relationships: HashMap::new(),
225 location: ItemLocation {
226 docname: "test.rst".to_string(),
227 lineno: Some(1),
228 source_path: None,
229 },
230 style: None,
231 }
232 }
233
234 #[test]
235 fn test_simple_equality() {
236 let item = create_test_item();
237
238 assert!(ExpressionEvaluator::evaluate("status == 'open'", &item).unwrap());
239 assert!(!ExpressionEvaluator::evaluate("status == 'closed'", &item).unwrap());
240 assert!(ExpressionEvaluator::evaluate("priority == 'high'", &item).unwrap());
241 }
242
243 #[test]
244 fn test_inequality() {
245 let item = create_test_item();
246
247 assert!(!ExpressionEvaluator::evaluate("status != 'open'", &item).unwrap());
248 assert!(ExpressionEvaluator::evaluate("status != 'closed'", &item).unwrap());
249 }
250
251 #[test]
252 fn test_or_logic() {
253 let item = create_test_item();
254
255 assert!(
256 ExpressionEvaluator::evaluate("status == 'open' or status == 'closed'", &item).unwrap()
257 );
258 assert!(
259 ExpressionEvaluator::evaluate("status == 'closed' or priority == 'high'", &item)
260 .unwrap()
261 );
262 assert!(
263 !ExpressionEvaluator::evaluate("status == 'closed' or priority == 'low'", &item)
264 .unwrap()
265 );
266 }
267
268 #[test]
269 fn test_and_logic() {
270 let item = create_test_item();
271
272 assert!(
273 ExpressionEvaluator::evaluate("status == 'open' and priority == 'high'", &item)
274 .unwrap()
275 );
276 assert!(
277 !ExpressionEvaluator::evaluate("status == 'open' and priority == 'low'", &item)
278 .unwrap()
279 );
280 }
281
282 #[test]
283 fn test_in_list() {
284 let item = create_test_item();
285
286 assert!(
287 ExpressionEvaluator::evaluate("priority in ['low', 'medium', 'high']", &item).unwrap()
288 );
289 assert!(!ExpressionEvaluator::evaluate("priority in ['low', 'medium']", &item).unwrap());
290 assert!(ExpressionEvaluator::evaluate("status in ['open', 'closed']", &item).unwrap());
291 }
292
293 #[test]
294 fn test_complex_expression() {
295 let item = create_test_item();
296
297 assert!(!ExpressionEvaluator::evaluate(
299 "priority != 'high' or status == 'complete' or status == 'verified'",
300 &item
301 )
302 .unwrap());
303
304 assert!(ExpressionEvaluator::evaluate(
306 "priority != 'critical' or status == 'complete'",
307 &item
308 )
309 .unwrap());
310 }
311}