1use crate::error::Result;
16use serde_json::Value;
17use std::collections::HashMap;
18use std::path::Path;
19use tera::Context;
20
21#[derive(Debug, Clone, Default)]
28pub struct TemplateContext {
29 pub vars: HashMap<String, Value>,
31 pub matrix: HashMap<String, Value>,
33 pub otel: HashMap<String, Value>,
35}
36
37impl TemplateContext {
38 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn with_defaults() -> Self {
54 let mut ctx = Self::new();
55
56 ctx.add_var_with_precedence("svc", "SERVICE_NAME", "clnrm");
58 ctx.add_var_with_precedence("env", "ENV", "ci");
59 ctx.add_var_with_precedence("endpoint", "OTEL_ENDPOINT", "http://localhost:4318");
60 ctx.add_var_with_precedence("exporter", "OTEL_TRACES_EXPORTER", "otlp");
61 ctx.add_var_with_precedence("image", "CLNRM_IMAGE", "registry/clnrm:1.0.0");
62 ctx.add_var_with_precedence("freeze_clock", "FREEZE_CLOCK", "2025-01-01T00:00:00Z");
63 ctx.add_var_with_precedence("token", "OTEL_TOKEN", "");
64
65 ctx
66 }
67
68 pub fn add_var_with_precedence(&mut self, key: &str, env_key: &str, default: &str) {
78 if self.vars.contains_key(key) {
80 return;
81 }
82
83 if let Ok(env_value) = std::env::var(env_key) {
85 self.vars.insert(key.to_string(), Value::String(env_value));
86 return;
87 }
88
89 self.vars
91 .insert(key.to_string(), Value::String(default.to_string()));
92 }
93
94 pub fn with_vars(mut self, vars: HashMap<String, Value>) -> Self {
96 self.vars = vars;
97 self
98 }
99
100 pub fn with_matrix(mut self, matrix: HashMap<String, Value>) -> Self {
102 self.matrix = matrix;
103 self
104 }
105
106 pub fn with_otel(mut self, otel: HashMap<String, Value>) -> Self {
108 self.otel = otel;
109 self
110 }
111
112 pub fn to_tera_context(&self) -> Result<Context> {
117 let mut ctx = Context::new();
118
119 for (key, value) in &self.vars {
121 ctx.insert(key, value);
122 }
123
124 ctx.insert("vars", &self.vars);
126 ctx.insert("matrix", &self.matrix);
127 ctx.insert("otel", &self.otel);
128
129 Ok(ctx)
130 }
131
132 pub fn add_var(&mut self, key: String, value: Value) {
134 self.vars.insert(key, value);
135 }
136
137 pub fn add_matrix_param(&mut self, key: String, value: Value) {
139 self.matrix.insert(key, value);
140 }
141
142 pub fn add_otel_config(&mut self, key: String, value: Value) {
144 self.otel.insert(key, value);
145 }
146
147 pub fn merge_user_vars(&mut self, user_vars: HashMap<String, Value>) {
151 for (key, value) in user_vars {
152 self.vars.insert(key, value);
153 }
154 }
155}
156
157pub struct TemplateContextBuilder {
172 context: TemplateContext,
173}
174
175impl TemplateContextBuilder {
176 pub fn new() -> Self {
178 Self {
179 context: TemplateContext::new(),
180 }
181 }
182
183 pub fn with_defaults() -> Self {
185 Self {
186 context: TemplateContext::with_defaults(),
187 }
188 }
189
190 pub fn var<K: Into<String>, V: Into<Value>>(mut self, key: K, value: V) -> Self {
196 self.context.vars.insert(key.into(), value.into());
197 self
198 }
199
200 pub fn vars<K, V, I>(mut self, vars: I) -> Self
202 where
203 K: Into<String>,
204 V: Into<Value>,
205 I: IntoIterator<Item = (K, V)>,
206 {
207 for (key, value) in vars {
208 self.context.vars.insert(key.into(), value.into());
209 }
210 self
211 }
212
213 pub fn matrix<K: Into<String>, V: Into<Value>>(mut self, key: K, value: V) -> Self {
219 self.context.matrix.insert(key.into(), value.into());
220 self
221 }
222
223 pub fn matrix_params<K, V, I>(mut self, params: I) -> Self
225 where
226 K: Into<String>,
227 V: Into<Value>,
228 I: IntoIterator<Item = (K, V)>,
229 {
230 for (key, value) in params {
231 self.context.matrix.insert(key.into(), value.into());
232 }
233 self
234 }
235
236 pub fn otel<K: Into<String>, V: Into<Value>>(mut self, key: K, value: V) -> Self {
242 self.context.otel.insert(key.into(), value.into());
243 self
244 }
245
246 pub fn otel_config<K, V, I>(mut self, config: I) -> Self
248 where
249 K: Into<String>,
250 V: Into<Value>,
251 I: IntoIterator<Item = (K, V)>,
252 {
253 for (key, value) in config {
254 self.context.otel.insert(key.into(), value.into());
255 }
256 self
257 }
258
259 pub fn var_with_env(mut self, key: &str, env_key: &str, default: &str) -> Self {
263 self.context.add_var_with_precedence(key, env_key, default);
264 self
265 }
266
267 pub fn merge_vars(mut self, user_vars: HashMap<String, Value>) -> Self {
269 self.context.merge_user_vars(user_vars);
270 self
271 }
272
273 pub fn load_vars_from_file<P: AsRef<Path>>(mut self, path: P) -> Result<Self> {
278 let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
279 crate::error::TemplateError::IoError(format!("Failed to read vars file: {}", e))
280 })?;
281
282 let vars: HashMap<String, Value> = serde_json::from_str(&content).map_err(|e| {
283 crate::error::TemplateError::ConfigError(format!("Invalid JSON in vars file: {}", e))
284 })?;
285
286 self.context.merge_user_vars(vars);
287 Ok(self)
288 }
289
290 pub fn load_matrix_from_file<P: AsRef<Path>>(mut self, path: P) -> Result<Self> {
295 let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
296 crate::error::TemplateError::IoError(format!("Failed to read matrix file: {}", e))
297 })?;
298
299 let matrix: HashMap<String, Value> = toml::from_str(&content).map_err(|e| {
300 crate::error::TemplateError::ConfigError(format!("Invalid TOML in matrix file: {}", e))
301 })?;
302
303 self.context.matrix = matrix;
304 Ok(self)
305 }
306
307 pub fn build(self) -> TemplateContext {
309 self.context
310 }
311}
312
313impl Default for TemplateContextBuilder {
314 fn default() -> Self {
315 Self::new()
316 }
317}
318
319pub mod patterns {
321 use super::*;
322
323 pub fn test_scenario() -> TemplateContextBuilder {
325 TemplateContextBuilder::new()
326 .var_with_env("service", "SERVICE_NAME", "test-service")
327 .var_with_env("environment", "ENV", "test")
328 .var("timestamp", Value::String(chrono::Utc::now().to_rfc3339()))
329 }
330
331 pub fn ci_pipeline() -> TemplateContextBuilder {
333 TemplateContextBuilder::new()
334 .var_with_env("service", "SERVICE_NAME", "pipeline-service")
335 .var_with_env("environment", "ENV", "ci")
336 .var_with_env("branch", "BRANCH", "main")
337 .var_with_env("commit", "COMMIT_SHA", "unknown")
338 .var("build_id", Value::String(uuid::Uuid::new_v4().to_string()))
339 }
340
341 pub fn production() -> TemplateContextBuilder {
343 TemplateContextBuilder::new()
344 .var_with_env("service", "SERVICE_NAME", "production-service")
345 .var_with_env("environment", "ENV", "production")
346 .var_with_env("region", "AWS_REGION", "us-east-1")
347 .var_with_env("cluster", "K8S_CLUSTER", "production")
348 }
349
350 pub fn development() -> TemplateContextBuilder {
352 TemplateContextBuilder::new()
353 .var("service", "dev-service")
354 .var("environment", "development")
355 .var("debug", Value::Bool(true))
356 .var("log_level", "debug")
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn test_context_builder_fluent_api() {
366 let context = TemplateContext::builder()
367 .var("service", "my-service")
368 .var("version", "1.0.0")
369 .matrix("browsers", vec!["chrome", "firefox"])
370 .otel("endpoint", "http://localhost:4318")
371 .build();
372
373 assert_eq!(
374 context.vars["service"],
375 Value::String("my-service".to_string())
376 );
377 assert_eq!(context.vars["version"], Value::String("1.0.0".to_string()));
378
379 let browsers = context.matrix["browsers"].as_array().unwrap();
380 assert_eq!(browsers.len(), 2);
381 assert_eq!(browsers[0], Value::String("chrome".to_string()));
382
383 assert_eq!(
384 context.otel["endpoint"],
385 Value::String("http://localhost:4318".to_string())
386 );
387 }
388
389 #[test]
390 fn test_context_builder_patterns() {
391 let context = patterns::test_scenario()
392 .var("test_type", "integration")
393 .build();
394
395 assert!(context.vars.contains_key("service"));
396 assert!(context.vars.contains_key("environment"));
397 assert!(context.vars.contains_key("timestamp"));
398 assert_eq!(
399 context.vars["test_type"],
400 Value::String("integration".to_string())
401 );
402 }
403
404 #[test]
405 fn test_context_with_defaults() {
406 let context = TemplateContext::with_defaults();
407
408 assert!(context.vars.contains_key("svc"));
410 assert!(context.vars.contains_key("env"));
411 assert!(context.vars.contains_key("endpoint"));
412 assert!(context.vars.contains_key("exporter"));
413
414 assert_eq!(context.vars["svc"], Value::String("clnrm".to_string()));
415 assert_eq!(context.vars["env"], Value::String("ci".to_string()));
416 }
417}