sphinx_ultra/validation/
expression_evaluator.rs

1//! Simple expression evaluator for constraint validation
2//!
3//! This module provides a basic expression evaluator that can handle
4//! constraint expressions similar to those used in sphinx-needs.
5
6use std::collections::HashMap;
7
8use crate::error::BuildError;
9use crate::validation::{ContentItem, FieldValue};
10
11/// Simple expression evaluator for constraint validation
12pub struct ExpressionEvaluator;
13
14impl ExpressionEvaluator {
15    /// Evaluate a constraint expression against a content item
16    pub fn evaluate(expression: &str, item: &ContentItem) -> Result<bool, BuildError> {
17        // Create context from the item
18        let mut context = HashMap::new();
19
20        // Add basic fields
21        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        // Add metadata fields
29        for (key, value) in &item.metadata {
30            context.insert(key.clone(), value.clone());
31        }
32
33        // Simple expression parser - handles basic comparisons and logic
34        Self::evaluate_expression(expression, &context)
35    }
36
37    /// Parse and evaluate a simple boolean expression
38    fn evaluate_expression(
39        expr: &str,
40        context: &HashMap<String, FieldValue>,
41    ) -> Result<bool, BuildError> {
42        let expr = expr.trim();
43
44        // Handle OR operations
45        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        // Handle AND operations
56        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        // Handle NOT operations
67        if let Some(inner_expr) = expr.strip_prefix("not ") {
68            return Ok(!Self::evaluate_expression(inner_expr, context)?);
69        }
70
71        // Handle comparisons
72        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        // Handle 'in' operations
99        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            // Parse list syntax [item1, item2, ...]
111            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        // Handle simple variable access (return truthy value)
126        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    /// Get a value from the context
137    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    /// Parse a literal value (string, number, boolean)
148    fn parse_literal(literal: &str) -> Result<FieldValue, BuildError> {
149        let literal = literal.trim();
150
151        // String literal
152        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        // Boolean literal
160        if literal == "true" {
161            return Ok(FieldValue::Boolean(true));
162        }
163        if literal == "false" {
164            return Ok(FieldValue::Boolean(false));
165        }
166
167        // Number literal
168        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        // Default to string
176        Ok(FieldValue::String(literal.to_string()))
177    }
178
179    /// Check if two field values are equal
180    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    /// Check if a value is truthy
193    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        // This should fail because priority is 'high' but status is not 'complete' or 'verified'
298        assert!(!ExpressionEvaluator::evaluate(
299            "priority != 'high' or status == 'complete' or status == 'verified'",
300            &item
301        )
302        .unwrap());
303
304        // This should pass because priority is not 'critical'
305        assert!(ExpressionEvaluator::evaluate(
306            "priority != 'critical' or status == 'complete'",
307            &item
308        )
309        .unwrap());
310    }
311}