pub mod constraint_engine;
pub mod expression_evaluator;
use std::collections::HashMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::error::BuildError;
pub use constraint_engine::ConstraintEngine;
pub use expression_evaluator::ExpressionEvaluator;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ValidationSeverity {
Info,
Warning,
Error,
Critical,
}
impl fmt::Display for ValidationSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationSeverity::Info => write!(f, "info"),
ValidationSeverity::Warning => write!(f, "warning"),
ValidationSeverity::Error => write!(f, "error"),
ValidationSeverity::Critical => write!(f, "critical"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstraintActions {
pub on_fail: Vec<FailureAction>,
pub style_changes: Vec<String>,
pub force_style: bool,
}
impl Default for ConstraintActions {
fn default() -> Self {
Self {
on_fail: vec![FailureAction::Warn],
style_changes: Vec::new(),
force_style: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FailureAction {
Warn,
Break,
Style,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationRule {
pub name: String,
pub description: Option<String>,
pub constraint: String,
pub severity: ValidationSeverity,
pub actions: ConstraintActions,
pub error_template: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub passed: bool,
pub error_message: Option<String>,
pub context: HashMap<String, String>,
}
impl ValidationResult {
pub fn success() -> Self {
Self {
passed: true,
error_message: None,
context: HashMap::new(),
}
}
pub fn failure(message: String) -> Self {
Self {
passed: false,
error_message: Some(message),
context: HashMap::new(),
}
}
pub fn failure_with_context(message: String, context: HashMap<String, String>) -> Self {
Self {
passed: false,
error_message: Some(message),
context,
}
}
pub fn with_context(mut self, key: String, value: String) -> Self {
self.context.insert(key, value);
self
}
}
#[derive(Debug)]
pub struct ActionResult {
pub success: bool,
pub warnings: Vec<String>,
pub errors: Vec<BuildError>,
}
impl ActionResult {
pub fn success() -> Self {
Self {
success: true,
warnings: Vec::new(),
errors: Vec::new(),
}
}
pub fn failure(error: BuildError) -> Self {
Self {
success: false,
warnings: Vec::new(),
errors: vec![error],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentItem {
pub id: String,
pub title: String,
pub content: String,
pub metadata: HashMap<String, FieldValue>,
pub constraints: Vec<String>,
pub relationships: HashMap<String, Vec<String>>,
pub location: ItemLocation,
pub style: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FieldValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Array(Vec<FieldValue>),
Object(HashMap<String, FieldValue>),
}
impl FieldValue {
pub fn as_string(&self) -> Option<&str> {
match self {
FieldValue::String(s) => Some(s),
_ => None,
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
FieldValue::Integer(i) => Some(*i),
_ => None,
}
}
pub fn as_boolean(&self) -> Option<bool> {
match self {
FieldValue::Boolean(b) => Some(*b),
_ => None,
}
}
pub fn as_array(&self) -> Option<&Vec<FieldValue>> {
match self {
FieldValue::Array(arr) => Some(arr),
_ => None,
}
}
}
impl fmt::Display for FieldValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FieldValue::String(s) => write!(f, "{}", s),
FieldValue::Integer(i) => write!(f, "{}", i),
FieldValue::Float(fl) => write!(f, "{}", fl),
FieldValue::Boolean(b) => write!(f, "{}", b),
FieldValue::Array(arr) => {
write!(f, "[")?;
for (i, item) in arr.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", item)?;
}
write!(f, "]")
}
FieldValue::Object(obj) => {
write!(f, "{{")?;
for (i, (key, value)) in obj.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}: {}", key, value)?;
}
write!(f, "}}")
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ItemLocation {
pub docname: String,
pub lineno: Option<u32>,
pub source_path: Option<String>,
}
#[derive(Debug)]
pub struct ValidationContext<'a> {
pub current_item: &'a ContentItem,
pub all_items: &'a HashMap<String, ContentItem>,
pub config: &'a ValidationConfig,
pub variables: HashMap<String, FieldValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationConfig {
pub constraints: HashMap<String, ConstraintDefinition>,
pub constraint_failed_options: HashMap<String, ConstraintActions>,
pub settings: ValidationSettings,
}
impl Default for ValidationConfig {
fn default() -> Self {
let mut constraint_failed_options = HashMap::new();
constraint_failed_options.insert(
"info".to_string(),
ConstraintActions {
on_fail: vec![],
style_changes: vec![],
force_style: false,
},
);
constraint_failed_options.insert(
"warning".to_string(),
ConstraintActions {
on_fail: vec![FailureAction::Warn],
style_changes: vec!["constraint-warning".to_string()],
force_style: false,
},
);
constraint_failed_options.insert(
"error".to_string(),
ConstraintActions {
on_fail: vec![FailureAction::Warn, FailureAction::Style],
style_changes: vec!["constraint-error".to_string()],
force_style: false,
},
);
constraint_failed_options.insert(
"critical".to_string(),
ConstraintActions {
on_fail: vec![FailureAction::Break],
style_changes: vec!["constraint-critical".to_string()],
force_style: true,
},
);
Self {
constraints: HashMap::new(),
constraint_failed_options,
settings: ValidationSettings::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstraintDefinition {
pub checks: HashMap<String, String>,
pub severity: ValidationSeverity,
pub error_message: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationSettings {
pub enable_constraints: bool,
pub cache_results: bool,
pub max_errors: Option<usize>,
pub continue_on_error: bool,
}
impl Default for ValidationSettings {
fn default() -> Self {
Self {
enable_constraints: true,
cache_results: true,
max_errors: None,
continue_on_error: true,
}
}
}
pub trait Validator {
fn validate(&self, context: &ValidationContext) -> ValidationResult;
fn get_validation_rules(&self) -> Vec<ValidationRule>;
fn get_severity(&self) -> ValidationSeverity;
fn supports_incremental(&self) -> bool {
false
}
}
pub trait ConstraintValidator: Validator {
fn validate_constraint(&self, rule: &ValidationRule, item: &ContentItem) -> ValidationResult;
fn apply_actions(
&self,
failures: &[ValidationFailure],
actions: &ConstraintActions,
) -> ActionResult;
}
#[derive(Debug, Clone)]
pub struct ValidationFailure {
pub rule: ValidationRule,
pub result: ValidationResult,
pub item_id: String,
pub severity: ValidationSeverity,
}
impl ValidationFailure {
pub fn new(rule: ValidationRule, result: ValidationResult, item_id: String) -> Self {
let severity = rule.severity;
Self {
rule,
result,
item_id,
severity,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_result_creation() {
let success = ValidationResult::success();
assert!(success.passed);
assert!(success.error_message.is_none());
let failure = ValidationResult::failure("Test error".to_string());
assert!(!failure.passed);
assert_eq!(failure.error_message.unwrap(), "Test error");
}
#[test]
fn test_field_value_display() {
let string_val = FieldValue::String("test".to_string());
assert_eq!(format!("{}", string_val), "test");
let int_val = FieldValue::Integer(42);
assert_eq!(format!("{}", int_val), "42");
let bool_val = FieldValue::Boolean(true);
assert_eq!(format!("{}", bool_val), "true");
}
#[test]
fn test_validation_config_defaults() {
let config = ValidationConfig::default();
assert!(config.settings.enable_constraints);
assert!(config.settings.cache_results);
assert!(config.constraint_failed_options.contains_key("critical"));
}
#[test]
fn test_content_item_creation() {
let item = ContentItem {
id: "test-001".to_string(),
title: "Test Item".to_string(),
content: "Test content".to_string(),
metadata: HashMap::new(),
constraints: vec!["test-constraint".to_string()],
relationships: HashMap::new(),
location: ItemLocation {
docname: "test.rst".to_string(),
lineno: Some(10),
source_path: None,
},
style: None,
};
assert_eq!(item.id, "test-001");
assert_eq!(item.constraints.len(), 1);
}
}