faf_rust_sdk/
parser.rs

1//! Core FAF parser - optimized for inference workloads
2
3use std::fs;
4use std::path::Path;
5use thiserror::Error;
6
7use crate::types::FafData;
8
9/// FAF parsing errors
10#[derive(Error, Debug)]
11pub enum FafError {
12    #[error("Empty content")]
13    EmptyContent,
14
15    #[error("Invalid YAML: {0}")]
16    YamlError(#[from] serde_yaml::Error),
17
18    #[error("IO error: {0}")]
19    IoError(#[from] std::io::Error),
20
21    #[error("Missing required field: {0}")]
22    MissingField(String),
23}
24
25/// Parsed FAF file with convenient accessors
26#[derive(Debug, Clone)]
27pub struct FafFile {
28    /// Parsed and typed data
29    pub data: FafData,
30    /// Original file path (if loaded from file)
31    pub path: Option<String>,
32}
33
34impl FafFile {
35    /// Get project name
36    #[inline]
37    pub fn project_name(&self) -> &str {
38        &self.data.project.name
39    }
40
41    /// Get AI score as integer (0-100)
42    pub fn score(&self) -> Option<u8> {
43        self.data.ai_score.as_ref().and_then(|s| {
44            s.trim_end_matches('%')
45                .parse()
46                .ok()
47        })
48    }
49
50    /// Get FAF version
51    #[inline]
52    pub fn version(&self) -> &str {
53        &self.data.faf_version
54    }
55
56    /// Get tech stack string
57    pub fn tech_stack(&self) -> Option<&str> {
58        self.data
59            .instant_context
60            .as_ref()
61            .and_then(|ic| ic.tech_stack.as_deref())
62    }
63
64    /// Get what building
65    pub fn what_building(&self) -> Option<&str> {
66        self.data
67            .instant_context
68            .as_ref()
69            .and_then(|ic| ic.what_building.as_deref())
70    }
71
72    /// Get key files
73    pub fn key_files(&self) -> &[String] {
74        self.data
75            .instant_context
76            .as_ref()
77            .map(|ic| ic.key_files.as_slice())
78            .unwrap_or(&[])
79    }
80
81    /// Get project goal
82    pub fn goal(&self) -> Option<&str> {
83        self.data.project.goal.as_deref()
84    }
85
86    /// Check if score indicates high quality (>= 70%)
87    pub fn is_high_quality(&self) -> bool {
88        self.score().map(|s| s >= 70).unwrap_or(false)
89    }
90}
91
92/// Parse FAF content from string
93///
94/// # Example
95///
96/// ```rust
97/// use faf_sdk::parse;
98///
99/// let content = r#"
100/// faf_version: 2.5.0
101/// project:
102///   name: test
103/// "#;
104///
105/// let faf = parse(content).unwrap();
106/// assert_eq!(faf.project_name(), "test");
107/// ```
108pub fn parse(content: &str) -> Result<FafFile, FafError> {
109    let content = content.trim();
110    if content.is_empty() {
111        return Err(FafError::EmptyContent);
112    }
113
114    let data: FafData = serde_yaml::from_str(content)?;
115
116    Ok(FafFile { data, path: None })
117}
118
119/// Parse FAF from file path
120///
121/// # Example
122///
123/// ```rust,no_run
124/// use faf_sdk::parse_file;
125///
126/// let faf = parse_file("project.faf").unwrap();
127/// println!("Project: {}", faf.project_name());
128/// ```
129pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<FafFile, FafError> {
130    let path_str = path.as_ref().to_string_lossy().to_string();
131    let content = fs::read_to_string(&path)?;
132
133    let mut faf = parse(&content)?;
134    faf.path = Some(path_str);
135
136    Ok(faf)
137}
138
139/// Serialize FAF back to YAML string
140pub fn stringify(faf: &FafFile) -> Result<String, FafError> {
141    Ok(serde_yaml::to_string(&faf.data)?)
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_parse_minimal() {
150        let content = r#"
151faf_version: 2.5.0
152project:
153  name: test-project
154"#;
155        let faf = parse(content).unwrap();
156        assert_eq!(faf.project_name(), "test-project");
157        assert_eq!(faf.version(), "2.5.0");
158    }
159
160    #[test]
161    fn test_parse_with_score() {
162        let content = r#"
163faf_version: 2.5.0
164ai_score: "85%"
165project:
166  name: test
167"#;
168        let faf = parse(content).unwrap();
169        assert_eq!(faf.score(), Some(85));
170    }
171
172    #[test]
173    fn test_parse_full() {
174        let content = r#"
175faf_version: 2.5.0
176ai_score: "90%"
177project:
178  name: full-test
179  goal: Test everything
180instant_context:
181  what_building: Test app
182  tech_stack: Rust, Python
183  key_files:
184    - src/main.rs
185    - src/lib.rs
186stack:
187  backend: Rust
188  database: PostgreSQL
189"#;
190        let faf = parse(content).unwrap();
191        assert_eq!(faf.project_name(), "full-test");
192        assert_eq!(faf.tech_stack(), Some("Rust, Python"));
193        assert_eq!(faf.key_files().len(), 2);
194        assert!(faf.is_high_quality());
195    }
196
197    #[test]
198    fn test_empty_content() {
199        let result = parse("");
200        assert!(matches!(result, Err(FafError::EmptyContent)));
201    }
202
203    #[test]
204    fn test_invalid_yaml() {
205        let result = parse("invalid: [unclosed");
206        assert!(matches!(result, Err(FafError::YamlError(_))));
207    }
208}