ricecoder_learning/
rule_validator.rs

1/// Rule validation component
2///
3/// Validates rules before storage to ensure they meet syntax, structure,
4/// and consistency requirements.
5
6use crate::error::{LearningError, Result};
7use crate::models::Rule;
8use regex::Regex;
9use serde_json::Value;
10
11/// Validates rules before storage
12#[derive(Debug, Clone)]
13pub struct RuleValidator {
14    /// Cached regex patterns for validation
15    pattern_cache: std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, Regex>>>,
16}
17
18impl RuleValidator {
19    /// Create a new rule validator
20    pub fn new() -> Self {
21        Self {
22            pattern_cache: std::sync::Arc::new(std::sync::Mutex::new(
23                std::collections::HashMap::new(),
24            )),
25        }
26    }
27
28    /// Validate a rule before storage
29    ///
30    /// Performs comprehensive validation including:
31    /// - JSON schema validation
32    /// - Syntax verification
33    /// - Reference checking
34    /// - Conflict detection
35    pub fn validate(&self, rule: &Rule) -> Result<()> {
36        // Validate basic structure
37        self.validate_structure(rule)?;
38
39        // Validate pattern syntax
40        self.validate_pattern_syntax(&rule.pattern)?;
41
42        // Validate action syntax
43        self.validate_action_syntax(&rule.action)?;
44
45        // Validate metadata
46        self.validate_metadata(&rule.metadata)?;
47
48        // Validate confidence score
49        self.validate_confidence(rule.confidence)?;
50
51        // Validate success rate
52        self.validate_success_rate(rule.success_rate)?;
53
54        Ok(())
55    }
56
57    /// Validate the basic structure of a rule
58    fn validate_structure(&self, rule: &Rule) -> Result<()> {
59        // Check required fields
60        if rule.id.is_empty() {
61            return Err(LearningError::RuleValidationFailed(
62                "Rule ID cannot be empty".to_string(),
63            ));
64        }
65
66        if rule.pattern.is_empty() {
67            return Err(LearningError::RuleValidationFailed(
68                "Rule pattern cannot be empty".to_string(),
69            ));
70        }
71
72        if rule.action.is_empty() {
73            return Err(LearningError::RuleValidationFailed(
74                "Rule action cannot be empty".to_string(),
75            ));
76        }
77
78        // Validate version
79        if rule.version == 0 {
80            return Err(LearningError::RuleValidationFailed(
81                "Rule version must be greater than 0".to_string(),
82            ));
83        }
84
85        // Note: usage_count is u64, so it cannot be negative
86
87        Ok(())
88    }
89
90    /// Validate pattern syntax
91    fn validate_pattern_syntax(&self, pattern: &str) -> Result<()> {
92        // Pattern should not be empty
93        if pattern.is_empty() {
94            return Err(LearningError::RuleValidationFailed(
95                "Pattern cannot be empty".to_string(),
96            ));
97        }
98
99        // Pattern should not exceed reasonable length
100        if pattern.len() > 10000 {
101            return Err(LearningError::RuleValidationFailed(
102                "Pattern exceeds maximum length of 10000 characters".to_string(),
103            ));
104        }
105
106        // Try to compile as regex if it looks like one
107        if pattern.starts_with('^') || pattern.contains('*') || pattern.contains('+') {
108            if let Err(e) = self.compile_regex(pattern) {
109                return Err(LearningError::RuleValidationFailed(format!(
110                    "Invalid regex pattern: {}",
111                    e
112                )));
113            }
114        }
115
116        Ok(())
117    }
118
119    /// Validate action syntax
120    fn validate_action_syntax(&self, action: &str) -> Result<()> {
121        // Action should not be empty
122        if action.is_empty() {
123            return Err(LearningError::RuleValidationFailed(
124                "Action cannot be empty".to_string(),
125            ));
126        }
127
128        // Action should not exceed reasonable length
129        if action.len() > 50000 {
130            return Err(LearningError::RuleValidationFailed(
131                "Action exceeds maximum length of 50000 characters".to_string(),
132            ));
133        }
134
135        // Try to parse as JSON if it looks like JSON
136        if action.trim().starts_with('{') || action.trim().starts_with('[') {
137            if let Err(e) = serde_json::from_str::<Value>(action) {
138                return Err(LearningError::RuleValidationFailed(format!(
139                    "Invalid JSON in action: {}",
140                    e
141                )));
142            }
143        }
144
145        Ok(())
146    }
147
148    /// Validate metadata
149    fn validate_metadata(&self, metadata: &Value) -> Result<()> {
150        // Metadata should be an object
151        if !metadata.is_object() {
152            return Err(LearningError::RuleValidationFailed(
153                "Metadata must be a JSON object".to_string(),
154            ));
155        }
156
157        // Metadata should not be too large
158        let metadata_str = metadata.to_string();
159        if metadata_str.len() > 100000 {
160            return Err(LearningError::RuleValidationFailed(
161                "Metadata exceeds maximum size".to_string(),
162            ));
163        }
164
165        Ok(())
166    }
167
168    /// Validate confidence score
169    fn validate_confidence(&self, confidence: f32) -> Result<()> {
170        if !(0.0..=1.0).contains(&confidence) {
171            return Err(LearningError::RuleValidationFailed(
172                "Confidence score must be between 0.0 and 1.0".to_string(),
173            ));
174        }
175
176        if confidence.is_nan() || confidence.is_infinite() {
177            return Err(LearningError::RuleValidationFailed(
178                "Confidence score must be a valid number".to_string(),
179            ));
180        }
181
182        Ok(())
183    }
184
185    /// Validate success rate
186    fn validate_success_rate(&self, success_rate: f32) -> Result<()> {
187        if !(0.0..=1.0).contains(&success_rate) {
188            return Err(LearningError::RuleValidationFailed(
189                "Success rate must be between 0.0 and 1.0".to_string(),
190            ));
191        }
192
193        if success_rate.is_nan() || success_rate.is_infinite() {
194            return Err(LearningError::RuleValidationFailed(
195                "Success rate must be a valid number".to_string(),
196            ));
197        }
198
199        Ok(())
200    }
201
202    /// Compile and cache a regex pattern
203    fn compile_regex(&self, pattern: &str) -> Result<()> {
204        let mut cache = self
205            .pattern_cache
206            .lock()
207            .map_err(|e| LearningError::RuleValidationFailed(format!("Lock error: {}", e)))?;
208
209        if cache.contains_key(pattern) {
210            return Ok(());
211        }
212
213        match Regex::new(pattern) {
214            Ok(regex) => {
215                cache.insert(pattern.to_string(), regex);
216                Ok(())
217            }
218            Err(e) => Err(LearningError::RuleValidationFailed(format!(
219                "Invalid regex: {}",
220                e
221            ))),
222        }
223    }
224
225    /// Validate that a rule doesn't conflict with existing rules
226    pub fn check_conflicts(&self, new_rule: &Rule, existing_rules: &[Rule]) -> Result<()> {
227        for existing in existing_rules {
228            // Rules in the same scope with identical patterns conflict
229            if existing.scope == new_rule.scope && existing.pattern == new_rule.pattern {
230                return Err(LearningError::RuleValidationFailed(format!(
231                    "Rule conflicts with existing rule '{}' in {} scope",
232                    existing.id, existing.scope
233                )));
234            }
235        }
236
237        Ok(())
238    }
239
240    /// Generate a detailed validation report
241    pub fn validate_with_report(&self, rule: &Rule) -> ValidationReport {
242        let mut report = ValidationReport::new();
243
244        // Check structure
245        if let Err(e) = self.validate_structure(rule) {
246            report.add_error("structure", e.to_string());
247        }
248
249        // Check pattern
250        if let Err(e) = self.validate_pattern_syntax(&rule.pattern) {
251            report.add_error("pattern", e.to_string());
252        }
253
254        // Check action
255        if let Err(e) = self.validate_action_syntax(&rule.action) {
256            report.add_error("action", e.to_string());
257        }
258
259        // Check metadata
260        if let Err(e) = self.validate_metadata(&rule.metadata) {
261            report.add_error("metadata", e.to_string());
262        }
263
264        // Check confidence
265        if let Err(e) = self.validate_confidence(rule.confidence) {
266            report.add_error("confidence", e.to_string());
267        }
268
269        // Check success rate
270        if let Err(e) = self.validate_success_rate(rule.success_rate) {
271            report.add_error("success_rate", e.to_string());
272        }
273
274        report
275    }
276}
277
278impl Default for RuleValidator {
279    fn default() -> Self {
280        Self::new()
281    }
282}
283
284/// Detailed validation report
285#[derive(Debug, Clone)]
286pub struct ValidationReport {
287    /// Errors found during validation
288    errors: std::collections::HashMap<String, Vec<String>>,
289    /// Warnings found during validation
290    warnings: std::collections::HashMap<String, Vec<String>>,
291}
292
293impl ValidationReport {
294    /// Create a new validation report
295    pub fn new() -> Self {
296        Self {
297            errors: std::collections::HashMap::new(),
298            warnings: std::collections::HashMap::new(),
299        }
300    }
301
302    /// Add an error to the report
303    pub fn add_error(&mut self, field: &str, message: String) {
304        self.errors
305            .entry(field.to_string())
306            .or_insert_with(Vec::new)
307            .push(message);
308    }
309
310    /// Add a warning to the report
311    pub fn add_warning(&mut self, field: &str, message: String) {
312        self.warnings
313            .entry(field.to_string())
314            .or_insert_with(Vec::new)
315            .push(message);
316    }
317
318    /// Check if the report has any errors
319    pub fn has_errors(&self) -> bool {
320        !self.errors.is_empty()
321    }
322
323    /// Check if the report has any warnings
324    pub fn has_warnings(&self) -> bool {
325        !self.warnings.is_empty()
326    }
327
328    /// Get all errors
329    pub fn errors(&self) -> &std::collections::HashMap<String, Vec<String>> {
330        &self.errors
331    }
332
333    /// Get all warnings
334    pub fn warnings(&self) -> &std::collections::HashMap<String, Vec<String>> {
335        &self.warnings
336    }
337
338    /// Get a formatted error message
339    pub fn error_message(&self) -> String {
340        if self.errors.is_empty() {
341            return String::new();
342        }
343
344        let mut message = String::from("Validation errors:\n");
345        for (field, errors) in &self.errors {
346            message.push_str(&format!("  {}: ", field));
347            message.push_str(&errors.join(", "));
348            message.push('\n');
349        }
350
351        message
352    }
353}
354
355impl Default for ValidationReport {
356    fn default() -> Self {
357        Self::new()
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364    use crate::models::{Rule, RuleScope, RuleSource};
365
366    #[test]
367    fn test_validator_creation() {
368        let validator = RuleValidator::new();
369        assert!(validator.pattern_cache.lock().is_ok());
370    }
371
372    #[test]
373    fn test_validate_valid_rule() {
374        let validator = RuleValidator::new();
375        let rule = Rule::new(
376            RuleScope::Global,
377            "test_pattern".to_string(),
378            "test_action".to_string(),
379            RuleSource::Learned,
380        );
381
382        assert!(validator.validate(&rule).is_ok());
383    }
384
385    #[test]
386    fn test_validate_empty_pattern() {
387        let validator = RuleValidator::new();
388        let mut rule = Rule::new(
389            RuleScope::Global,
390            "test".to_string(),
391            "action".to_string(),
392            RuleSource::Learned,
393        );
394        rule.pattern = String::new();
395
396        assert!(validator.validate(&rule).is_err());
397    }
398
399    #[test]
400    fn test_validate_empty_action() {
401        let validator = RuleValidator::new();
402        let mut rule = Rule::new(
403            RuleScope::Global,
404            "pattern".to_string(),
405            "action".to_string(),
406            RuleSource::Learned,
407        );
408        rule.action = String::new();
409
410        assert!(validator.validate(&rule).is_err());
411    }
412
413    #[test]
414    fn test_validate_invalid_confidence() {
415        let validator = RuleValidator::new();
416        let mut rule = Rule::new(
417            RuleScope::Global,
418            "pattern".to_string(),
419            "action".to_string(),
420            RuleSource::Learned,
421        );
422        rule.confidence = 1.5;
423
424        assert!(validator.validate(&rule).is_err());
425    }
426
427    #[test]
428    fn test_validate_invalid_success_rate() {
429        let validator = RuleValidator::new();
430        let mut rule = Rule::new(
431            RuleScope::Global,
432            "pattern".to_string(),
433            "action".to_string(),
434            RuleSource::Learned,
435        );
436        rule.success_rate = -0.5;
437
438        assert!(validator.validate(&rule).is_err());
439    }
440
441    #[test]
442    fn test_validate_json_action() {
443        let validator = RuleValidator::new();
444        let rule = Rule::new(
445            RuleScope::Global,
446            "pattern".to_string(),
447            r#"{"key": "value"}"#.to_string(),
448            RuleSource::Learned,
449        );
450
451        assert!(validator.validate(&rule).is_ok());
452    }
453
454    #[test]
455    fn test_validate_invalid_json_action() {
456        let validator = RuleValidator::new();
457        let rule = Rule::new(
458            RuleScope::Global,
459            "pattern".to_string(),
460            r#"{"key": invalid}"#.to_string(),
461            RuleSource::Learned,
462        );
463
464        assert!(validator.validate(&rule).is_err());
465    }
466
467    #[test]
468    fn test_check_conflicts() {
469        let validator = RuleValidator::new();
470        let rule1 = Rule::new(
471            RuleScope::Global,
472            "pattern".to_string(),
473            "action1".to_string(),
474            RuleSource::Learned,
475        );
476
477        let rule2 = Rule::new(
478            RuleScope::Global,
479            "pattern".to_string(),
480            "action2".to_string(),
481            RuleSource::Learned,
482        );
483
484        assert!(validator.check_conflicts(&rule2, &[rule1]).is_err());
485    }
486
487    #[test]
488    fn test_check_no_conflicts_different_scope() {
489        let validator = RuleValidator::new();
490        let rule1 = Rule::new(
491            RuleScope::Global,
492            "pattern".to_string(),
493            "action1".to_string(),
494            RuleSource::Learned,
495        );
496
497        let rule2 = Rule::new(
498            RuleScope::Project,
499            "pattern".to_string(),
500            "action2".to_string(),
501            RuleSource::Learned,
502        );
503
504        assert!(validator.check_conflicts(&rule2, &[rule1]).is_ok());
505    }
506
507    #[test]
508    fn test_validation_report() {
509        let mut report = ValidationReport::new();
510        assert!(!report.has_errors());
511
512        report.add_error("field1", "error1".to_string());
513        assert!(report.has_errors());
514
515        report.add_warning("field2", "warning1".to_string());
516        assert!(report.has_warnings());
517
518        let message = report.error_message();
519        assert!(message.contains("field1"));
520        assert!(message.contains("error1"));
521    }
522}