1pub mod constraint_engine;
8pub mod expression_evaluator;
9
10use std::collections::HashMap;
11use std::fmt;
12
13use serde::{Deserialize, Serialize};
14
15use crate::error::BuildError;
16
17pub use constraint_engine::ConstraintEngine;
18pub use expression_evaluator::ExpressionEvaluator;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum ValidationSeverity {
24 Info,
25 Warning,
26 Error,
27 Critical,
28}
29
30impl fmt::Display for ValidationSeverity {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 ValidationSeverity::Info => write!(f, "info"),
34 ValidationSeverity::Warning => write!(f, "warning"),
35 ValidationSeverity::Error => write!(f, "error"),
36 ValidationSeverity::Critical => write!(f, "critical"),
37 }
38 }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ConstraintActions {
44 pub on_fail: Vec<FailureAction>,
46 pub style_changes: Vec<String>,
48 pub force_style: bool,
50}
51
52impl Default for ConstraintActions {
53 fn default() -> Self {
54 Self {
55 on_fail: vec![FailureAction::Warn],
56 style_changes: Vec::new(),
57 force_style: false,
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "lowercase")]
65pub enum FailureAction {
66 Warn,
68 Break,
70 Style,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ValidationRule {
77 pub name: String,
79 pub description: Option<String>,
81 pub constraint: String,
83 pub severity: ValidationSeverity,
85 pub actions: ConstraintActions,
87 pub error_template: Option<String>,
89}
90
91#[derive(Debug, Clone)]
93pub struct ValidationResult {
94 pub passed: bool,
96 pub error_message: Option<String>,
98 pub context: HashMap<String, String>,
100}
101
102impl ValidationResult {
103 pub fn success() -> Self {
105 Self {
106 passed: true,
107 error_message: None,
108 context: HashMap::new(),
109 }
110 }
111
112 pub fn failure(message: String) -> Self {
114 Self {
115 passed: false,
116 error_message: Some(message),
117 context: HashMap::new(),
118 }
119 }
120
121 pub fn failure_with_context(message: String, context: HashMap<String, String>) -> Self {
123 Self {
124 passed: false,
125 error_message: Some(message),
126 context,
127 }
128 }
129
130 pub fn with_context(mut self, key: String, value: String) -> Self {
132 self.context.insert(key, value);
133 self
134 }
135}
136
137#[derive(Debug)]
139pub struct ActionResult {
140 pub success: bool,
142 pub warnings: Vec<String>,
144 pub errors: Vec<BuildError>,
146}
147
148impl ActionResult {
149 pub fn success() -> Self {
150 Self {
151 success: true,
152 warnings: Vec::new(),
153 errors: Vec::new(),
154 }
155 }
156
157 pub fn failure(error: BuildError) -> Self {
158 Self {
159 success: false,
160 warnings: Vec::new(),
161 errors: vec![error],
162 }
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct ContentItem {
169 pub id: String,
171 pub title: String,
173 pub content: String,
175 pub metadata: HashMap<String, FieldValue>,
177 pub constraints: Vec<String>,
179 pub relationships: HashMap<String, Vec<String>>,
181 pub location: ItemLocation,
183 pub style: Option<String>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(untagged)]
190pub enum FieldValue {
191 String(String),
192 Integer(i64),
193 Float(f64),
194 Boolean(bool),
195 Array(Vec<FieldValue>),
196 Object(HashMap<String, FieldValue>),
197}
198
199impl FieldValue {
200 pub fn as_string(&self) -> Option<&str> {
202 match self {
203 FieldValue::String(s) => Some(s),
204 _ => None,
205 }
206 }
207
208 pub fn as_integer(&self) -> Option<i64> {
210 match self {
211 FieldValue::Integer(i) => Some(*i),
212 _ => None,
213 }
214 }
215
216 pub fn as_boolean(&self) -> Option<bool> {
218 match self {
219 FieldValue::Boolean(b) => Some(*b),
220 _ => None,
221 }
222 }
223
224 pub fn as_array(&self) -> Option<&Vec<FieldValue>> {
226 match self {
227 FieldValue::Array(arr) => Some(arr),
228 _ => None,
229 }
230 }
231}
232
233impl fmt::Display for FieldValue {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 match self {
236 FieldValue::String(s) => write!(f, "{}", s),
237 FieldValue::Integer(i) => write!(f, "{}", i),
238 FieldValue::Float(fl) => write!(f, "{}", fl),
239 FieldValue::Boolean(b) => write!(f, "{}", b),
240 FieldValue::Array(arr) => {
241 write!(f, "[")?;
242 for (i, item) in arr.iter().enumerate() {
243 if i > 0 {
244 write!(f, ", ")?;
245 }
246 write!(f, "{}", item)?;
247 }
248 write!(f, "]")
249 }
250 FieldValue::Object(obj) => {
251 write!(f, "{{")?;
252 for (i, (key, value)) in obj.iter().enumerate() {
253 if i > 0 {
254 write!(f, ", ")?;
255 }
256 write!(f, "{}: {}", key, value)?;
257 }
258 write!(f, "}}")
259 }
260 }
261 }
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct ItemLocation {
267 pub docname: String,
269 pub lineno: Option<u32>,
271 pub source_path: Option<String>,
273}
274
275#[derive(Debug)]
277pub struct ValidationContext<'a> {
278 pub current_item: &'a ContentItem,
280 pub all_items: &'a HashMap<String, ContentItem>,
282 pub config: &'a ValidationConfig,
284 pub variables: HashMap<String, FieldValue>,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct ValidationConfig {
291 pub constraints: HashMap<String, ConstraintDefinition>,
293 pub constraint_failed_options: HashMap<String, ConstraintActions>,
295 pub settings: ValidationSettings,
297}
298
299impl Default for ValidationConfig {
300 fn default() -> Self {
301 let mut constraint_failed_options = HashMap::new();
302
303 constraint_failed_options.insert(
305 "info".to_string(),
306 ConstraintActions {
307 on_fail: vec![],
308 style_changes: vec![],
309 force_style: false,
310 },
311 );
312
313 constraint_failed_options.insert(
314 "warning".to_string(),
315 ConstraintActions {
316 on_fail: vec![FailureAction::Warn],
317 style_changes: vec!["constraint-warning".to_string()],
318 force_style: false,
319 },
320 );
321
322 constraint_failed_options.insert(
323 "error".to_string(),
324 ConstraintActions {
325 on_fail: vec![FailureAction::Warn, FailureAction::Style],
326 style_changes: vec!["constraint-error".to_string()],
327 force_style: false,
328 },
329 );
330
331 constraint_failed_options.insert(
332 "critical".to_string(),
333 ConstraintActions {
334 on_fail: vec![FailureAction::Break],
335 style_changes: vec!["constraint-critical".to_string()],
336 force_style: true,
337 },
338 );
339
340 Self {
341 constraints: HashMap::new(),
342 constraint_failed_options,
343 settings: ValidationSettings::default(),
344 }
345 }
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct ConstraintDefinition {
351 pub checks: HashMap<String, String>,
353 pub severity: ValidationSeverity,
355 pub error_message: Option<String>,
357 pub description: Option<String>,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct ValidationSettings {
364 pub enable_constraints: bool,
366 pub cache_results: bool,
368 pub max_errors: Option<usize>,
370 pub continue_on_error: bool,
372}
373
374impl Default for ValidationSettings {
375 fn default() -> Self {
376 Self {
377 enable_constraints: true,
378 cache_results: true,
379 max_errors: None,
380 continue_on_error: true,
381 }
382 }
383}
384
385pub trait Validator {
387 fn validate(&self, context: &ValidationContext) -> ValidationResult;
389
390 fn get_validation_rules(&self) -> Vec<ValidationRule>;
392
393 fn get_severity(&self) -> ValidationSeverity;
395
396 fn supports_incremental(&self) -> bool {
398 false
399 }
400}
401
402pub trait ConstraintValidator: Validator {
404 fn validate_constraint(&self, rule: &ValidationRule, item: &ContentItem) -> ValidationResult;
406
407 fn apply_actions(
409 &self,
410 failures: &[ValidationFailure],
411 actions: &ConstraintActions,
412 ) -> ActionResult;
413}
414
415#[derive(Debug, Clone)]
417pub struct ValidationFailure {
418 pub rule: ValidationRule,
420 pub result: ValidationResult,
422 pub item_id: String,
424 pub severity: ValidationSeverity,
426}
427
428impl ValidationFailure {
429 pub fn new(rule: ValidationRule, result: ValidationResult, item_id: String) -> Self {
430 let severity = rule.severity;
431 Self {
432 rule,
433 result,
434 item_id,
435 severity,
436 }
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_validation_result_creation() {
446 let success = ValidationResult::success();
447 assert!(success.passed);
448 assert!(success.error_message.is_none());
449
450 let failure = ValidationResult::failure("Test error".to_string());
451 assert!(!failure.passed);
452 assert_eq!(failure.error_message.unwrap(), "Test error");
453 }
454
455 #[test]
456 fn test_field_value_display() {
457 let string_val = FieldValue::String("test".to_string());
458 assert_eq!(format!("{}", string_val), "test");
459
460 let int_val = FieldValue::Integer(42);
461 assert_eq!(format!("{}", int_val), "42");
462
463 let bool_val = FieldValue::Boolean(true);
464 assert_eq!(format!("{}", bool_val), "true");
465 }
466
467 #[test]
468 fn test_validation_config_defaults() {
469 let config = ValidationConfig::default();
470 assert!(config.settings.enable_constraints);
471 assert!(config.settings.cache_results);
472 assert!(config.constraint_failed_options.contains_key("critical"));
473 }
474
475 #[test]
476 fn test_content_item_creation() {
477 let item = ContentItem {
478 id: "test-001".to_string(),
479 title: "Test Item".to_string(),
480 content: "Test content".to_string(),
481 metadata: HashMap::new(),
482 constraints: vec!["test-constraint".to_string()],
483 relationships: HashMap::new(),
484 location: ItemLocation {
485 docname: "test.rst".to_string(),
486 lineno: Some(10),
487 source_path: None,
488 },
489 style: None,
490 };
491
492 assert_eq!(item.id, "test-001");
493 assert_eq!(item.constraints.len(), 1);
494 }
495}