ass_core/analysis/styles/
validation.rs

1//! Style validation and conflict detection for ASS subtitle styles
2//!
3//! Provides comprehensive validation of style definitions including property
4//! validation, inheritance analysis, and conflict detection. Designed for
5//! editor integration and script quality assurance.
6//!
7//! # Features
8//!
9//! - Property validation with configurable severity levels
10//! - Circular inheritance detection and resolution
11//! - Duplicate name and missing reference detection
12//! - Performance impact assessment for validation issues
13//! - Zero-copy validation with lifetime-generic references
14//!
15//! # Performance
16//!
17//! - Target: <0.5ms per style validation
18//! - Memory: Minimal allocations via zero-copy issue references
19//! - Validation: Configurable depth limits for inheritance analysis
20
21use alloc::{format, string::String, string::ToString, vec, vec::Vec};
22use core::fmt;
23
24/// Severity level for style validation issues
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
26pub enum ValidationSeverity {
27    /// Informational message about style properties
28    Info,
29    /// Warning about potential rendering or performance issues
30    Warning,
31    /// Error that violates ASS specification or causes problems
32    Error,
33}
34
35impl fmt::Display for ValidationSeverity {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            Self::Info => write!(f, "info"),
39            Self::Warning => write!(f, "warning"),
40            Self::Error => write!(f, "error"),
41        }
42    }
43}
44
45/// Style validation issue with context and suggestions
46#[derive(Debug, Clone)]
47pub struct StyleValidationIssue {
48    /// Issue severity level
49    pub severity: ValidationSeverity,
50    /// Human-readable issue description
51    pub message: String,
52    /// Style field that caused the issue
53    pub field: String,
54    /// Optional suggested fix or improvement
55    pub suggestion: Option<String>,
56}
57
58/// Style inheritance tracking and analysis
59#[derive(Debug, Clone)]
60pub struct StyleInheritance<'a> {
61    /// Style name (zero-copy reference)
62    pub name: &'a str,
63    /// Parent styles in inheritance chain
64    pub parents: Vec<&'a str>,
65    /// Child styles that inherit from this style
66    pub children: Vec<&'a str>,
67    /// Inheritance depth (0 = root style, no parents)
68    pub depth: usize,
69    /// Whether circular inheritance was detected
70    pub has_circular_inheritance: bool,
71}
72
73/// Style conflict detection and classification
74#[derive(Debug, Clone)]
75pub struct StyleConflict<'a> {
76    /// Names of conflicting styles
77    pub styles: Vec<&'a str>,
78    /// Type of conflict detected
79    pub conflict_type: ConflictType,
80    /// Detailed conflict description
81    pub description: String,
82    /// Severity of the conflict
83    pub severity: ValidationSeverity,
84}
85
86/// Types of style conflicts that can be detected
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum ConflictType {
89    /// Multiple styles with identical names
90    DuplicateName,
91    /// Circular reference in style inheritance
92    CircularInheritance,
93    /// Conflicting property values between related styles
94    PropertyConflict,
95    /// Reference to non-existent style
96    MissingReference,
97}
98
99impl StyleValidationIssue {
100    /// Create new validation issue
101    #[must_use]
102    pub fn new(
103        severity: ValidationSeverity,
104        field: &str,
105        message: &str,
106        suggestion: Option<&str>,
107    ) -> Self {
108        Self {
109            severity,
110            message: message.to_string(),
111            field: field.to_string(),
112            suggestion: suggestion.map(ToString::to_string),
113        }
114    }
115
116    /// Create error-level issue
117    #[must_use]
118    pub fn error(field: &str, message: &str) -> Self {
119        Self::new(ValidationSeverity::Error, field, message, None)
120    }
121
122    /// Create warning-level issue
123    #[must_use]
124    pub fn warning(field: &str, message: &str) -> Self {
125        Self::new(ValidationSeverity::Warning, field, message, None)
126    }
127
128    /// Create info-level issue with suggestion
129    #[must_use]
130    pub fn info_with_suggestion(field: &str, message: &str, suggestion: &str) -> Self {
131        Self::new(ValidationSeverity::Info, field, message, Some(suggestion))
132    }
133}
134
135impl<'a> StyleInheritance<'a> {
136    /// Create new inheritance tracker for style
137    #[must_use]
138    pub const fn new(name: &'a str) -> Self {
139        Self {
140            name,
141            parents: Vec::new(),
142            children: Vec::new(),
143            depth: 0,
144            has_circular_inheritance: false,
145        }
146    }
147
148    /// Add parent style to inheritance chain
149    pub fn add_parent(&mut self, parent: &'a str) {
150        if !self.parents.contains(&parent) {
151            self.parents.push(parent);
152        }
153    }
154
155    /// Set parent style (for single inheritance)
156    pub fn set_parent(&mut self, parent: &'a str) {
157        self.parents.clear();
158        self.parents.push(parent);
159        self.depth = 1; // Will be adjusted later based on parent's depth
160    }
161
162    /// Add child style that inherits from this one
163    pub fn add_child(&mut self, child: &'a str) {
164        if !self.children.contains(&child) {
165            self.children.push(child);
166        }
167    }
168
169    /// Check if style has inheritance relationships
170    #[must_use]
171    pub fn has_inheritance(&self) -> bool {
172        !self.parents.is_empty() || !self.children.is_empty()
173    }
174
175    /// Check if style is root (no parents)
176    #[must_use]
177    pub fn is_root(&self) -> bool {
178        self.parents.is_empty()
179    }
180
181    /// Check if style is leaf (no children)
182    #[must_use]
183    pub fn is_leaf(&self) -> bool {
184        self.children.is_empty()
185    }
186}
187
188impl<'a> StyleConflict<'a> {
189    /// Create new style conflict
190    #[must_use]
191    pub fn new(
192        conflict_type: ConflictType,
193        styles: Vec<&'a str>,
194        description: &str,
195        severity: ValidationSeverity,
196    ) -> Self {
197        Self {
198            styles,
199            conflict_type,
200            description: description.to_string(),
201            severity,
202        }
203    }
204
205    /// Create duplicate name conflict
206    #[must_use]
207    pub fn duplicate_name(style_names: Vec<&'a str>) -> Self {
208        let description = format!("Duplicate style names found: {style_names:?}");
209        Self::new(
210            ConflictType::DuplicateName,
211            style_names,
212            &description,
213            ValidationSeverity::Error,
214        )
215    }
216
217    /// Create circular inheritance conflict
218    #[must_use]
219    pub fn circular_inheritance(cycle_styles: Vec<&'a str>) -> Self {
220        let description = format!("Circular inheritance detected: {cycle_styles:?}");
221        Self::new(
222            ConflictType::CircularInheritance,
223            cycle_styles,
224            &description,
225            ValidationSeverity::Error,
226        )
227    }
228
229    /// Create missing reference conflict
230    #[must_use]
231    pub fn missing_reference(referencing_style: &'a str, missing_style: &'a str) -> Self {
232        let description =
233            format!("Style '{referencing_style}' references non-existent style '{missing_style}'");
234        Self::new(
235            ConflictType::MissingReference,
236            vec![referencing_style],
237            &description,
238            ValidationSeverity::Error,
239        )
240    }
241
242    /// Create missing parent conflict
243    #[must_use]
244    pub fn missing_parent(style_name: &'a str, parent_name: &'a str) -> Self {
245        let description =
246            format!("Style '{style_name}' inherits from non-existent parent '{parent_name}'");
247        Self::new(
248            ConflictType::MissingReference,
249            vec![style_name],
250            &description,
251            ValidationSeverity::Warning,
252        )
253    }
254}
255
256impl fmt::Display for ConflictType {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        match self {
259            Self::DuplicateName => write!(f, "duplicate_name"),
260            Self::CircularInheritance => write!(f, "circular_inheritance"),
261            Self::PropertyConflict => write!(f, "property_conflict"),
262            Self::MissingReference => write!(f, "missing_reference"),
263        }
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn validation_issue_creation() {
273        let issue = StyleValidationIssue::error("font_size", "Invalid font size value");
274        assert_eq!(issue.severity, ValidationSeverity::Error);
275        assert_eq!(issue.field, "font_size");
276        assert!(issue.suggestion.is_none());
277
278        let info = StyleValidationIssue::info_with_suggestion(
279            "font_name",
280            "Font may not be available",
281            "Use Arial as fallback",
282        );
283        assert_eq!(info.severity, ValidationSeverity::Info);
284        assert!(info.suggestion.is_some());
285    }
286
287    #[test]
288    fn inheritance_tracking() {
289        let mut inheritance = StyleInheritance::new("Child");
290        assert!(inheritance.is_root());
291        assert!(inheritance.is_leaf());
292
293        inheritance.add_parent("Parent");
294        assert!(!inheritance.is_root());
295        assert!(inheritance.has_inheritance());
296
297        inheritance.add_child("Grandchild");
298        assert!(!inheritance.is_leaf());
299    }
300
301    #[test]
302    fn conflict_creation() {
303        let conflict = StyleConflict::duplicate_name(vec!["Style1", "Style1"]);
304        assert_eq!(conflict.conflict_type, ConflictType::DuplicateName);
305        assert_eq!(conflict.severity, ValidationSeverity::Error);
306
307        let missing = StyleConflict::missing_reference("Child", "MissingParent");
308        assert_eq!(missing.conflict_type, ConflictType::MissingReference);
309        assert!(missing.description.contains("MissingParent"));
310    }
311
312    #[test]
313    fn severity_ordering() {
314        assert!(ValidationSeverity::Error > ValidationSeverity::Warning);
315        assert!(ValidationSeverity::Warning > ValidationSeverity::Info);
316    }
317}