Skip to main content

faf_rust_sdk/
discovery.rs

1//! FAF file discovery - find project.faf in directory tree
2
3use std::env;
4use std::path::{Path, PathBuf};
5
6/// Maximum directories to traverse upward
7const MAX_DEPTH: usize = 10;
8
9/// FAF file names to search for (in priority order)
10const FAF_FILES: &[&str] = &["project.faf", ".faf"];
11
12/// Find FAF file starting from given directory, walking up to parents
13///
14/// Searches for `project.faf` (modern) or `.faf` (legacy) in the starting
15/// directory and up to 10 parent directories.
16///
17/// # Example
18///
19/// ```rust,no_run
20/// use faf_rust_sdk::find_faf_file;
21/// use std::path::PathBuf;
22///
23/// // Search from current directory
24/// if let Some(path) = find_faf_file::<PathBuf>(None) {
25///     println!("Found FAF at: {}", path.display());
26/// }
27///
28/// // Search from specific directory
29/// if let Some(path) = find_faf_file(Some("/path/to/project")) {
30///     println!("Found FAF at: {}", path.display());
31/// }
32/// ```
33pub fn find_faf_file<P: AsRef<Path>>(start_dir: Option<P>) -> Option<PathBuf> {
34    let start = match start_dir {
35        Some(p) => p.as_ref().to_path_buf(),
36        None => env::current_dir().ok()?,
37    };
38
39    let mut current = start.as_path();
40    let mut depth = 0;
41
42    while depth < MAX_DEPTH {
43        // Check for FAF files in priority order
44        for &filename in FAF_FILES {
45            let candidate = current.join(filename);
46            if candidate.is_file() {
47                return Some(candidate);
48            }
49        }
50
51        // Move to parent directory
52        match current.parent() {
53            Some(parent) if parent != current => {
54                current = parent;
55                depth += 1;
56            }
57            _ => break,
58        }
59    }
60
61    None
62}
63
64/// Find and parse FAF file in one call
65///
66/// Convenience function that combines `find_faf_file` and `parse_file`.
67///
68/// # Example
69///
70/// ```rust,no_run
71/// use faf_rust_sdk::find_and_parse;
72/// use std::path::PathBuf;
73///
74/// match find_and_parse::<PathBuf>(None) {
75///     Ok(faf) => println!("Project: {}", faf.project_name()),
76///     Err(e) => eprintln!("Error: {}", e),
77/// }
78/// ```
79pub fn find_and_parse<P: AsRef<Path>>(
80    start_dir: Option<P>,
81) -> Result<crate::parser::FafFile, FindError> {
82    let path = find_faf_file(start_dir).ok_or(FindError::NotFound)?;
83    crate::parser::parse_file(&path).map_err(FindError::ParseError)
84}
85
86/// Errors from find operations
87#[derive(Debug)]
88pub enum FindError {
89    /// No FAF file found in directory tree
90    NotFound,
91    /// FAF file found but failed to parse
92    ParseError(crate::parser::FafError),
93}
94
95impl std::fmt::Display for FindError {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            FindError::NotFound => write!(f, "No FAF file found in directory tree"),
99            FindError::ParseError(e) => write!(f, "Parse error: {}", e),
100        }
101    }
102}
103
104impl std::error::Error for FindError {}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use std::fs;
110    use tempfile::TempDir;
111
112    #[test]
113    fn test_find_in_current_dir() {
114        let dir = TempDir::new().unwrap();
115        let faf_path = dir.path().join("project.faf");
116        fs::write(&faf_path, "faf_version: 2.5.0\nproject:\n  name: test").unwrap();
117
118        let found = find_faf_file(Some(dir.path()));
119        assert!(found.is_some());
120        assert_eq!(found.unwrap(), faf_path);
121    }
122
123    #[test]
124    fn test_find_in_parent() {
125        let parent = TempDir::new().unwrap();
126        let child = parent.path().join("subdir");
127        fs::create_dir(&child).unwrap();
128
129        let faf_path = parent.path().join("project.faf");
130        fs::write(&faf_path, "faf_version: 2.5.0\nproject:\n  name: test").unwrap();
131
132        let found = find_faf_file(Some(&child));
133        assert!(found.is_some());
134        assert_eq!(found.unwrap(), faf_path);
135    }
136
137    #[test]
138    fn test_find_legacy_faf() {
139        let dir = TempDir::new().unwrap();
140        let faf_path = dir.path().join(".faf");
141        fs::write(&faf_path, "faf_version: 2.5.0\nproject:\n  name: test").unwrap();
142
143        let found = find_faf_file(Some(dir.path()));
144        assert!(found.is_some());
145        assert_eq!(found.unwrap(), faf_path);
146    }
147
148    #[test]
149    fn test_modern_takes_priority() {
150        let dir = TempDir::new().unwrap();
151
152        // Create both files
153        let modern = dir.path().join("project.faf");
154        let legacy = dir.path().join(".faf");
155        fs::write(&modern, "faf_version: 2.5.0\nproject:\n  name: modern").unwrap();
156        fs::write(&legacy, "faf_version: 2.5.0\nproject:\n  name: legacy").unwrap();
157
158        let found = find_faf_file(Some(dir.path()));
159        assert!(found.is_some());
160        // Modern should be found first
161        assert_eq!(found.unwrap(), modern);
162    }
163
164    #[test]
165    fn test_not_found() {
166        let dir = TempDir::new().unwrap();
167        let found = find_faf_file(Some(dir.path()));
168        assert!(found.is_none());
169    }
170
171    #[test]
172    fn test_find_and_parse() {
173        let dir = TempDir::new().unwrap();
174        let faf_path = dir.path().join("project.faf");
175        fs::write(
176            &faf_path,
177            "faf_version: 2.5.0\nproject:\n  name: parsed-test",
178        )
179        .unwrap();
180
181        let result = find_and_parse(Some(dir.path()));
182        assert!(result.is_ok());
183        assert_eq!(result.unwrap().project_name(), "parsed-test");
184    }
185
186    #[test]
187    fn test_find_and_parse_not_found() {
188        let dir = TempDir::new().unwrap();
189        let result = find_and_parse(Some(dir.path()));
190        assert!(matches!(result, Err(FindError::NotFound)));
191    }
192
193    #[test]
194    fn test_depth_limit() {
195        let base = TempDir::new().unwrap();
196
197        // Create deeply nested directory (deeper than MAX_DEPTH)
198        let mut deep = base.path().to_path_buf();
199        for i in 0..15 {
200            deep = deep.join(format!("level{}", i));
201        }
202        fs::create_dir_all(&deep).unwrap();
203
204        // Put FAF at base
205        let faf_path = base.path().join("project.faf");
206        fs::write(&faf_path, "faf_version: 2.5.0\nproject:\n  name: test").unwrap();
207
208        // Should NOT find it (too deep)
209        let found = find_faf_file(Some(&deep));
210        assert!(found.is_none());
211    }
212}