use std::fs;
use std::path::Path;
use thiserror::Error;
use crate::types::FafData;
#[derive(Error, Debug)]
pub enum FafError {
#[error("Empty content")]
EmptyContent,
#[error("Invalid YAML: {0}")]
YamlError(#[from] serde_yaml_ng::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Missing required field: {0}")]
MissingField(String),
}
#[derive(Debug, Clone)]
pub struct FafFile {
pub data: FafData,
pub path: Option<String>,
}
impl FafFile {
#[inline]
pub fn project_name(&self) -> &str {
&self.data.project.name
}
pub fn score(&self) -> Option<u8> {
self.data
.ai_score
.as_ref()
.and_then(|s| s.trim_end_matches('%').parse().ok())
}
#[inline]
pub fn version(&self) -> &str {
&self.data.faf_version
}
pub fn tech_stack(&self) -> Option<&str> {
self.data
.instant_context
.as_ref()
.and_then(|ic| ic.tech_stack.as_deref())
}
pub fn what_building(&self) -> Option<&str> {
self.data
.instant_context
.as_ref()
.and_then(|ic| ic.what_building.as_deref())
}
pub fn key_files(&self) -> &[String] {
self.data
.instant_context
.as_ref()
.map(|ic| ic.key_files.as_slice())
.unwrap_or(&[])
}
pub fn goal(&self) -> Option<&str> {
self.data.project.goal.as_deref()
}
pub fn is_high_quality(&self) -> bool {
self.score().map(|s| s >= 70).unwrap_or(false)
}
}
pub fn parse(content: &str) -> Result<FafFile, FafError> {
let content = content.trim();
if content.is_empty() {
return Err(FafError::EmptyContent);
}
let data: FafData = serde_yaml_ng::from_str(content)?;
Ok(FafFile { data, path: None })
}
pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<FafFile, FafError> {
let path_str = path.as_ref().to_string_lossy().to_string();
let content = fs::read_to_string(&path)?;
let mut faf = parse(&content)?;
faf.path = Some(path_str);
Ok(faf)
}
pub fn stringify(faf: &FafFile) -> Result<String, FafError> {
Ok(serde_yaml_ng::to_string(&faf.data)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal() {
let content = r#"
faf_version: 2.5.0
project:
name: test-project
"#;
let faf = parse(content).unwrap();
assert_eq!(faf.project_name(), "test-project");
assert_eq!(faf.version(), "2.5.0");
}
#[test]
fn test_parse_with_score() {
let content = r#"
faf_version: 2.5.0
ai_score: "85%"
project:
name: test
"#;
let faf = parse(content).unwrap();
assert_eq!(faf.score(), Some(85));
}
#[test]
fn test_parse_full() {
let content = r#"
faf_version: 2.5.0
ai_score: "90%"
project:
name: full-test
goal: Test everything
instant_context:
what_building: Test app
tech_stack: Rust, Python
key_files:
- src/main.rs
- src/lib.rs
stack:
backend: Rust
database: PostgreSQL
"#;
let faf = parse(content).unwrap();
assert_eq!(faf.project_name(), "full-test");
assert_eq!(faf.tech_stack(), Some("Rust, Python"));
assert_eq!(faf.key_files().len(), 2);
assert!(faf.is_high_quality());
}
#[test]
fn test_empty_content() {
let result = parse("");
assert!(matches!(result, Err(FafError::EmptyContent)));
}
#[test]
fn test_invalid_yaml() {
let result = parse("invalid: [unclosed");
assert!(matches!(result, Err(FafError::YamlError(_))));
}
}