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_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 faf.data.instant_context.is_none() {
50        warnings.push("Missing instant_context section".to_string());
51    } else {
52        let ic = faf.data.instant_context.as_ref().unwrap();
53        if ic.what_building.is_none() {
54            warnings.push("Missing instant_context.what_building".to_string());
55        }
56        if ic.tech_stack.is_none() {
57            warnings.push("Missing instant_context.tech_stack".to_string());
58        }
59    }
60
61    if faf.data.stack.is_none() {
62        warnings.push("Missing stack section".to_string());
63    }
64
65    if faf.data.human_context.is_none() {
66        warnings.push("Missing human_context section".to_string());
67    }
68
69    // Calculate score
70    let score = calculate_score(faf);
71
72    ValidationResult {
73        valid: errors.is_empty(),
74        errors,
75        warnings,
76        score,
77    }
78}
79
80fn calculate_score(faf: &FafFile) -> u8 {
81    let mut score: u8 = 0;
82
83    // Required fields (30 points)
84    if !faf.data.faf_version.is_empty() {
85        score += 10;
86    }
87    if !faf.data.project.name.is_empty() {
88        score += 10;
89    }
90    if faf.data.project.goal.is_some() {
91        score += 10;
92    }
93
94    // Instant context (30 points)
95    if let Some(ic) = &faf.data.instant_context {
96        if ic.what_building.is_some() {
97            score += 10;
98        }
99        if ic.tech_stack.is_some() {
100            score += 10;
101        }
102        if !ic.key_files.is_empty() {
103            score += 10;
104        }
105    }
106
107    // Stack (15 points)
108    if faf.data.stack.is_some() {
109        score += 15;
110    }
111
112    // Human context (15 points)
113    if faf.data.human_context.is_some() {
114        score += 15;
115    }
116
117    // Extras (10 points)
118    if !faf.data.tags.is_empty() {
119        score += 5;
120    }
121    if faf.data.state.is_some() {
122        score += 5;
123    }
124
125    score.min(100)
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::parse;
132
133    #[test]
134    fn test_validate_minimal() {
135        let content = r#"
136faf_version: 2.5.0
137project:
138  name: test
139"#;
140        let faf = parse(content).unwrap();
141        let result = validate(&faf);
142        assert!(result.valid);
143        assert!(result.score >= 20);
144    }
145
146    #[test]
147    fn test_validate_full() {
148        let content = r#"
149faf_version: 2.5.0
150project:
151  name: test
152  goal: Testing
153instant_context:
154  what_building: Test
155  tech_stack: Rust
156  key_files:
157    - main.rs
158stack:
159  backend: Rust
160human_context:
161  who: Developers
162tags:
163  - rust
164state:
165  phase: dev
166"#;
167        let faf = parse(content).unwrap();
168        let result = validate(&faf);
169        assert!(result.valid);
170        assert!(result.score >= 90);
171    }
172}