fleetflow_atom/
discovery.rs1use crate::error::{FlowError, Result};
6use std::collections::HashSet;
7use std::path::{Path, PathBuf};
8use tracing::{debug, info, warn};
9
10#[derive(Debug, Clone, Default)]
12pub struct DiscoveredFiles {
13 pub root: Option<PathBuf>,
15 pub services: Vec<PathBuf>,
17 pub stages: Vec<PathBuf>,
19 pub variables: Vec<PathBuf>,
21 pub local_override: Option<PathBuf>,
23}
24
25#[tracing::instrument]
31pub fn find_project_root() -> Result<PathBuf> {
32 if let Ok(root) = std::env::var("FLOW_PROJECT_ROOT") {
34 let path = PathBuf::from(&root);
35 debug!(env_root = %root, "Checking FLOW_PROJECT_ROOT");
36 if path.join("flow.kdl").exists() {
37 info!(project_root = %path.display(), "Found project root from environment variable");
38 return Ok(path);
39 }
40 }
41
42 let start_dir = std::env::current_dir()?;
44 let mut current = start_dir.clone();
45 debug!(start_dir = %start_dir.display(), "Searching for project root");
46
47 loop {
48 let flow_file = current.join("flow.kdl");
49 debug!(checking = %current.display(), "Looking for flow.kdl");
50 if flow_file.exists() {
51 info!(project_root = %current.display(), "Found project root");
52 return Ok(current);
53 }
54
55 if !current.pop() {
57 break;
58 }
59 }
60
61 warn!(start_dir = %start_dir.display(), "Project root not found");
62 Err(FlowError::ProjectRootNotFound(start_dir))
63}
64
65#[tracing::instrument(skip(project_root), fields(project_root = %project_root.display()))]
67pub fn discover_files(project_root: &Path) -> Result<DiscoveredFiles> {
68 debug!("Starting file discovery");
69 let mut discovered = DiscoveredFiles::default();
70
71 let root_file = project_root.join("flow.kdl");
73 if root_file.exists() {
74 debug!(file = %root_file.display(), "Found root file");
75 discovered.root = Some(root_file);
76 }
77
78 let services_dir = project_root.join("services");
80 if services_dir.is_dir() {
81 discovered.services = discover_kdl_files(&services_dir)?;
82 info!(
83 service_count = discovered.services.len(),
84 "Discovered service files"
85 );
86 }
87
88 let stages_dir = project_root.join("stages");
90 if stages_dir.is_dir() {
91 discovered.stages = discover_kdl_files(&stages_dir)?;
92 info!(
93 stage_count = discovered.stages.len(),
94 "Discovered stage files"
95 );
96 }
97
98 let variables_dir = project_root.join("variables");
100 if variables_dir.is_dir() {
101 discovered.variables = discover_kdl_files(&variables_dir)?;
102 info!(
103 variable_count = discovered.variables.len(),
104 "Discovered variable files"
105 );
106 }
107
108 let local_override = project_root.join("flow.local.kdl");
110 if local_override.exists() {
111 discovered.local_override = Some(local_override);
112 }
113
114 Ok(discovered)
115}
116
117fn discover_kdl_files(dir: &Path) -> Result<Vec<PathBuf>> {
121 let mut files = Vec::new();
122 let mut visited = HashSet::new();
123
124 visit_dir(dir, &mut files, &mut visited)?;
125
126 files.sort();
128
129 Ok(files)
130}
131
132fn visit_dir(dir: &Path, files: &mut Vec<PathBuf>, visited: &mut HashSet<PathBuf>) -> Result<()> {
134 if !dir.is_dir() {
135 return Ok(());
136 }
137
138 let canonical_dir = dir.canonicalize().map_err(|e| FlowError::DiscoveryError {
140 path: dir.to_path_buf(),
141 message: format!("パスの正規化に失敗: {}", e),
142 })?;
143
144 if visited.contains(&canonical_dir) {
146 warn!(dir = %canonical_dir.display(), "Symlink loop detected, skipping");
147 return Ok(());
148 }
149
150 visited.insert(canonical_dir.clone());
152
153 let entries = std::fs::read_dir(dir).map_err(|e| FlowError::DiscoveryError {
154 path: dir.to_path_buf(),
155 message: format!("ディレクトリの読み込みに失敗: {}", e),
156 })?;
157
158 for entry in entries {
159 let entry = entry.map_err(|e| FlowError::DiscoveryError {
160 path: dir.to_path_buf(),
161 message: format!("ディレクトリエントリの読み込みに失敗: {}", e),
162 })?;
163 let path = entry.path();
164
165 if path.is_dir() {
166 visit_dir(&path, files, visited)?;
168 } else if path.extension().and_then(|s| s.to_str()) == Some("kdl") {
169 files.push(path);
170 }
171 }
172
173 Ok(())
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use std::fs;
180
181 fn create_test_project(base: &Path) -> Result<()> {
182 fs::write(base.join("flow.kdl"), "// root")?;
184
185 fs::create_dir_all(base.join("services"))?;
187 fs::write(base.join("services/api.kdl"), "service \"api\" {}")?;
188 fs::write(
189 base.join("services/postgres.kdl"),
190 "service \"postgres\" {}",
191 )?;
192
193 fs::create_dir_all(base.join("services/backend"))?;
195 fs::write(
196 base.join("services/backend/worker.kdl"),
197 "service \"worker\" {}",
198 )?;
199
200 fs::create_dir_all(base.join("stages"))?;
202 fs::write(base.join("stages/local.kdl"), "stage \"local\" {}")?;
203 fs::write(base.join("stages/prod.kdl"), "stage \"prod\" {}")?;
204
205 fs::create_dir_all(base.join("variables"))?;
207 fs::write(base.join("variables/common.kdl"), "variables {}")?;
208
209 fs::write(base.join("flow.local.kdl"), "// local override")?;
211
212 Ok(())
213 }
214
215 #[test]
216 fn test_discover_files() -> Result<()> {
217 let temp_dir = tempfile::tempdir().unwrap();
218 let project_root = temp_dir.path();
219
220 create_test_project(project_root)?;
221
222 let discovered = discover_files(project_root)?;
223
224 assert!(discovered.root.is_some());
226
227 assert_eq!(discovered.services.len(), 3);
229 assert!(discovered.services[0].ends_with("services/api.kdl"));
230 assert!(discovered.services[1].ends_with("services/backend/worker.kdl"));
231 assert!(discovered.services[2].ends_with("services/postgres.kdl"));
232
233 assert_eq!(discovered.stages.len(), 2);
235 assert!(discovered.stages[0].ends_with("stages/local.kdl"));
236 assert!(discovered.stages[1].ends_with("stages/prod.kdl"));
237
238 assert_eq!(discovered.variables.len(), 1);
240 assert!(discovered.variables[0].ends_with("variables/common.kdl"));
241
242 assert!(discovered.local_override.is_some());
244
245 Ok(())
246 }
247
248 #[test]
249 fn test_discover_files_minimal() -> Result<()> {
250 let temp_dir = tempfile::tempdir().unwrap();
251 let project_root = temp_dir.path();
252
253 fs::write(project_root.join("flow.kdl"), "// root")?;
255
256 let discovered = discover_files(project_root)?;
257
258 assert!(discovered.root.is_some());
259 assert_eq!(discovered.services.len(), 0);
260 assert_eq!(discovered.stages.len(), 0);
261 assert_eq!(discovered.variables.len(), 0);
262 assert!(discovered.local_override.is_none());
263
264 Ok(())
265 }
266
267 #[test]
268 fn test_alphabetical_order() -> Result<()> {
269 let temp_dir = tempfile::tempdir().unwrap();
270 let project_root = temp_dir.path();
271
272 fs::write(project_root.join("flow.kdl"), "// root")?;
273 fs::create_dir_all(project_root.join("services"))?;
274
275 fs::write(project_root.join("services/zebra.kdl"), "")?;
277 fs::write(project_root.join("services/alpha.kdl"), "")?;
278 fs::write(project_root.join("services/beta.kdl"), "")?;
279
280 let discovered = discover_files(project_root)?;
281
282 assert!(discovered.services[0].ends_with("services/alpha.kdl"));
284 assert!(discovered.services[1].ends_with("services/beta.kdl"));
285 assert!(discovered.services[2].ends_with("services/zebra.kdl"));
286
287 Ok(())
288 }
289}