fleetflow_atom/
loader.rs

1//! 統合ローダー
2//!
3//! ファイル発見、テンプレート展開、パースを統合
4
5use 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
13/// ファイルあたりの推定バイト数(容量事前確保用)
14const ESTIMATED_BYTES_PER_FILE: usize = 500;
15
16/// プロジェクト全体をロードしてFlowを生成
17///
18/// 以下の処理を実行:
19/// 1. プロジェクトルートの検出
20/// 2. ファイルの自動発見
21/// 3. 変数の収集
22/// 4. テンプレート展開
23/// 5. KDLパース
24#[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/// 指定されたルートディレクトリからプロジェクトをロード
32#[instrument(skip(project_root), fields(project_root = %project_root.display()))]
33pub fn load_project_from_root(project_root: &Path) -> Result<Flow> {
34    // 1. ファイル発見
35    debug!("Step 1: Discovering files");
36    let discovered = discover_files(project_root)?;
37
38    // 2. 変数収集とテンプレート準備
39    debug!("Step 2: Preparing template processor");
40    let mut processor = prepare_template_processor(&discovered)?;
41
42    // 3. テンプレート展開
43    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    // 4. KDLパース
51    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
67/// テンプレートプロセッサを準備
68fn prepare_template_processor(discovered: &DiscoveredFiles) -> Result<TemplateProcessor> {
69    let mut processor = TemplateProcessor::new();
70    let mut all_variables = Variables::new();
71
72    // 1. グローバル変数(flow.kdl)
73    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    // 2. variables/**/*.kdl
83    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    // 3. 環境変数を追加
93    processor.add_env_variables();
94
95    // 4. 収集した変数を追加
96    processor.add_variables(all_variables);
97
98    Ok(processor)
99}
100
101/// 全ファイルをテンプレート展開して結合
102fn expand_all_files(
103    discovered: &DiscoveredFiles,
104    processor: &mut TemplateProcessor,
105) -> Result<String> {
106    // ファイル数から概算容量を計算
107    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    // 読み込み順序:
120    // 1. flow.kdl(グローバル設定)
121    // 2. services/**/*.kdl
122    // 3. stages/**/*.kdl
123    // 4. flow.local.kdl(オーバーライド)
124
125    // 1. flow.kdl
126    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    // 2. services/**/*.kdl
133    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    // 3. stages/**/*.kdl
140    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    // 4. flow.local.kdl(オーバーライド)
147    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
156/// デバッグ情報を表示しながらロード
157pub fn load_project_with_debug(project_root: &Path) -> Result<Flow> {
158    println!("🔍 プロジェクト検出");
159    println!("  ルート: {}", project_root.display());
160
161    // ファイル発見
162    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        // flow.kdl
247        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        // services/api.kdl
258        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        // services/postgres.kdl
269        fs::write(
270            base.join("services/postgres.kdl"),
271            r#"
272service "postgres" {
273    version "16"
274}
275"#,
276        )?;
277
278        // stages/local.kdl
279        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        // サービス
303        assert_eq!(config.services.len(), 2);
304        assert!(config.services.contains_key("api"));
305        assert!(config.services.contains_key("postgres"));
306
307        // テンプレート展開の確認
308        let api = &config.services["api"];
309        assert_eq!(api.image.as_ref().unwrap(), "ghcr.io/myorg/api:1.0.0");
310
311        // ステージ
312        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        // flow.kdl
329        fs::write(project_root.join("flow.kdl"), "")?;
330
331        // variables/common.kdl
332        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        // services/api.kdl
344        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        // flow.kdl
368        fs::write(project_root.join("flow.kdl"), "")?;
369
370        // services/api.kdl
371        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        // flow.local.kdl(オーバーライド)
382        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        // flow.local.kdl の定義が優先される
394        let api = &config.services["api"];
395        assert_eq!(api.version.as_ref().unwrap(), "16");
396
397        Ok(())
398    }
399}