Skip to main content

oparry_validators/
lib.rs

1//! Specialized validators for Parry
2
3pub mod tailwind;
4pub mod imports;
5pub mod rust;
6pub mod react;
7pub mod css;
8pub mod components;
9pub mod accessibility;
10pub mod security;
11pub mod performance;
12pub mod typescript;
13pub mod testing;
14
15pub use tailwind::TailwindValidator;
16pub use imports::ImportValidator;
17pub use rust::RustValidator;
18pub use react::ReactValidator;
19pub use css::CssValidator;
20pub use components::ComponentValidator;
21pub use accessibility::A11yValidator;
22pub use security::SecurityValidator;
23pub use performance::PerformanceValidator;
24pub use typescript::TypeScriptValidator;
25pub use testing::TestingValidator;
26
27use oparry_core::{Issue, IssueLevel, Result, ValidationResult};
28use oparry_parser::{ParsedCode, Language};
29use std::path::Path;
30
31/// Validator trait
32pub trait Validator: Send + Sync {
33    /// Get validator name
34    fn name(&self) -> &str;
35
36    /// Check if validator supports given language
37    fn supports(&self, language: Language) -> bool;
38
39    /// Validate parsed code
40    fn validate_parsed(&self, code: &ParsedCode, file: &Path) -> Result<ValidationResult>;
41
42    /// Validate raw source (fallback)
43    fn validate_raw(&self, source: &str, file: &Path) -> Result<ValidationResult> {
44        let _ = source;
45        let _ = file;
46        Ok(ValidationResult::new())
47    }
48}
49
50/// Collection of validators
51#[derive(Default)]
52pub struct Validators {
53    tailwind: Option<TailwindValidator>,
54    imports: Option<ImportValidator>,
55    rust: Option<RustValidator>,
56    react: Option<ReactValidator>,
57    css: Option<CssValidator>,
58    components: Option<ComponentValidator>,
59    accessibility: Option<A11yValidator>,
60    security: Option<SecurityValidator>,
61    performance: Option<PerformanceValidator>,
62    typescript: Option<TypeScriptValidator>,
63    testing: Option<TestingValidator>,
64}
65
66impl Validators {
67    /// Create new validators collection
68    pub fn new() -> Self {
69        Self::default()
70    }
71
72    /// Add Tailwind validator
73    pub fn with_tailwind(mut self, validator: TailwindValidator) -> Self {
74        self.tailwind = Some(validator);
75        self
76    }
77
78    /// Add Import validator
79    pub fn with_imports(mut self, validator: ImportValidator) -> Self {
80        self.imports = Some(validator);
81        self
82    }
83
84    /// Add Rust validator
85    pub fn with_rust(mut self, validator: RustValidator) -> Self {
86        self.rust = Some(validator);
87        self
88    }
89
90    /// Add React validator
91    pub fn with_react(mut self, validator: ReactValidator) -> Self {
92        self.react = Some(validator);
93        self
94    }
95
96    /// Add CSS validator
97    pub fn with_css(mut self, validator: CssValidator) -> Self {
98        self.css = Some(validator);
99        self
100    }
101
102    /// Add Component validator
103    pub fn with_components(mut self, validator: ComponentValidator) -> Self {
104        self.components = Some(validator);
105        self
106    }
107
108    /// Add Accessibility validator
109    pub fn with_accessibility(mut self, validator: A11yValidator) -> Self {
110        self.accessibility = Some(validator);
111        self
112    }
113
114    /// Add Security validator
115    pub fn with_security(mut self, validator: SecurityValidator) -> Self {
116        self.security = Some(validator);
117        self
118    }
119
120    /// Add Performance validator
121    pub fn with_performance(mut self, validator: PerformanceValidator) -> Self {
122        self.performance = Some(validator);
123        self
124    }
125
126    /// Add TypeScript validator
127    pub fn with_typescript(mut self, validator: TypeScriptValidator) -> Self {
128        self.typescript = Some(validator);
129        self
130    }
131
132    /// Add Testing validator
133    pub fn with_testing(mut self, validator: TestingValidator) -> Self {
134        self.testing = Some(validator);
135        self
136    }
137
138    /// Validate code with all applicable validators
139    pub fn validate(&self, code: &ParsedCode, file: &Path) -> Result<ValidationResult> {
140        let mut result = ValidationResult::new();
141        result.files_checked = 1;
142
143        let language = Language::from_path(file);
144
145        // Run applicable validators
146        if let Some(ref validator) = self.tailwind {
147            if validator.supports(language) {
148                result.merge(validator.validate_parsed(code, file)?);
149            }
150        }
151
152        if let Some(ref validator) = self.imports {
153            if validator.supports(language) {
154                result.merge(validator.validate_parsed(code, file)?);
155            }
156        }
157
158        if let Some(ref validator) = self.rust {
159            if validator.supports(language) {
160                result.merge(validator.validate_parsed(code, file)?);
161            }
162        }
163
164        if let Some(ref validator) = self.react {
165            if validator.supports(language) {
166                result.merge(validator.validate_parsed(code, file)?);
167            }
168        }
169
170        if let Some(ref validator) = self.css {
171            if validator.supports(language) {
172                result.merge(validator.validate_parsed(code, file)?);
173            }
174        }
175
176        if let Some(ref validator) = self.components {
177            if validator.supports(language) {
178                result.merge(validator.validate_parsed(code, file)?);
179            }
180        }
181
182        if let Some(ref validator) = self.accessibility {
183            if validator.supports(language) {
184                result.merge(validator.validate_parsed(code, file)?);
185            }
186        }
187
188        if let Some(ref validator) = self.security {
189            if validator.supports(language) {
190                result.merge(validator.validate_parsed(code, file)?);
191            }
192        }
193
194        if let Some(ref validator) = self.performance {
195            if validator.supports(language) {
196                result.merge(validator.validate_parsed(code, file)?);
197            }
198        }
199
200        if let Some(ref validator) = self.typescript {
201            if validator.supports(language) {
202                result.merge(validator.validate_parsed(code, file)?);
203            }
204        }
205
206        if let Some(ref validator) = self.testing {
207            if validator.supports(language) {
208                result.merge(validator.validate_parsed(code, file)?);
209            }
210        }
211
212        Ok(result)
213    }
214
215    /// Get all enabled validators
216    pub fn validators(&self) -> Vec<&dyn Validator> {
217        let mut validators = Vec::new();
218
219        if let Some(ref v) = self.tailwind {
220            validators.push(v as &dyn Validator);
221        }
222        if let Some(ref v) = self.imports {
223            validators.push(v as &dyn Validator);
224        }
225        if let Some(ref v) = self.rust {
226            validators.push(v as &dyn Validator);
227        }
228        if let Some(ref v) = self.react {
229            validators.push(v as &dyn Validator);
230        }
231        if let Some(ref v) = self.css {
232            validators.push(v as &dyn Validator);
233        }
234        if let Some(ref v) = self.components {
235            validators.push(v as &dyn Validator);
236        }
237        if let Some(ref v) = self.accessibility {
238            validators.push(v as &dyn Validator);
239        }
240        if let Some(ref v) = self.security {
241            validators.push(v as &dyn Validator);
242        }
243        if let Some(ref v) = self.performance {
244            validators.push(v as &dyn Validator);
245        }
246        if let Some(ref v) = self.typescript {
247            validators.push(v as &dyn Validator);
248        }
249        if let Some(ref v) = self.testing {
250            validators.push(v as &dyn Validator);
251        }
252
253        validators
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_validators_default() {
263        let validators = Validators::new();
264        assert!(validators.validators().is_empty());
265    }
266
267    #[test]
268    fn test_validators_builder() {
269        let validators = Validators::new()
270            .with_tailwind(TailwindValidator::default_config())
271            .with_imports(ImportValidator::default_config())
272            .with_rust(RustValidator::default_config());
273
274        assert_eq!(validators.validators().len(), 3);
275    }
276
277    #[test]
278    fn test_validators_validate_tsx() {
279        let validators = Validators::new()
280            .with_tailwind(TailwindValidator::default_config())
281            .with_react(ReactValidator::default_config());
282
283        let code = ParsedCode::Generic(r#"
284            function Button() {
285                return <button className="flex">Click</button>;
286            }
287        "#.to_string());
288
289        let result = validators.validate(&code, Path::new("test.tsx")).unwrap();
290        assert!(result.passed);
291        assert_eq!(result.files_checked, 1);
292    }
293
294    #[test]
295    fn test_validators_validate_rust() {
296        let validators = Validators::new()
297            .with_rust(RustValidator::default_config());
298
299        let code = ParsedCode::Generic(r#"
300            fn main() {
301                println!("Hello");
302            }
303        "#.to_string());
304
305        let result = validators.validate(&code, Path::new("test.rs")).unwrap();
306        assert!(result.passed);
307    }
308
309    #[test]
310    fn test_validators_empty() {
311        let validators = Validators::new();
312        let code = ParsedCode::Generic("const x = 5;".to_string());
313
314        let result = validators.validate(&code, Path::new("test.js")).unwrap();
315        assert!(result.passed);
316    }
317
318    #[test]
319    fn test_validators_language_filtering() {
320        let validators = Validators::new()
321            .with_tailwind(TailwindValidator::default_config())
322            .with_rust(RustValidator::default_config());
323
324        let code = ParsedCode::Generic("const x = 5;".to_string());
325
326        // For JS, only tailwind should run
327        let result = validators.validate(&code, Path::new("test.js")).unwrap();
328        assert!(result.passed);
329    }
330
331    #[test]
332    fn test_validators_merge_results() {
333        let validators = Validators::new()
334            .with_tailwind(TailwindValidator::default_config())
335            .with_react(ReactValidator::default_config());
336
337        let code = ParsedCode::Generic(r#"
338            class Button extends React.Component {
339                render() {
340                    return <button className="invalid-class">Click</button>;
341                }
342            }
343        "#.to_string());
344
345        let result = validators.validate(&code, Path::new("test.tsx")).unwrap();
346        // Should have issues from both validators
347        assert!(!result.passed || result.issues.len() >= 1);
348    }
349
350    #[test]
351    fn test_validator_trait_bounds() {
352        // Test that Validator trait is object-safe
353        let validator: &dyn Validator = &TailwindValidator::default_config();
354        assert_eq!(validator.name(), "Tailwind");
355        assert!(validator.supports(Language::JavaScript));
356    }
357}