1use crate::error::{LearningError, Result};
7use crate::models::Rule;
8use regex::Regex;
9use serde_json::Value;
10
11#[derive(Debug, Clone)]
13pub struct RuleValidator {
14 pattern_cache: std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, Regex>>>,
16}
17
18impl RuleValidator {
19 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 pub fn validate(&self, rule: &Rule) -> Result<()> {
36 self.validate_structure(rule)?;
38
39 self.validate_pattern_syntax(&rule.pattern)?;
41
42 self.validate_action_syntax(&rule.action)?;
44
45 self.validate_metadata(&rule.metadata)?;
47
48 self.validate_confidence(rule.confidence)?;
50
51 self.validate_success_rate(rule.success_rate)?;
53
54 Ok(())
55 }
56
57 fn validate_structure(&self, rule: &Rule) -> Result<()> {
59 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 if rule.version == 0 {
80 return Err(LearningError::RuleValidationFailed(
81 "Rule version must be greater than 0".to_string(),
82 ));
83 }
84
85 Ok(())
88 }
89
90 fn validate_pattern_syntax(&self, pattern: &str) -> Result<()> {
92 if pattern.is_empty() {
94 return Err(LearningError::RuleValidationFailed(
95 "Pattern cannot be empty".to_string(),
96 ));
97 }
98
99 if pattern.len() > 10000 {
101 return Err(LearningError::RuleValidationFailed(
102 "Pattern exceeds maximum length of 10000 characters".to_string(),
103 ));
104 }
105
106 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 fn validate_action_syntax(&self, action: &str) -> Result<()> {
121 if action.is_empty() {
123 return Err(LearningError::RuleValidationFailed(
124 "Action cannot be empty".to_string(),
125 ));
126 }
127
128 if action.len() > 50000 {
130 return Err(LearningError::RuleValidationFailed(
131 "Action exceeds maximum length of 50000 characters".to_string(),
132 ));
133 }
134
135 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 fn validate_metadata(&self, metadata: &Value) -> Result<()> {
150 if !metadata.is_object() {
152 return Err(LearningError::RuleValidationFailed(
153 "Metadata must be a JSON object".to_string(),
154 ));
155 }
156
157 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 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 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 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 pub fn check_conflicts(&self, new_rule: &Rule, existing_rules: &[Rule]) -> Result<()> {
227 for existing in existing_rules {
228 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 pub fn validate_with_report(&self, rule: &Rule) -> ValidationReport {
242 let mut report = ValidationReport::new();
243
244 if let Err(e) = self.validate_structure(rule) {
246 report.add_error("structure", e.to_string());
247 }
248
249 if let Err(e) = self.validate_pattern_syntax(&rule.pattern) {
251 report.add_error("pattern", e.to_string());
252 }
253
254 if let Err(e) = self.validate_action_syntax(&rule.action) {
256 report.add_error("action", e.to_string());
257 }
258
259 if let Err(e) = self.validate_metadata(&rule.metadata) {
261 report.add_error("metadata", e.to_string());
262 }
263
264 if let Err(e) = self.validate_confidence(rule.confidence) {
266 report.add_error("confidence", e.to_string());
267 }
268
269 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#[derive(Debug, Clone)]
286pub struct ValidationReport {
287 errors: std::collections::HashMap<String, Vec<String>>,
289 warnings: std::collections::HashMap<String, Vec<String>>,
291}
292
293impl ValidationReport {
294 pub fn new() -> Self {
296 Self {
297 errors: std::collections::HashMap::new(),
298 warnings: std::collections::HashMap::new(),
299 }
300 }
301
302 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 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 pub fn has_errors(&self) -> bool {
320 !self.errors.is_empty()
321 }
322
323 pub fn has_warnings(&self) -> bool {
325 !self.warnings.is_empty()
326 }
327
328 pub fn errors(&self) -> &std::collections::HashMap<String, Vec<String>> {
330 &self.errors
331 }
332
333 pub fn warnings(&self) -> &std::collections::HashMap<String, Vec<String>> {
335 &self.warnings
336 }
337
338 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}