use std::env;
use std::path::{Path, PathBuf};
const MAX_DEPTH: usize = 10;
const FAF_FILES: &[&str] = &["project.faf", ".faf"];
pub fn find_faf_file<P: AsRef<Path>>(start_dir: Option<P>) -> Option<PathBuf> {
let start = match start_dir {
Some(p) => p.as_ref().to_path_buf(),
None => env::current_dir().ok()?,
};
let mut current = start.as_path();
let mut depth = 0;
while depth < MAX_DEPTH {
for &filename in FAF_FILES {
let candidate = current.join(filename);
if candidate.is_file() {
return Some(candidate);
}
}
match current.parent() {
Some(parent) if parent != current => {
current = parent;
depth += 1;
}
_ => break,
}
}
None
}
pub fn find_and_parse<P: AsRef<Path>>(
start_dir: Option<P>,
) -> Result<crate::parser::FafFile, FindError> {
let path = find_faf_file(start_dir).ok_or(FindError::NotFound)?;
crate::parser::parse_file(&path).map_err(FindError::ParseError)
}
#[derive(Debug)]
pub enum FindError {
NotFound,
ParseError(crate::parser::FafError),
}
impl std::fmt::Display for FindError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FindError::NotFound => write!(f, "No FAF file found in directory tree"),
FindError::ParseError(e) => write!(f, "Parse error: {}", e),
}
}
}
impl std::error::Error for FindError {}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_find_in_current_dir() {
let dir = TempDir::new().unwrap();
let faf_path = dir.path().join("project.faf");
fs::write(&faf_path, "faf_version: 2.5.0\nproject:\n name: test").unwrap();
let found = find_faf_file(Some(dir.path()));
assert!(found.is_some());
assert_eq!(found.unwrap(), faf_path);
}
#[test]
fn test_find_in_parent() {
let parent = TempDir::new().unwrap();
let child = parent.path().join("subdir");
fs::create_dir(&child).unwrap();
let faf_path = parent.path().join("project.faf");
fs::write(&faf_path, "faf_version: 2.5.0\nproject:\n name: test").unwrap();
let found = find_faf_file(Some(&child));
assert!(found.is_some());
assert_eq!(found.unwrap(), faf_path);
}
#[test]
fn test_find_legacy_faf() {
let dir = TempDir::new().unwrap();
let faf_path = dir.path().join(".faf");
fs::write(&faf_path, "faf_version: 2.5.0\nproject:\n name: test").unwrap();
let found = find_faf_file(Some(dir.path()));
assert!(found.is_some());
assert_eq!(found.unwrap(), faf_path);
}
#[test]
fn test_modern_takes_priority() {
let dir = TempDir::new().unwrap();
let modern = dir.path().join("project.faf");
let legacy = dir.path().join(".faf");
fs::write(&modern, "faf_version: 2.5.0\nproject:\n name: modern").unwrap();
fs::write(&legacy, "faf_version: 2.5.0\nproject:\n name: legacy").unwrap();
let found = find_faf_file(Some(dir.path()));
assert!(found.is_some());
assert_eq!(found.unwrap(), modern);
}
#[test]
fn test_not_found() {
let dir = TempDir::new().unwrap();
let found = find_faf_file(Some(dir.path()));
assert!(found.is_none());
}
#[test]
fn test_find_and_parse() {
let dir = TempDir::new().unwrap();
let faf_path = dir.path().join("project.faf");
fs::write(
&faf_path,
"faf_version: 2.5.0\nproject:\n name: parsed-test",
)
.unwrap();
let result = find_and_parse(Some(dir.path()));
assert!(result.is_ok());
assert_eq!(result.unwrap().project_name(), "parsed-test");
}
#[test]
fn test_find_and_parse_not_found() {
let dir = TempDir::new().unwrap();
let result = find_and_parse(Some(dir.path()));
assert!(matches!(result, Err(FindError::NotFound)));
}
#[test]
fn test_depth_limit() {
let base = TempDir::new().unwrap();
let mut deep = base.path().to_path_buf();
for i in 0..15 {
deep = deep.join(format!("level{}", i));
}
fs::create_dir_all(&deep).unwrap();
let faf_path = base.path().join("project.faf");
fs::write(&faf_path, "faf_version: 2.5.0\nproject:\n name: test").unwrap();
let found = find_faf_file(Some(&deep));
assert!(found.is_none());
}
}