1use crate::discovery::{DiscoveredFiles, discover_files, find_project_root};
6use crate::error::{FlowError, Result};
7use crate::model::Flow;
8use crate::parser::parse_kdl_string;
9use crate::template::{TemplateProcessor, Variables, extract_variables};
10use std::path::Path;
11use tracing::{debug, info, instrument};
12
13const ESTIMATED_BYTES_PER_FILE: usize = 500;
15
16#[instrument]
25pub fn load_project() -> Result<Flow> {
26 info!("Starting project load");
27 let project_root = find_project_root()?;
28 load_project_from_root(&project_root)
29}
30
31#[instrument(skip(project_root), fields(project_root = %project_root.display()))]
33pub fn load_project_from_root(project_root: &Path) -> Result<Flow> {
34 debug!("Step 1: Discovering files");
36 let discovered = discover_files(project_root)?;
37
38 debug!("Step 2: Preparing template processor");
40 let mut processor = prepare_template_processor(&discovered)?;
41
42 debug!("Step 3: Expanding templates");
44 let expanded_content = expand_all_files(&discovered, &mut processor)?;
45 info!(
46 content_size = expanded_content.len(),
47 "Template expansion complete"
48 );
49
50 debug!("Step 4: Parsing KDL");
52 let name = project_root
53 .file_name()
54 .and_then(|n| n.to_str())
55 .unwrap_or("unnamed")
56 .to_string();
57 let flow = parse_kdl_string(&expanded_content, name)?;
58 info!(
59 services = flow.services.len(),
60 stages = flow.stages.len(),
61 "Project loaded successfully"
62 );
63
64 Ok(flow)
65}
66
67fn prepare_template_processor(discovered: &DiscoveredFiles) -> Result<TemplateProcessor> {
69 let mut processor = TemplateProcessor::new();
70 let mut all_variables = Variables::new();
71
72 if let Some(root_file) = &discovered.root {
74 let content = std::fs::read_to_string(root_file).map_err(|e| FlowError::IoError {
75 path: root_file.clone(),
76 message: e.to_string(),
77 })?;
78 let vars = extract_variables(&content)?;
79 all_variables.extend(vars);
80 }
81
82 for var_file in &discovered.variables {
84 let content = std::fs::read_to_string(var_file).map_err(|e| FlowError::IoError {
85 path: var_file.clone(),
86 message: e.to_string(),
87 })?;
88 let vars = extract_variables(&content)?;
89 all_variables.extend(vars);
90 }
91
92 processor.add_env_variables();
94
95 processor.add_variables(all_variables);
97
98 Ok(processor)
99}
100
101fn expand_all_files(
103 discovered: &DiscoveredFiles,
104 processor: &mut TemplateProcessor,
105) -> Result<String> {
106 let file_count = discovered.services.len()
108 + discovered.stages.len()
109 + if discovered.root.is_some() { 1 } else { 0 }
110 + if discovered.local_override.is_some() {
111 1
112 } else {
113 0
114 };
115 let estimated_capacity = file_count * ESTIMATED_BYTES_PER_FILE;
116
117 let mut expanded = String::with_capacity(estimated_capacity);
118
119 if let Some(root_file) = &discovered.root {
127 let rendered = processor.render_file(root_file)?;
128 expanded.push_str(&rendered);
129 expanded.push('\n');
130 }
131
132 for service_file in &discovered.services {
134 let rendered = processor.render_file(service_file)?;
135 expanded.push_str(&rendered);
136 expanded.push('\n');
137 }
138
139 for stage_file in &discovered.stages {
141 let rendered = processor.render_file(stage_file)?;
142 expanded.push_str(&rendered);
143 expanded.push('\n');
144 }
145
146 if let Some(local_file) = &discovered.local_override {
148 let rendered = processor.render_file(local_file)?;
149 expanded.push_str(&rendered);
150 expanded.push('\n');
151 }
152
153 Ok(expanded)
154}
155
156pub fn load_project_with_debug(project_root: &Path) -> Result<Flow> {
158 println!("🔍 プロジェクト検出");
159 println!(" ルート: {}", project_root.display());
160
161 let discovered = discover_files(project_root)?;
163
164 if discovered.root.is_some() {
165 println!(" flow.kdl: ✓ 検出");
166 } else {
167 println!(" flow.kdl: ✗ 未検出");
168 }
169
170 println!("\n🔍 ディレクトリスキャン");
171 println!(
172 " services/: {}",
173 if discovered.services.is_empty() {
174 "未検出"
175 } else {
176 "✓ 検出"
177 }
178 );
179 println!(
180 " stages/: {}",
181 if discovered.stages.is_empty() {
182 "未検出"
183 } else {
184 "✓ 検出"
185 }
186 );
187 println!(
188 " variables/: {}",
189 if discovered.variables.is_empty() {
190 "未検出"
191 } else {
192 "✓ 検出"
193 }
194 );
195
196 if !discovered.services.is_empty() {
197 println!("\n📂 ファイル発見 (services/)");
198 for service in &discovered.services {
199 println!(" ✓ {}", service.display());
200 }
201 }
202
203 if !discovered.stages.is_empty() {
204 println!("\n📂 ファイル発見 (stages/)");
205 for stage in &discovered.stages {
206 println!(" ✓ {}", stage.display());
207 }
208 }
209
210 if !discovered.variables.is_empty() {
211 println!("\n📂 ファイル発見 (variables/)");
212 for var in &discovered.variables {
213 println!(" ✓ {}", var.display());
214 }
215 }
216
217 println!("\n📖 変数収集");
218 let mut processor = prepare_template_processor(&discovered)?;
219 println!(" ✓ 完了");
220
221 println!("\n📝 テンプレート展開");
222 let expanded = expand_all_files(&discovered, &mut processor)?;
223 println!(" ✓ 完了 ({}バイト)", expanded.len());
224
225 println!("\n⚙️ KDLパース");
226 let name = project_root
227 .file_name()
228 .and_then(|n| n.to_str())
229 .unwrap_or("unnamed")
230 .to_string();
231 let flow = parse_kdl_string(&expanded, name)?;
232 println!(" サービス: {}個", flow.services.len());
233 println!(" ステージ: {}個", flow.stages.len());
234
235 println!("\n✅ ロード完了\n");
236
237 Ok(flow)
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use std::fs;
244
245 fn create_test_project(base: &Path) -> Result<()> {
246 fs::write(
248 base.join("flow.kdl"),
249 r#"
250variables {
251 app_version "1.0.0"
252 registry "ghcr.io/myorg"
253}
254"#,
255 )?;
256
257 fs::create_dir_all(base.join("services"))?;
259 fs::write(
260 base.join("services/api.kdl"),
261 r#"
262service "api" {
263 image "{{ registry }}/api:{{ app_version }}"
264}
265"#,
266 )?;
267
268 fs::write(
270 base.join("services/postgres.kdl"),
271 r#"
272service "postgres" {
273 version "16"
274}
275"#,
276 )?;
277
278 fs::create_dir_all(base.join("stages"))?;
280 fs::write(
281 base.join("stages/local.kdl"),
282 r#"
283stage "local" {
284 service "api"
285 service "postgres"
286}
287"#,
288 )?;
289
290 Ok(())
291 }
292
293 #[test]
294 fn test_load_project_basic() -> Result<()> {
295 let temp_dir = tempfile::tempdir().unwrap();
296 let project_root = temp_dir.path();
297
298 create_test_project(project_root)?;
299
300 let config = load_project_from_root(project_root)?;
301
302 assert_eq!(config.services.len(), 2);
304 assert!(config.services.contains_key("api"));
305 assert!(config.services.contains_key("postgres"));
306
307 let api = &config.services["api"];
309 assert_eq!(api.image.as_ref().unwrap(), "ghcr.io/myorg/api:1.0.0");
310
311 assert_eq!(config.stages.len(), 1);
313 assert!(config.stages.contains_key("local"));
314
315 let local = &config.stages["local"];
316 assert_eq!(local.services.len(), 2);
317 assert!(local.services.contains(&"api".to_string()));
318 assert!(local.services.contains(&"postgres".to_string()));
319
320 Ok(())
321 }
322
323 #[test]
324 fn test_load_project_with_variables_dir() -> Result<()> {
325 let temp_dir = tempfile::tempdir().unwrap();
326 let project_root = temp_dir.path();
327
328 fs::write(project_root.join("flow.kdl"), "")?;
330
331 fs::create_dir_all(project_root.join("variables"))?;
333 fs::write(
334 project_root.join("variables/common.kdl"),
335 r#"
336variables {
337 image_registry "myregistry"
338 version "2.0.0"
339}
340"#,
341 )?;
342
343 fs::create_dir_all(project_root.join("services"))?;
345 fs::write(
346 project_root.join("services/api.kdl"),
347 r#"
348service "api" {
349 image "{{ image_registry }}/api:{{ version }}"
350}
351"#,
352 )?;
353
354 let config = load_project_from_root(project_root)?;
355
356 let api = &config.services["api"];
357 assert_eq!(api.image.as_ref().unwrap(), "myregistry/api:2.0.0");
358
359 Ok(())
360 }
361
362 #[test]
363 fn test_load_project_with_local_override() -> Result<()> {
364 let temp_dir = tempfile::tempdir().unwrap();
365 let project_root = temp_dir.path();
366
367 fs::write(project_root.join("flow.kdl"), "")?;
369
370 fs::create_dir_all(project_root.join("services"))?;
372 fs::write(
373 project_root.join("services/api.kdl"),
374 r#"
375service "api" {
376 version "15"
377}
378"#,
379 )?;
380
381 fs::write(
383 project_root.join("flow.local.kdl"),
384 r#"
385service "api" {
386 version "16"
387}
388"#,
389 )?;
390
391 let config = load_project_from_root(project_root)?;
392
393 let api = &config.services["api"];
395 assert_eq!(api.version.as_ref().unwrap(), "16");
396
397 Ok(())
398 }
399}