Skip to main content

tldr_core/patterns/
validation.rs

1//! Input validation pattern detection
2//!
3//! Detects validation patterns:
4//! - Pydantic BaseModel usage
5//! - Zod schema definitions
6//! - Guard clauses at function start
7//! - Assert statements
8//! - Type validation (isinstance, typeof)
9
10use super::signals::PatternSignals;
11use crate::types::ValidationPattern;
12
13/// Convert signals to validation pattern
14pub fn signals_to_pattern(
15    signals: &PatternSignals,
16    evidence_limit: usize,
17) -> Option<ValidationPattern> {
18    let validation = &signals.validation;
19
20    if !validation.has_signals() {
21        return None;
22    }
23
24    let confidence = validation.calculate_confidence();
25
26    // Detect frameworks
27    let mut frameworks = Vec::new();
28    if !validation.pydantic_models.is_empty() {
29        frameworks.push("pydantic".to_string());
30    }
31    if !validation.zod_schemas.is_empty() {
32        frameworks.push("zod".to_string());
33    }
34    for (name, _) in &validation.other_validators {
35        frameworks.push(name.clone());
36    }
37    frameworks.sort();
38    frameworks.dedup();
39
40    // Detect patterns
41    let mut patterns = Vec::new();
42    if !validation.pydantic_models.is_empty() || !validation.zod_schemas.is_empty() {
43        patterns.push("schema_validation".to_string());
44    }
45    if !validation.guard_clauses.is_empty() {
46        patterns.push("guard_clauses".to_string());
47    }
48    if !validation.assert_statements.is_empty() {
49        patterns.push("assertions".to_string());
50    }
51    if !validation.type_checks.is_empty() {
52        patterns.push("type_checking".to_string());
53    }
54
55    // Collect evidence (limited)
56    let mut evidence = Vec::new();
57    evidence.extend(
58        validation
59            .pydantic_models
60            .iter()
61            .take(evidence_limit)
62            .cloned(),
63    );
64    evidence.extend(validation.zod_schemas.iter().take(evidence_limit).cloned());
65    evidence.extend(
66        validation
67            .guard_clauses
68            .iter()
69            .take(evidence_limit)
70            .cloned(),
71    );
72    evidence.extend(
73        validation
74            .assert_statements
75            .iter()
76            .take(evidence_limit)
77            .cloned(),
78    );
79    evidence.extend(validation.type_checks.iter().take(evidence_limit).cloned());
80    evidence.truncate(evidence_limit);
81
82    Some(ValidationPattern {
83        confidence,
84        frameworks,
85        patterns,
86        evidence,
87    })
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::types::Evidence;
94
95    #[test]
96    fn test_no_signals_returns_none() {
97        let signals = PatternSignals::default();
98        assert!(signals_to_pattern(&signals, 3).is_none());
99    }
100
101    #[test]
102    fn test_pydantic_detected() {
103        let mut signals = PatternSignals::default();
104        signals.validation.pydantic_models.push(Evidence::new(
105            "models.py",
106            5,
107            "class User(BaseModel):",
108        ));
109
110        let pattern = signals_to_pattern(&signals, 3).unwrap();
111        assert!(pattern.confidence >= 0.5);
112        assert!(pattern.frameworks.contains(&"pydantic".to_string()));
113        assert!(pattern.patterns.contains(&"schema_validation".to_string()));
114    }
115
116    #[test]
117    fn test_zod_detected() {
118        let mut signals = PatternSignals::default();
119        signals.validation.zod_schemas.push(Evidence::new(
120            "schema.ts",
121            10,
122            "const userSchema = z.object({ name: z.string() })",
123        ));
124
125        let pattern = signals_to_pattern(&signals, 3).unwrap();
126        assert!(pattern.frameworks.contains(&"zod".to_string()));
127    }
128
129    #[test]
130    fn test_assert_pattern_detected() {
131        let mut signals = PatternSignals::default();
132        signals.validation.assert_statements.push(Evidence::new(
133            "utils.py",
134            15,
135            "assert x > 0, 'x must be positive'",
136        ));
137
138        let pattern = signals_to_pattern(&signals, 3).unwrap();
139        assert!(pattern.patterns.contains(&"assertions".to_string()));
140    }
141}