fleetflow_atom/
template.rs

1//! テンプレート展開機能
2//!
3//! Teraを使用してKDLファイルのテンプレート展開を行います。
4
5use crate::error::{FlowError, Result};
6use std::collections::HashMap;
7use std::path::Path;
8use tera::{Context, Tera};
9use tracing::{debug, info};
10
11/// ファイルあたりの推定バイト数(容量事前確保用)
12const ESTIMATED_BYTES_PER_FILE: usize = 500;
13
14/// 変数コンテキスト
15pub type Variables = HashMap<String, serde_json::Value>;
16
17/// テンプレートプロセッサ
18pub struct TemplateProcessor {
19    tera: Tera,
20    context: Context,
21}
22
23impl TemplateProcessor {
24    /// 新しいテンプレートプロセッサを作成
25    pub fn new() -> Self {
26        Self {
27            tera: Tera::default(),
28            context: Context::new(),
29        }
30    }
31
32    /// 変数を追加
33    pub fn add_variable(&mut self, key: impl Into<String>, value: serde_json::Value) {
34        self.context.insert(key.into(), &value);
35    }
36
37    /// 複数の変数を追加
38    pub fn add_variables(&mut self, variables: Variables) {
39        for (key, value) in variables {
40            self.context.insert(key, &value);
41        }
42    }
43
44    /// 環境変数を追加(安全なもののみ)
45    ///
46    /// セキュリティ上の理由から、以下のプレフィックスを持つ環境変数のみを許可:
47    /// - FLOW_*: FleetFlow専用の環境変数
48    /// - CI_*: CI/CD環境の変数
49    /// - APP_*: アプリケーション設定
50    #[tracing::instrument(skip(self))]
51    pub fn add_env_variables(&mut self) {
52        const ALLOWED_PREFIXES: &[&str] = &["FLOW_", "CI_", "APP_"];
53        let mut count = 0;
54
55        for (key, value) in std::env::vars() {
56            // 許可されたプレフィックスを持つ環境変数のみを追加
57            if ALLOWED_PREFIXES
58                .iter()
59                .any(|prefix| key.starts_with(prefix))
60            {
61                debug!(key = %key, "Adding environment variable");
62                self.context.insert(key, &serde_json::Value::String(value));
63                count += 1;
64            }
65        }
66
67        info!(
68            env_var_count = count,
69            "Added filtered environment variables"
70        );
71    }
72
73    /// 環境変数を安全にフィルタリングせずに追加(テスト用)
74    ///
75    /// # Safety
76    /// この関数は信頼できる環境でのみ使用してください。
77    /// 本番環境では `add_env_variables()` を使用することを推奨します。
78    #[cfg(test)]
79    pub fn add_env_variables_unfiltered(&mut self) {
80        for (key, value) in std::env::vars() {
81            self.context.insert(key, &serde_json::Value::String(value));
82        }
83    }
84
85    /// env() 関数を登録
86    pub fn register_env_function(&mut self) {
87        // Teraの組み込み関数として env() を使えるようにする
88        // 実装は後で追加
89    }
90
91    /// 文字列をテンプレートとして展開
92    pub fn render_str(&mut self, template: &str) -> Result<String> {
93        self.tera
94            .render_str(template, &self.context)
95            .map_err(|e| FlowError::TemplateRenderError(format!("テンプレート展開エラー: {}", e)))
96    }
97
98    /// ファイルを読み込んでテンプレート展開
99    pub fn render_file(&mut self, path: &Path) -> Result<String> {
100        let content = std::fs::read_to_string(path).map_err(|e| FlowError::IoError {
101            path: path.to_path_buf(),
102            message: e.to_string(),
103        })?;
104
105        self.render_str(&content).map_err(|e| {
106            // TemplateRenderErrorをより詳細なTemplateErrorに変換
107            if let FlowError::TemplateRenderError(msg) = e {
108                FlowError::TemplateError {
109                    file: path.to_path_buf(),
110                    line: None,
111                    message: msg,
112                }
113            } else {
114                e
115            }
116        })
117    }
118
119    /// 複数のファイルを順に展開して結合
120    pub fn render_files(&mut self, paths: &[impl AsRef<Path>]) -> Result<String> {
121        // ファイル数から概算容量を計算
122        let estimated_capacity = paths.len() * ESTIMATED_BYTES_PER_FILE;
123        let mut result = String::with_capacity(estimated_capacity);
124
125        for path in paths {
126            let rendered = self.render_file(path.as_ref())?;
127            result.push_str(&rendered);
128            result.push('\n'); // ファイル間の区切り
129        }
130
131        Ok(result)
132    }
133}
134
135impl Default for TemplateProcessor {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141/// KDLファイルから変数定義を抽出
142///
143/// variables { ... } ブロックを探してHashMapに変換
144pub fn extract_variables(kdl_content: &str) -> Result<Variables> {
145    let doc: kdl::KdlDocument = kdl_content
146        .parse()
147        .map_err(|e| FlowError::InvalidConfig(format!("KDL パースエラー: {}", e)))?;
148
149    let mut variables = HashMap::new();
150
151    // variables ノードを探す
152    for node in doc.nodes() {
153        if node.name().value() == "variables" {
154            if let Some(children) = node.children() {
155                for var_node in children.nodes() {
156                    let key = var_node.name().value().to_string();
157
158                    // 最初のエントリから値を取得
159                    if let Some(entry) = var_node.entries().first() {
160                        let value = kdl_value_to_json(entry.value());
161                        variables.insert(key, value);
162                    }
163                }
164            }
165        }
166    }
167
168    Ok(variables)
169}
170
171/// KDL値をJSON値に変換
172fn kdl_value_to_json(value: &kdl::KdlValue) -> serde_json::Value {
173    if let Some(s) = value.as_string() {
174        serde_json::Value::String(s.to_string())
175    } else if let Some(i) = value.as_i64() {
176        serde_json::Value::Number(i.into())
177    } else if let Some(f) = value.as_f64() {
178        serde_json::Number::from_f64(f)
179            .map(serde_json::Value::Number)
180            .unwrap_or(serde_json::Value::Null)
181    } else if let Some(b) = value.as_bool() {
182        serde_json::Value::Bool(b)
183    } else {
184        serde_json::Value::Null
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_simple_variable_expansion() {
194        let mut processor = TemplateProcessor::new();
195        processor.add_variable("name", serde_json::Value::String("world".to_string()));
196
197        let template = "Hello {{ name }}!";
198        let result = processor.render_str(template).unwrap();
199
200        assert_eq!(result, "Hello world!");
201    }
202
203    #[test]
204    fn test_nested_variables() {
205        let mut processor = TemplateProcessor::new();
206        processor.add_variable("project", serde_json::Value::String("myapp".to_string()));
207        processor.add_variable("env", serde_json::Value::String("prod".to_string()));
208
209        let template = r#"image "{{ project }}:{{ env }}""#;
210        let result = processor.render_str(template).unwrap();
211
212        assert_eq!(result, r#"image "myapp:prod""#);
213    }
214
215    #[test]
216    fn test_filter_lower() {
217        let mut processor = TemplateProcessor::new();
218        processor.add_variable("name", serde_json::Value::String("HELLO".to_string()));
219
220        let template = "{{ name | lower }}";
221        let result = processor.render_str(template).unwrap();
222
223        assert_eq!(result, "hello");
224    }
225
226    #[test]
227    fn test_filter_upper() {
228        let mut processor = TemplateProcessor::new();
229        processor.add_variable("name", serde_json::Value::String("hello".to_string()));
230
231        let template = "{{ name | upper }}";
232        let result = processor.render_str(template).unwrap();
233
234        assert_eq!(result, "HELLO");
235    }
236
237    #[test]
238    fn test_if_condition() {
239        let mut processor = TemplateProcessor::new();
240        processor.add_variable("is_prod", serde_json::Value::Bool(true));
241
242        let template = r#"
243{% if is_prod %}
244replicas 3
245{% else %}
246replicas 1
247{% endif %}
248"#;
249        let result = processor.render_str(template).unwrap();
250
251        assert!(result.contains("replicas 3"));
252        assert!(!result.contains("replicas 1"));
253    }
254
255    #[test]
256    fn test_for_loop() {
257        let mut processor = TemplateProcessor::new();
258        let services = vec!["api", "worker", "scheduler"];
259        processor.add_variable(
260            "services",
261            serde_json::Value::Array(
262                services
263                    .iter()
264                    .map(|s| serde_json::Value::String(s.to_string()))
265                    .collect(),
266            ),
267        );
268
269        let template = r#"
270{% for service in services %}
271service "{{ service }}"
272{% endfor %}
273"#;
274        let result = processor.render_str(template).unwrap();
275
276        assert!(result.contains(r#"service "api""#));
277        assert!(result.contains(r#"service "worker""#));
278        assert!(result.contains(r#"service "scheduler""#));
279    }
280
281    #[test]
282    fn test_extract_variables() {
283        let kdl = r#"
284variables {
285    app_version "1.0.0"
286    port 8080
287    debug true
288}
289"#;
290
291        let vars = extract_variables(kdl).unwrap();
292
293        assert_eq!(vars.get("app_version").unwrap(), "1.0.0");
294        assert_eq!(vars.get("port").unwrap(), 8080);
295        assert_eq!(vars.get("debug").unwrap(), true);
296    }
297
298    #[test]
299    fn test_extract_multiple_variables_blocks() {
300        let kdl = r#"
301variables {
302    name "first"
303}
304
305service "api" {}
306
307variables {
308    name "second"
309}
310"#;
311
312        let vars = extract_variables(kdl).unwrap();
313
314        // 最後の定義が優先される(後勝ち)
315        assert_eq!(vars.get("name").unwrap(), "second");
316    }
317
318    #[test]
319    fn test_undefined_variable_error() {
320        let mut processor = TemplateProcessor::new();
321
322        let template = "Hello {{ undefined }}!";
323        let result = processor.render_str(template);
324
325        assert!(result.is_err());
326    }
327
328    #[test]
329    fn test_env_variables_filtering() {
330        // 環境変数を設定
331        unsafe {
332            std::env::set_var("FLOW_VERSION", "1.0.0");
333            std::env::set_var("CI_PIPELINE_ID", "12345");
334            std::env::set_var("APP_NAME", "myapp");
335            std::env::set_var("SECRET_KEY", "should_not_be_included");
336            std::env::set_var("HOME", "/home/user");
337        }
338
339        let mut processor = TemplateProcessor::new();
340        processor.add_env_variables();
341
342        // 許可されたプレフィックスの変数は展開できる
343        let template1 = "{{ FLOW_VERSION }}";
344        assert_eq!(processor.render_str(template1).unwrap(), "1.0.0");
345
346        let template2 = "{{ CI_PIPELINE_ID }}";
347        assert_eq!(processor.render_str(template2).unwrap(), "12345");
348
349        let template3 = "{{ APP_NAME }}";
350        assert_eq!(processor.render_str(template3).unwrap(), "myapp");
351
352        // 許可されていない変数は展開できない(エラーになる)
353        let template4 = "{{ SECRET_KEY }}";
354        assert!(processor.render_str(template4).is_err());
355
356        let template5 = "{{ HOME }}";
357        assert!(processor.render_str(template5).is_err());
358
359        // クリーンアップ
360        unsafe {
361            std::env::remove_var("FLOW_VERSION");
362            std::env::remove_var("CI_PIPELINE_ID");
363            std::env::remove_var("APP_NAME");
364            std::env::remove_var("SECRET_KEY");
365            std::env::remove_var("HOME");
366        }
367    }
368}