Skip to main content

faf_rust_sdk/
validator.rs

1//! FAF validation
2
3use crate::parser::FafFile;
4
5/// Validation result
6#[derive(Debug, Clone)]
7pub struct ValidationResult {
8    /// True if no errors
9    pub valid: bool,
10    /// Critical errors
11    pub errors: Vec<String>,
12    /// Non-critical warnings
13    pub warnings: Vec<String>,
14    /// Completeness score (0-100)
15    pub score: u8,
16}
17
18/// Validate FAF file structure
19///
20/// # Example
21///
22/// ```rust
23/// use faf_rust_sdk::{parse, validate};
24///
25/// let content = r#"
26/// faf_version: 2.5.0
27/// project:
28///   name: test
29/// "#;
30///
31/// let faf = parse(content).unwrap();
32/// let result = validate(&faf);
33/// assert!(result.valid);
34/// ```
35pub fn validate(faf: &FafFile) -> ValidationResult {
36    let mut errors = Vec::new();
37    let mut warnings = Vec::new();
38
39    // Required fields
40    if faf.data.faf_version.is_empty() {
41        errors.push("Missing faf_version".to_string());
42    }
43
44    if faf.data.project.name.is_empty() {
45        errors.push("Missing project.name".to_string());
46    }
47
48    // Recommended sections
49    if let Some(ic) = &faf.data.instant_context {
50        if ic.what_building.is_none() {
51            warnings.push("Missing instant_context.what_building".to_string());
52        }
53        if ic.tech_stack.is_none() {
54            warnings.push("Missing instant_context.tech_stack".to_string());
55        }
56    } else {
57        warnings.push("Missing instant_context section".to_string());
58    }
59
60    if faf.data.stack.is_none() {
61        warnings.push("Missing stack section".to_string());
62    }
63
64    if faf.data.human_context.is_none() {
65        warnings.push("Missing human_context section".to_string());
66    }
67
68    // Calculate score
69    let score = calculate_score(faf);
70
71    ValidationResult {
72        valid: errors.is_empty(),
73        errors,
74        warnings,
75        score,
76    }
77}
78
79fn calculate_score(faf: &FafFile) -> u8 {
80    let mut score: u8 = 0;
81
82    // Required fields (30 points)
83    if !faf.data.faf_version.is_empty() {
84        score += 10;
85    }
86    if !faf.data.project.name.is_empty() {
87        score += 10;
88    }
89    if faf.data.project.goal.is_some() {
90        score += 10;
91    }
92
93    // Instant context (30 points)
94    if let Some(ic) = &faf.data.instant_context {
95        if ic.what_building.is_some() {
96            score += 10;
97        }
98        if ic.tech_stack.is_some() {
99            score += 10;
100        }
101        if !ic.key_files.is_empty() {
102            score += 10;
103        }
104    }
105
106    // Stack (15 points)
107    if faf.data.stack.is_some() {
108        score += 15;
109    }
110
111    // Human context (15 points)
112    if faf.data.human_context.is_some() {
113        score += 15;
114    }
115
116    // Extras (10 points)
117    if !faf.data.tags.is_empty() {
118        score += 5;
119    }
120    if faf.data.state.is_some() {
121        score += 5;
122    }
123
124    score.min(100)
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::parse;
131
132    #[test]
133    fn test_validate_minimal() {
134        let content = r#"
135faf_version: 2.5.0
136project:
137  name: test
138"#;
139        let faf = parse(content).unwrap();
140        let result = validate(&faf);
141        assert!(result.valid);
142        assert!(result.score >= 20);
143    }
144
145    #[test]
146    fn test_validate_full() {
147        let content = r#"
148faf_version: 2.5.0
149project:
150  name: test
151  goal: Testing
152instant_context:
153  what_building: Test
154  tech_stack: Rust
155  key_files:
156    - main.rs
157stack:
158  backend: Rust
159human_context:
160  who: Developers
161tags:
162  - rust
163state:
164  phase: dev
165"#;
166        let faf = parse(content).unwrap();
167        let result = validate(&faf);
168        assert!(result.valid);
169        assert!(result.score >= 90);
170    }
171}