sphinx_ultra/directives/
validation.rs

1//! Directive and Role Validation System
2//!
3//! This module provides comprehensive validation for Sphinx directives and roles,
4//! including option validation, content requirements, and parameter checking.
5
6use std::collections::HashMap;
7use std::fmt;
8
9pub mod builtin;
10pub mod parser;
11pub mod roles;
12
13pub use builtin::*;
14pub use parser::*;
15pub use roles::*;
16
17/// Source location information for diagnostics
18#[derive(Debug, Clone, PartialEq)]
19pub struct SourceLocation {
20    /// File path where the directive/role was found
21    pub file: String,
22    /// Line number (1-based)
23    pub line: usize,
24    /// Column number (1-based)
25    pub column: usize,
26}
27
28/// Represents a parsed directive with validation context
29#[derive(Debug, Clone, PartialEq)]
30pub struct ParsedDirective {
31    /// The directive name (e.g., "code-block", "note", "warning")
32    pub name: String,
33    /// Arguments provided to the directive
34    pub arguments: Vec<String>,
35    /// Options specified for the directive (key-value pairs)
36    pub options: HashMap<String, String>,
37    /// The content body of the directive
38    pub content: String,
39    /// Source location information
40    pub location: SourceLocation,
41}
42
43/// Represents a parsed role with validation context
44#[derive(Debug, Clone, PartialEq)]
45pub struct ParsedRole {
46    /// The role name (e.g., "doc", "ref", "download")
47    pub name: String,
48    /// The target of the role
49    pub target: String,
50    /// Display text (if different from target)
51    pub display_text: Option<String>,
52    /// Source location information
53    pub location: SourceLocation,
54}
55
56/// Result of directive validation
57#[derive(Debug, Clone, PartialEq)]
58pub enum DirectiveValidationResult {
59    /// Directive is valid
60    Valid,
61    /// Directive has warnings but is acceptable
62    Warning(String),
63    /// Directive has errors and should be fixed
64    Error(String),
65    /// Directive is unknown/unregistered
66    Unknown,
67}
68
69/// Result of role validation
70#[derive(Debug, Clone, PartialEq)]
71pub enum RoleValidationResult {
72    /// Role is valid
73    Valid,
74    /// Role has warnings but is acceptable
75    Warning(String),
76    /// Role has errors and should be fixed
77    Error(String),
78    /// Role is unknown/unregistered
79    Unknown,
80}
81
82/// Trait for implementing directive validators
83pub trait DirectiveValidator: Send + Sync {
84    /// Returns the name of the directive this validator handles
85    fn name(&self) -> &str;
86
87    /// Validates a parsed directive
88    fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult;
89
90    /// Returns expected arguments for this directive
91    fn expected_arguments(&self) -> Vec<String>;
92
93    /// Returns valid options for this directive
94    fn valid_options(&self) -> Vec<String>;
95
96    /// Returns whether this directive requires content
97    fn requires_content(&self) -> bool;
98
99    /// Returns whether this directive allows content
100    fn allows_content(&self) -> bool;
101
102    /// Provides suggestions for fixing directive issues
103    fn get_suggestions(&self, directive: &ParsedDirective) -> Vec<String> {
104        let mut suggestions = Vec::new();
105
106        // Check for common issues and provide suggestions
107        if directive.content.is_empty() && self.requires_content() {
108            suggestions.push(format!("The '{}' directive requires content", self.name()));
109        }
110
111        if !directive.content.is_empty() && !self.allows_content() {
112            suggestions.push(format!(
113                "The '{}' directive does not allow content",
114                self.name()
115            ));
116        }
117
118        // Check for invalid options
119        let valid_options = self.valid_options();
120        for option in directive.options.keys() {
121            if !valid_options.contains(&option.to_string()) {
122                suggestions.push(format!(
123                    "Unknown option '{}' for directive '{}'",
124                    option,
125                    self.name()
126                ));
127
128                // Suggest similar options
129                for valid_option in &valid_options {
130                    if valid_option.contains(option) || option.contains(valid_option) {
131                        suggestions.push(format!("Did you mean '{}'?", valid_option));
132                        break;
133                    }
134                }
135            }
136        }
137
138        suggestions
139    }
140}
141
142/// Trait for implementing role validators
143pub trait RoleValidator: Send + Sync {
144    /// Returns the name of the role this validator handles
145    fn name(&self) -> &str;
146
147    /// Validates a parsed role
148    fn validate(&self, role: &ParsedRole) -> RoleValidationResult;
149
150    /// Returns whether this role requires a target
151    fn requires_target(&self) -> bool;
152
153    /// Returns whether this role allows display text
154    fn allows_display_text(&self) -> bool;
155
156    /// Provides suggestions for fixing role issues
157    fn get_suggestions(&self, role: &ParsedRole) -> Vec<String> {
158        let mut suggestions = Vec::new();
159
160        if role.target.is_empty() && self.requires_target() {
161            suggestions.push(format!("The '{}' role requires a target", self.name()));
162        }
163
164        if role.display_text.is_some() && !self.allows_display_text() {
165            suggestions.push(format!(
166                "The '{}' role does not support display text",
167                self.name()
168            ));
169        }
170
171        suggestions
172    }
173}
174
175/// Registry for managing directive validators
176#[derive(Default)]
177pub struct DirectiveRegistry {
178    validators: HashMap<String, Box<dyn DirectiveValidator>>,
179}
180
181impl DirectiveRegistry {
182    /// Creates a new directive registry
183    pub fn new() -> Self {
184        Self::default()
185    }
186
187    /// Creates a registry with built-in validators
188    pub fn with_builtin_validators() -> Self {
189        let mut registry = Self::new();
190        registry.register_builtin_validators();
191        registry
192    }
193
194    /// Registers a directive validator
195    pub fn register_validator(&mut self, validator: Box<dyn DirectiveValidator>) {
196        let name = validator.name().to_string();
197        self.validators.insert(name, validator);
198    }
199
200    /// Registers all built-in validators
201    pub fn register_builtin_validators(&mut self) {
202        // Register built-in directive validators
203        self.register_validator(Box::new(builtin::CodeBlockValidator::new()));
204        self.register_validator(Box::new(builtin::NoteValidator::new()));
205        self.register_validator(Box::new(builtin::WarningValidator::new()));
206        self.register_validator(Box::new(builtin::ImageValidator::new()));
207        self.register_validator(Box::new(builtin::FigureValidator::new()));
208        self.register_validator(Box::new(builtin::TocTreeValidator::new()));
209        self.register_validator(Box::new(builtin::IncludeValidator::new()));
210        self.register_validator(Box::new(builtin::LiteralIncludeValidator::new()));
211        self.register_validator(Box::new(builtin::AdmonitionValidator::new()));
212        self.register_validator(Box::new(builtin::MathValidator::new()));
213    }
214
215    /// Validates a directive
216    pub fn validate_directive(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
217        match self.validators.get(&directive.name) {
218            Some(validator) => validator.validate(directive),
219            None => DirectiveValidationResult::Unknown,
220        }
221    }
222
223    /// Gets suggestions for a directive
224    pub fn get_directive_suggestions(&self, directive: &ParsedDirective) -> Vec<String> {
225        match self.validators.get(&directive.name) {
226            Some(validator) => validator.get_suggestions(directive),
227            None => {
228                let mut suggestions = vec![format!("Unknown directive '{}'", directive.name)];
229
230                // Suggest similar directive names
231                for validator_name in self.validators.keys() {
232                    if validator_name.contains(&directive.name)
233                        || directive.name.contains(validator_name)
234                    {
235                        suggestions.push(format!("Did you mean '{}'?", validator_name));
236                    }
237                }
238
239                suggestions
240            }
241        }
242    }
243
244    /// Returns all registered directive names
245    pub fn get_registered_directives(&self) -> Vec<String> {
246        self.validators.keys().cloned().collect()
247    }
248
249    /// Checks if a directive is registered
250    pub fn is_directive_registered(&self, name: &str) -> bool {
251        self.validators.contains_key(name)
252    }
253}
254
255/// Registry for managing role validators
256#[derive(Default)]
257pub struct RoleRegistry {
258    validators: HashMap<String, Box<dyn RoleValidator>>,
259}
260
261impl RoleRegistry {
262    /// Creates a new role registry
263    pub fn new() -> Self {
264        Self::default()
265    }
266
267    /// Creates a registry with built-in validators
268    pub fn with_builtin_validators() -> Self {
269        let mut registry = Self::new();
270        registry.register_builtin_validators();
271        registry
272    }
273
274    /// Registers a role validator
275    pub fn register_validator(&mut self, validator: Box<dyn RoleValidator>) {
276        let name = validator.name().to_string();
277        self.validators.insert(name, validator);
278    }
279
280    /// Registers all built-in validators
281    pub fn register_builtin_validators(&mut self) {
282        // Register built-in role validators
283        self.register_validator(Box::new(roles::DocRoleValidator::new()));
284        self.register_validator(Box::new(roles::RefRoleValidator::new()));
285        self.register_validator(Box::new(roles::DownloadRoleValidator::new()));
286        self.register_validator(Box::new(roles::MathRoleValidator::new()));
287        self.register_validator(Box::new(roles::AbbreviationRoleValidator::new()));
288        self.register_validator(Box::new(roles::CommandRoleValidator::new()));
289        self.register_validator(Box::new(roles::FileRoleValidator::new()));
290        self.register_validator(Box::new(roles::KbdRoleValidator::new()));
291        self.register_validator(Box::new(roles::MenuSelectionRoleValidator::new()));
292        self.register_validator(Box::new(roles::GuiLabelRoleValidator::new()));
293    }
294
295    /// Validates a role
296    pub fn validate_role(&self, role: &ParsedRole) -> RoleValidationResult {
297        match self.validators.get(&role.name) {
298            Some(validator) => validator.validate(role),
299            None => RoleValidationResult::Unknown,
300        }
301    }
302
303    /// Gets suggestions for a role
304    pub fn get_role_suggestions(&self, role: &ParsedRole) -> Vec<String> {
305        match self.validators.get(&role.name) {
306            Some(validator) => validator.get_suggestions(role),
307            None => {
308                let mut suggestions = vec![format!("Unknown role '{}'", role.name)];
309
310                // Suggest similar role names
311                for validator_name in self.validators.keys() {
312                    if validator_name.contains(&role.name) || role.name.contains(validator_name) {
313                        suggestions.push(format!("Did you mean '{}'?", validator_name));
314                    }
315                }
316
317                suggestions
318            }
319        }
320    }
321
322    /// Returns all registered role names
323    pub fn get_registered_roles(&self) -> Vec<String> {
324        self.validators.keys().cloned().collect()
325    }
326
327    /// Checks if a role is registered
328    pub fn is_role_registered(&self, name: &str) -> bool {
329        self.validators.contains_key(name)
330    }
331}
332
333/// Combined validation statistics
334#[derive(Debug, Default, Clone)]
335pub struct ValidationStatistics {
336    /// Total number of directives processed
337    pub total_directives: usize,
338    /// Number of valid directives
339    pub valid_directives: usize,
340    /// Number of directives with warnings
341    pub warning_directives: usize,
342    /// Number of directives with errors
343    pub error_directives: usize,
344    /// Number of unknown directives
345    pub unknown_directives: usize,
346
347    /// Total number of roles processed
348    pub total_roles: usize,
349    /// Number of valid roles
350    pub valid_roles: usize,
351    /// Number of roles with warnings
352    pub warning_roles: usize,
353    /// Number of roles with errors
354    pub error_roles: usize,
355    /// Number of unknown roles
356    pub unknown_roles: usize,
357
358    /// Breakdown by directive type
359    pub directives_by_type: HashMap<String, usize>,
360    /// Breakdown by role type
361    pub roles_by_type: HashMap<String, usize>,
362}
363
364impl ValidationStatistics {
365    /// Creates new validation statistics
366    pub fn new() -> Self {
367        Self::default()
368    }
369
370    /// Records a directive validation result
371    pub fn record_directive(
372        &mut self,
373        directive: &ParsedDirective,
374        result: &DirectiveValidationResult,
375    ) {
376        self.total_directives += 1;
377        *self
378            .directives_by_type
379            .entry(directive.name.clone())
380            .or_insert(0) += 1;
381
382        match result {
383            DirectiveValidationResult::Valid => self.valid_directives += 1,
384            DirectiveValidationResult::Warning(_) => self.warning_directives += 1,
385            DirectiveValidationResult::Error(_) => self.error_directives += 1,
386            DirectiveValidationResult::Unknown => self.unknown_directives += 1,
387        }
388    }
389
390    /// Records a role validation result
391    pub fn record_role(&mut self, role: &ParsedRole, result: &RoleValidationResult) {
392        self.total_roles += 1;
393        *self.roles_by_type.entry(role.name.clone()).or_insert(0) += 1;
394
395        match result {
396            RoleValidationResult::Valid => self.valid_roles += 1,
397            RoleValidationResult::Warning(_) => self.warning_roles += 1,
398            RoleValidationResult::Error(_) => self.error_roles += 1,
399            RoleValidationResult::Unknown => self.unknown_roles += 1,
400        }
401    }
402
403    /// Returns validation success rate for directives (0.0 to 1.0)
404    pub fn directive_success_rate(&self) -> f64 {
405        if self.total_directives == 0 {
406            return 1.0;
407        }
408        self.valid_directives as f64 / self.total_directives as f64
409    }
410
411    /// Returns validation success rate for roles (0.0 to 1.0)
412    pub fn role_success_rate(&self) -> f64 {
413        if self.total_roles == 0 {
414            return 1.0;
415        }
416        self.valid_roles as f64 / self.total_roles as f64
417    }
418
419    /// Returns overall validation success rate (0.0 to 1.0)
420    pub fn overall_success_rate(&self) -> f64 {
421        let total = self.total_directives + self.total_roles;
422        if total == 0 {
423            return 1.0;
424        }
425        (self.valid_directives + self.valid_roles) as f64 / total as f64
426    }
427}
428
429impl fmt::Display for ValidationStatistics {
430    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431        writeln!(f, "Directive & Role Validation Statistics")?;
432        writeln!(f, "=======================================")?;
433        writeln!(f)?;
434
435        writeln!(f, "Directives:")?;
436        writeln!(f, "  Total: {}", self.total_directives)?;
437        if self.total_directives > 0 {
438            writeln!(
439                f,
440                "  Valid: {} ({:.1}%)",
441                self.valid_directives,
442                self.valid_directives as f64 / self.total_directives as f64 * 100.0
443            )?;
444            writeln!(
445                f,
446                "  Warnings: {} ({:.1}%)",
447                self.warning_directives,
448                self.warning_directives as f64 / self.total_directives as f64 * 100.0
449            )?;
450            writeln!(
451                f,
452                "  Errors: {} ({:.1}%)",
453                self.error_directives,
454                self.error_directives as f64 / self.total_directives as f64 * 100.0
455            )?;
456            writeln!(
457                f,
458                "  Unknown: {} ({:.1}%)",
459                self.unknown_directives,
460                self.unknown_directives as f64 / self.total_directives as f64 * 100.0
461            )?;
462        }
463        writeln!(f)?;
464
465        writeln!(f, "Roles:")?;
466        writeln!(f, "  Total: {}", self.total_roles)?;
467        if self.total_roles > 0 {
468            writeln!(
469                f,
470                "  Valid: {} ({:.1}%)",
471                self.valid_roles,
472                self.valid_roles as f64 / self.total_roles as f64 * 100.0
473            )?;
474            writeln!(
475                f,
476                "  Warnings: {} ({:.1}%)",
477                self.warning_roles,
478                self.warning_roles as f64 / self.total_roles as f64 * 100.0
479            )?;
480            writeln!(
481                f,
482                "  Errors: {} ({:.1}%)",
483                self.error_roles,
484                self.error_roles as f64 / self.total_roles as f64 * 100.0
485            )?;
486            writeln!(
487                f,
488                "  Unknown: {} ({:.1}%)",
489                self.unknown_roles,
490                self.unknown_roles as f64 / self.total_roles as f64 * 100.0
491            )?;
492        }
493        writeln!(f)?;
494
495        writeln!(
496            f,
497            "Overall Success Rate: {:.1}%",
498            self.overall_success_rate() * 100.0
499        )?;
500
501        Ok(())
502    }
503}
504
505/// Comprehensive directive and role validation system
506pub struct DirectiveValidationSystem {
507    directive_registry: DirectiveRegistry,
508    role_registry: RoleRegistry,
509    statistics: ValidationStatistics,
510}
511
512impl Default for DirectiveValidationSystem {
513    fn default() -> Self {
514        Self::new()
515    }
516}
517
518impl DirectiveValidationSystem {
519    /// Creates a new validation system with built-in validators
520    pub fn new() -> Self {
521        Self {
522            directive_registry: DirectiveRegistry::with_builtin_validators(),
523            role_registry: RoleRegistry::with_builtin_validators(),
524            statistics: ValidationStatistics::new(),
525        }
526    }
527
528    /// Gets a reference to the directive registry
529    pub fn directive_registry(&self) -> &DirectiveRegistry {
530        &self.directive_registry
531    }
532
533    /// Gets a mutable reference to the directive registry
534    pub fn directive_registry_mut(&mut self) -> &mut DirectiveRegistry {
535        &mut self.directive_registry
536    }
537
538    /// Gets a reference to the role registry
539    pub fn role_registry(&self) -> &RoleRegistry {
540        &self.role_registry
541    }
542
543    /// Gets a mutable reference to the role registry
544    pub fn role_registry_mut(&mut self) -> &mut RoleRegistry {
545        &mut self.role_registry
546    }
547
548    /// Validates a directive and updates statistics
549    pub fn validate_directive(&mut self, directive: &ParsedDirective) -> DirectiveValidationResult {
550        let result = self.directive_registry.validate_directive(directive);
551        self.statistics.record_directive(directive, &result);
552        result
553    }
554
555    /// Validates a role and updates statistics
556    pub fn validate_role(&mut self, role: &ParsedRole) -> RoleValidationResult {
557        let result = self.role_registry.validate_role(role);
558        self.statistics.record_role(role, &result);
559        result
560    }
561
562    /// Gets directive suggestions
563    pub fn get_directive_suggestions(&self, directive: &ParsedDirective) -> Vec<String> {
564        self.directive_registry.get_directive_suggestions(directive)
565    }
566
567    /// Gets role suggestions
568    pub fn get_role_suggestions(&self, role: &ParsedRole) -> Vec<String> {
569        self.role_registry.get_role_suggestions(role)
570    }
571
572    /// Returns current validation statistics
573    pub fn statistics(&self) -> &ValidationStatistics {
574        &self.statistics
575    }
576
577    /// Resets validation statistics
578    pub fn reset_statistics(&mut self) {
579        self.statistics = ValidationStatistics::new();
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    #[test]
588    fn test_directive_registry_creation() {
589        let registry = DirectiveRegistry::new();
590        assert_eq!(registry.get_registered_directives().len(), 0);
591    }
592
593    #[test]
594    fn test_directive_registry_with_builtin() {
595        let registry = DirectiveRegistry::with_builtin_validators();
596        assert!(!registry.get_registered_directives().is_empty());
597        assert!(registry.is_directive_registered("code-block"));
598        assert!(registry.is_directive_registered("note"));
599        assert!(registry.is_directive_registered("warning"));
600    }
601
602    #[test]
603    fn test_role_registry_creation() {
604        let registry = RoleRegistry::new();
605        assert_eq!(registry.get_registered_roles().len(), 0);
606    }
607
608    #[test]
609    fn test_role_registry_with_builtin() {
610        let registry = RoleRegistry::with_builtin_validators();
611        assert!(!registry.get_registered_roles().is_empty());
612        assert!(registry.is_role_registered("doc"));
613        assert!(registry.is_role_registered("ref"));
614        assert!(registry.is_role_registered("download"));
615    }
616
617    #[test]
618    fn test_validation_statistics() {
619        let mut stats = ValidationStatistics::new();
620
621        let directive = ParsedDirective {
622            name: "note".to_string(),
623            arguments: vec![],
624            options: HashMap::new(),
625            content: "Test content".to_string(),
626            location: SourceLocation {
627                file: "test.rst".to_string(),
628                line: 1,
629                column: 1,
630            },
631        };
632
633        stats.record_directive(&directive, &DirectiveValidationResult::Valid);
634        assert_eq!(stats.total_directives, 1);
635        assert_eq!(stats.valid_directives, 1);
636        assert_eq!(stats.directive_success_rate(), 1.0);
637    }
638
639    #[test]
640    fn test_validation_system() {
641        let mut system = DirectiveValidationSystem::new();
642
643        let directive = ParsedDirective {
644            name: "note".to_string(),
645            arguments: vec![],
646            options: HashMap::new(),
647            content: "Test content".to_string(),
648            location: SourceLocation {
649                file: "test.rst".to_string(),
650                line: 1,
651                column: 1,
652            },
653        };
654
655        let result = system.validate_directive(&directive);
656        assert_eq!(result, DirectiveValidationResult::Valid);
657        assert_eq!(system.statistics().total_directives, 1);
658    }
659}