Skip to main content

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_ng::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
44            .ai_score
45            .as_ref()
46            .and_then(|s| s.trim_end_matches('%').parse().ok())
47    }
48
49    /// Get FAF version
50    #[inline]
51    pub fn version(&self) -> &str {
52        &self.data.faf_version
53    }
54
55    /// Get tech stack string
56    pub fn tech_stack(&self) -> Option<&str> {
57        self.data
58            .instant_context
59            .as_ref()
60            .and_then(|ic| ic.tech_stack.as_deref())
61    }
62
63    /// Get what building
64    pub fn what_building(&self) -> Option<&str> {
65        self.data
66            .instant_context
67            .as_ref()
68            .and_then(|ic| ic.what_building.as_deref())
69    }
70
71    /// Get key files
72    pub fn key_files(&self) -> &[String] {
73        self.data
74            .instant_context
75            .as_ref()
76            .map(|ic| ic.key_files.as_slice())
77            .unwrap_or(&[])
78    }
79
80    /// Get project goal
81    pub fn goal(&self) -> Option<&str> {
82        self.data.project.goal.as_deref()
83    }
84
85    /// Check if score indicates high quality (>= 70%)
86    pub fn is_high_quality(&self) -> bool {
87        self.score().map(|s| s >= 70).unwrap_or(false)
88    }
89}
90
91/// Parse FAF content from string
92///
93/// # Example
94///
95/// ```rust
96/// use faf_rust_sdk::parse;
97///
98/// let content = r#"
99/// faf_version: 2.5.0
100/// project:
101///   name: test
102/// "#;
103///
104/// let faf = parse(content).unwrap();
105/// assert_eq!(faf.project_name(), "test");
106/// ```
107pub fn parse(content: &str) -> Result<FafFile, FafError> {
108    let content = content.trim();
109    if content.is_empty() {
110        return Err(FafError::EmptyContent);
111    }
112
113    let data: FafData = serde_yaml_ng::from_str(content)?;
114
115    Ok(FafFile { data, path: None })
116}
117
118/// Parse FAF from file path
119///
120/// # Example
121///
122/// ```rust,no_run
123/// use faf_rust_sdk::parse_file;
124///
125/// let faf = parse_file("project.faf").unwrap();
126/// println!("Project: {}", faf.project_name());
127/// ```
128pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<FafFile, FafError> {
129    let path_str = path.as_ref().to_string_lossy().to_string();
130    let content = fs::read_to_string(&path)?;
131
132    let mut faf = parse(&content)?;
133    faf.path = Some(path_str);
134
135    Ok(faf)
136}
137
138/// Serialize FAF back to YAML string
139pub fn stringify(faf: &FafFile) -> Result<String, FafError> {
140    Ok(serde_yaml_ng::to_string(&faf.data)?)
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_parse_minimal() {
149        let content = r#"
150faf_version: 2.5.0
151project:
152  name: test-project
153"#;
154        let faf = parse(content).unwrap();
155        assert_eq!(faf.project_name(), "test-project");
156        assert_eq!(faf.version(), "2.5.0");
157    }
158
159    #[test]
160    fn test_parse_with_score() {
161        let content = r#"
162faf_version: 2.5.0
163ai_score: "85%"
164project:
165  name: test
166"#;
167        let faf = parse(content).unwrap();
168        assert_eq!(faf.score(), Some(85));
169    }
170
171    #[test]
172    fn test_parse_full() {
173        let content = r#"
174faf_version: 2.5.0
175ai_score: "90%"
176project:
177  name: full-test
178  goal: Test everything
179instant_context:
180  what_building: Test app
181  tech_stack: Rust, Python
182  key_files:
183    - src/main.rs
184    - src/lib.rs
185stack:
186  backend: Rust
187  database: PostgreSQL
188"#;
189        let faf = parse(content).unwrap();
190        assert_eq!(faf.project_name(), "full-test");
191        assert_eq!(faf.tech_stack(), Some("Rust, Python"));
192        assert_eq!(faf.key_files().len(), 2);
193        assert!(faf.is_high_quality());
194    }
195
196    #[test]
197    fn test_empty_content() {
198        let result = parse("");
199        assert!(matches!(result, Err(FafError::EmptyContent)));
200    }
201
202    #[test]
203    fn test_invalid_yaml() {
204        let result = parse("invalid: [unclosed");
205        assert!(matches!(result, Err(FafError::YamlError(_))));
206    }
207}