1use std::collections::HashMap;
7
8use minijinja::{Environment, Template};
9
10use crate::error::BuildError;
11use crate::validation::expression_evaluator::ExpressionEvaluator;
12use crate::validation::{
13 ActionResult, ConstraintActions, ConstraintValidator, ContentItem, FailureAction,
14 ValidationContext, ValidationFailure, ValidationResult, ValidationRule, ValidationSeverity,
15 Validator,
16};
17
18pub struct ConstraintEngine {
20 template_env: Environment<'static>,
22 template_cache: HashMap<String, Template<'static, 'static>>,
24}
25
26impl ConstraintEngine {
27 pub fn new() -> Self {
29 let mut env = Environment::new();
30
31 env.add_function("has_tag", |tags: Vec<String>, tag: String| -> bool {
33 tags.contains(&tag)
34 });
35
36 env.add_function("in_list", |value: String, list: Vec<String>| -> bool {
37 list.contains(&value)
38 });
39
40 env.add_function("not_empty", |value: String| -> bool { !value.is_empty() });
41
42 Self {
43 template_env: env,
44 template_cache: HashMap::new(),
45 }
46 }
47
48 pub fn process_constraints(
50 &mut self,
51 item: &ContentItem,
52 context: &ValidationContext,
53 ) -> Result<(ContentItem, Vec<ValidationFailure>), BuildError> {
54 let mut modified_item = item.clone();
55 let mut failures = Vec::new();
56
57 for constraint_name in &modified_item.constraints.clone() {
58 if let Some(constraint_def) = context.config.constraints.get(constraint_name) {
59 for (check_name, expression) in &constraint_def.checks {
61 let rule = ValidationRule {
62 name: format!("{}::{}", constraint_name, check_name),
63 description: constraint_def.description.clone(),
64 constraint: expression.clone(),
65 severity: constraint_def.severity,
66 actions: context
67 .config
68 .constraint_failed_options
69 .get(&constraint_def.severity.to_string())
70 .cloned()
71 .unwrap_or_default(),
72 error_template: constraint_def.error_message.clone(),
73 };
74
75 let result = self.validate_constraint(&rule, &modified_item)?;
76
77 if !result.passed {
78 let failure =
79 ValidationFailure::new(rule, result, modified_item.id.clone());
80 failures.push(failure);
81 }
82 }
83 }
84 }
85
86 if !failures.is_empty() {
88 self.apply_failure_actions(&mut modified_item, &failures)?;
89 }
90
91 Ok((modified_item, failures))
92 }
93
94 pub fn process_constraints_mut(
96 &mut self,
97 item: &mut ContentItem,
98 context: &ValidationContext,
99 ) -> Result<Vec<ValidationFailure>, BuildError> {
100 let mut failures = Vec::new();
101
102 for constraint_name in &item.constraints.clone() {
103 if let Some(constraint_def) = context.config.constraints.get(constraint_name) {
104 for (check_name, expression) in &constraint_def.checks {
106 let rule = ValidationRule {
107 name: format!("{}::{}", constraint_name, check_name),
108 description: constraint_def.description.clone(),
109 constraint: expression.clone(),
110 severity: constraint_def.severity,
111 actions: context
112 .config
113 .constraint_failed_options
114 .get(&constraint_def.severity.to_string())
115 .cloned()
116 .unwrap_or_default(),
117 error_template: constraint_def.error_message.clone(),
118 };
119
120 let result = self.validate_constraint(&rule, item)?;
121
122 if !result.passed {
123 let failure = ValidationFailure::new(rule, result, item.id.clone());
124 failures.push(failure);
125 }
126 }
127 }
128 }
129
130 if !failures.is_empty() {
132 self.apply_failure_actions(item, &failures)?;
133 }
134
135 Ok(failures)
136 }
137
138 pub fn validate_constraint(
140 &mut self,
141 rule: &ValidationRule,
142 item: &ContentItem,
143 ) -> Result<ValidationResult, BuildError> {
144 match ExpressionEvaluator::evaluate(&rule.constraint, item) {
146 Ok(passed) => {
147 if passed {
148 Ok(ValidationResult::success())
149 } else {
150 let error_message = self.generate_error_message(rule, item)?;
151 Ok(ValidationResult::failure(error_message))
152 }
153 }
154 Err(e) => Err(BuildError::ValidationError(format!(
155 "Failed to evaluate constraint '{}': {}",
156 rule.constraint, e
157 ))),
158 }
159 }
160
161 fn apply_failure_actions(
163 &self,
164 item: &mut ContentItem,
165 failures: &[ValidationFailure],
166 ) -> Result<(), BuildError> {
167 for failure in failures {
168 let actions = &failure.rule.actions;
169
170 for action in &actions.on_fail {
172 match action {
173 FailureAction::Warn => {
174 log::warn!(
175 "Constraint validation failed for item '{}': {} (rule: {})",
176 item.id,
177 failure
178 .result
179 .error_message
180 .as_deref()
181 .unwrap_or("Unknown error"),
182 failure.rule.name
183 );
184 }
185 FailureAction::Break => {
186 return Err(BuildError::ValidationError(format!(
187 "Critical constraint validation failed for item '{}': {} (rule: {})",
188 item.id,
189 failure
190 .result
191 .error_message
192 .as_deref()
193 .unwrap_or("Unknown error"),
194 failure.rule.name
195 )));
196 }
197 FailureAction::Style => {
198 }
200 }
201 }
202
203 if !actions.style_changes.is_empty() || actions.on_fail.contains(&FailureAction::Style)
205 {
206 self.apply_style_changes(item, actions);
207 }
208 }
209
210 Ok(())
211 }
212
213 fn apply_style_changes(&self, item: &mut ContentItem, actions: &ConstraintActions) {
215 let new_styles = actions.style_changes.join(", ");
216
217 if actions.force_style || item.style.is_none() {
218 item.style = Some(new_styles);
219 } else if let Some(existing_style) = &item.style {
220 if !new_styles.is_empty() {
221 item.style = Some(format!("{}, {}", existing_style, new_styles));
222 }
223 }
224 }
225
226 #[allow(mismatched_lifetime_syntaxes)]
228 fn get_or_compile_template(&mut self, expression: &str) -> Result<&Template, BuildError> {
229 if !self.template_cache.contains_key(expression) {
230 let template = self
231 .template_env
232 .template_from_str(expression)
233 .map_err(|e| {
234 BuildError::ValidationError(format!(
235 "Failed to compile constraint template '{}': {}",
236 expression, e
237 ))
238 })?;
239
240 let owned_template = unsafe {
242 std::mem::transmute::<Template<'_, '_>, Template<'static, 'static>>(template)
243 };
244 self.template_cache
245 .insert(expression.to_string(), owned_template);
246 }
247
248 Ok(self.template_cache.get(expression).unwrap())
249 }
250
251 fn create_template_context(&self, item: &ContentItem) -> minijinja::Value {
253 let mut item_data = HashMap::new();
254
255 item_data.insert("id".to_string(), item.id.clone().into());
257 item_data.insert("title".to_string(), item.title.clone().into());
258 item_data.insert("content".to_string(), item.content.clone().into());
259
260 for (key, value) in &item.metadata {
262 item_data.insert(key.clone(), Self::field_value_to_minijinja_value(value));
263 }
264
265 for (rel_type, targets) in &item.relationships {
267 let target_values: Vec<minijinja::Value> =
268 targets.iter().map(|s| s.clone().into()).collect();
269 item_data.insert(format!("rel_{}", rel_type), target_values.into());
270 }
271
272 item_data.insert("docname".to_string(), item.location.docname.clone().into());
274 if let Some(lineno) = item.location.lineno {
275 item_data.insert("lineno".to_string(), (lineno as i64).into());
276 }
277
278 item_data.into()
279 }
280
281 fn field_value_to_minijinja_value(
283 field_value: &crate::validation::FieldValue,
284 ) -> minijinja::Value {
285 use crate::validation::FieldValue;
286
287 match field_value {
288 FieldValue::String(s) => s.clone().into(),
289 FieldValue::Integer(i) => (*i).into(),
290 FieldValue::Float(f) => (*f).into(),
291 FieldValue::Boolean(b) => (*b).into(),
292 FieldValue::Array(arr) => arr
293 .iter()
294 .map(Self::field_value_to_minijinja_value)
295 .collect::<Vec<_>>()
296 .into(),
297 FieldValue::Object(obj) => obj
298 .iter()
299 .map(|(k, v)| (k.clone(), Self::field_value_to_minijinja_value(v)))
300 .collect::<HashMap<String, minijinja::Value>>()
301 .into(),
302 }
303 }
304
305 fn generate_error_message(
307 &mut self,
308 rule: &ValidationRule,
309 item: &ContentItem,
310 ) -> Result<String, BuildError> {
311 if let Some(error_template) = &rule.error_template {
312 let context = self.create_template_context(item);
313 let template = self.get_or_compile_template(error_template)?;
314
315 template.render(context).map_err(|e| {
316 BuildError::ValidationError(format!(
317 "Failed to render error message template: {}",
318 e
319 ))
320 })
321 } else {
322 Ok(format!(
323 "Constraint '{}' failed for item '{}'",
324 rule.name, item.id
325 ))
326 }
327 }
328}
329
330impl Default for ConstraintEngine {
331 fn default() -> Self {
332 Self::new()
333 }
334}
335
336impl Validator for ConstraintEngine {
337 fn validate(&self, _context: &ValidationContext) -> ValidationResult {
338 ValidationResult::success()
341 }
342
343 fn get_validation_rules(&self) -> Vec<ValidationRule> {
344 Vec::new() }
347
348 fn get_severity(&self) -> ValidationSeverity {
349 ValidationSeverity::Warning
350 }
351
352 fn supports_incremental(&self) -> bool {
353 true
354 }
355}
356
357impl ConstraintValidator for ConstraintEngine {
358 fn validate_constraint(&self, _rule: &ValidationRule, _item: &ContentItem) -> ValidationResult {
359 ValidationResult::success()
362 }
363
364 fn apply_actions(
365 &self,
366 _failures: &[ValidationFailure],
367 actions: &ConstraintActions,
368 ) -> ActionResult {
369 let mut warnings = Vec::new();
371 let mut errors = Vec::new();
372
373 for action in &actions.on_fail {
374 match action {
375 FailureAction::Warn => {
376 warnings.push("Constraint validation warning".to_string());
377 }
378 FailureAction::Break => {
379 errors.push(BuildError::ValidationError(
380 "Constraint validation failed critically".to_string(),
381 ));
382 }
383 FailureAction::Style => {
384 }
386 }
387 }
388
389 if errors.is_empty() {
390 ActionResult {
391 success: true,
392 warnings,
393 errors,
394 }
395 } else {
396 ActionResult {
397 success: false,
398 warnings,
399 errors,
400 }
401 }
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use crate::validation::ItemLocation;
409
410 fn create_test_item() -> ContentItem {
411 let mut metadata = HashMap::new();
412 metadata.insert(
413 "status".to_string(),
414 crate::validation::FieldValue::String("open".to_string()),
415 );
416 metadata.insert(
417 "priority".to_string(),
418 crate::validation::FieldValue::String("high".to_string()),
419 );
420
421 ContentItem {
422 id: "TEST-001".to_string(),
423 title: "Test Requirement".to_string(),
424 content: "This is a test requirement".to_string(),
425 metadata,
426 constraints: vec!["status_check".to_string()],
427 relationships: HashMap::new(),
428 location: ItemLocation {
429 docname: "requirements.rst".to_string(),
430 lineno: Some(42),
431 source_path: None,
432 },
433 style: None,
434 }
435 }
436
437 #[test]
438 fn test_constraint_engine_creation() {
439 let engine = ConstraintEngine::new();
440 assert!(!engine.template_cache.is_empty() || engine.template_cache.is_empty());
441 }
443
444 #[test]
445 fn test_template_context_creation() {
446 let engine = ConstraintEngine::new();
447 let item = create_test_item();
448
449 let context = engine.create_template_context(&item);
450
451 assert!(context.get_attr("id").is_ok());
453 assert!(context.get_attr("title").is_ok());
454 assert!(context.get_attr("status").is_ok());
455 assert!(context.get_attr("priority").is_ok());
456 }
457}