faf_rust_sdk/
discovery.rs1use std::env;
4use std::path::{Path, PathBuf};
5
6const MAX_DEPTH: usize = 10;
8
9const FAF_FILES: &[&str] = &["project.faf", ".faf"];
11
12pub 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 for &filename in FAF_FILES {
45 let candidate = current.join(filename);
46 if candidate.is_file() {
47 return Some(candidate);
48 }
49 }
50
51 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
64pub 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#[derive(Debug)]
88pub enum FindError {
89 NotFound,
91 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 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 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 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 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 let found = find_faf_file(Some(&deep));
210 assert!(found.is_none());
211 }
212}