fleetflow_atom/
template.rs1use crate::error::{FlowError, Result};
6use std::collections::HashMap;
7use std::path::Path;
8use tera::{Context, Tera};
9use tracing::{debug, info};
10
11const ESTIMATED_BYTES_PER_FILE: usize = 500;
13
14pub type Variables = HashMap<String, serde_json::Value>;
16
17pub struct TemplateProcessor {
19 tera: Tera,
20 context: Context,
21}
22
23impl TemplateProcessor {
24 pub fn new() -> Self {
26 Self {
27 tera: Tera::default(),
28 context: Context::new(),
29 }
30 }
31
32 pub fn add_variable(&mut self, key: impl Into<String>, value: serde_json::Value) {
34 self.context.insert(key.into(), &value);
35 }
36
37 pub fn add_variables(&mut self, variables: Variables) {
39 for (key, value) in variables {
40 self.context.insert(key, &value);
41 }
42 }
43
44 #[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 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 #[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 pub fn register_env_function(&mut self) {
87 }
90
91 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 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 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 pub fn render_files(&mut self, paths: &[impl AsRef<Path>]) -> Result<String> {
121 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'); }
130
131 Ok(result)
132 }
133}
134
135impl Default for TemplateProcessor {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141pub 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 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 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
171fn 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 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 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 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 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 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}