1use crate::error::Result;
16use serde_json::Value;
17use std::collections::HashMap;
18use tera::Context;
19
20#[derive(Debug, Clone, Default)]
27pub struct TemplateContext {
28 pub vars: HashMap<String, Value>,
30 pub matrix: HashMap<String, Value>,
32 pub otel: HashMap<String, Value>,
34}
35
36impl TemplateContext {
37 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn with_defaults() -> Self {
53 let mut ctx = Self::new();
54
55 ctx.add_var_with_precedence("svc", "SERVICE_NAME", "clnrm");
57 ctx.add_var_with_precedence("env", "ENV", "ci");
58 ctx.add_var_with_precedence("endpoint", "OTEL_ENDPOINT", "http://localhost:4318");
59 ctx.add_var_with_precedence("exporter", "OTEL_TRACES_EXPORTER", "otlp");
60 ctx.add_var_with_precedence("image", "CLNRM_IMAGE", "registry/clnrm:1.0.0");
61 ctx.add_var_with_precedence("freeze_clock", "FREEZE_CLOCK", "2025-01-01T00:00:00Z");
62 ctx.add_var_with_precedence("token", "OTEL_TOKEN", "");
63
64 ctx
65 }
66
67 pub fn add_var_with_precedence(&mut self, key: &str, env_key: &str, default: &str) {
77 if self.vars.contains_key(key) {
79 return;
80 }
81
82 if let Ok(env_value) = std::env::var(env_key) {
84 self.vars.insert(key.to_string(), Value::String(env_value));
85 return;
86 }
87
88 self.vars
90 .insert(key.to_string(), Value::String(default.to_string()));
91 }
92
93 pub fn with_vars(mut self, vars: HashMap<String, Value>) -> Self {
95 self.vars = vars;
96 self
97 }
98
99 pub fn with_matrix(mut self, matrix: HashMap<String, Value>) -> Self {
101 self.matrix = matrix;
102 self
103 }
104
105 pub fn with_otel(mut self, otel: HashMap<String, Value>) -> Self {
107 self.otel = otel;
108 self
109 }
110
111 pub fn to_tera_context(&self) -> Result<Context> {
116 let mut ctx = Context::new();
117
118 for (key, value) in &self.vars {
120 ctx.insert(key, value);
121 }
122
123 ctx.insert("vars", &self.vars);
125 ctx.insert("matrix", &self.matrix);
126 ctx.insert("otel", &self.otel);
127
128 Ok(ctx)
129 }
130
131 pub fn add_var(&mut self, key: String, value: Value) {
133 self.vars.insert(key, value);
134 }
135
136 pub fn add_matrix_param(&mut self, key: String, value: Value) {
138 self.matrix.insert(key, value);
139 }
140
141 pub fn add_otel_config(&mut self, key: String, value: Value) {
143 self.otel.insert(key, value);
144 }
145
146 pub fn merge_user_vars(&mut self, user_vars: HashMap<String, Value>) {
150 for (key, value) in user_vars {
151 self.vars.insert(key, value);
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 #![allow(
159 clippy::unwrap_used,
160 clippy::expect_used,
161 clippy::indexing_slicing,
162 clippy::panic
163 )]
164
165 use super::*;
166 use serde_json::json;
167
168 #[test]
169 fn test_context_creation() {
170 let context = TemplateContext::new();
171 assert!(context.vars.is_empty());
172 assert!(context.matrix.is_empty());
173 assert!(context.otel.is_empty());
174 }
175
176 #[test]
177 fn test_context_with_vars() {
178 let mut vars = HashMap::new();
179 vars.insert("key".to_string(), json!("value"));
180
181 let context = TemplateContext::new().with_vars(vars.clone());
182 assert_eq!(context.vars.get("key"), Some(&json!("value")));
183 }
184
185 #[test]
186 fn test_context_with_matrix() {
187 let mut matrix = HashMap::new();
188 matrix.insert("version".to_string(), json!("1.0"));
189
190 let context = TemplateContext::new().with_matrix(matrix.clone());
191 assert_eq!(context.matrix.get("version"), Some(&json!("1.0")));
192 }
193
194 #[test]
195 fn test_context_with_otel() {
196 let mut otel = HashMap::new();
197 otel.insert("enabled".to_string(), json!(true));
198
199 let context = TemplateContext::new().with_otel(otel.clone());
200 assert_eq!(context.otel.get("enabled"), Some(&json!(true)));
201 }
202
203 #[test]
204 fn test_to_tera_context() {
205 let mut context = TemplateContext::new();
206 context.add_var("name".to_string(), json!("test"));
207 context.add_matrix_param("env".to_string(), json!("prod"));
208 context.add_otel_config("trace".to_string(), json!(true));
209
210 let tera_ctx = context.to_tera_context().unwrap();
211 assert!(tera_ctx.get("vars").is_some());
212 assert!(tera_ctx.get("matrix").is_some());
213 assert!(tera_ctx.get("otel").is_some());
214 }
215
216 #[test]
217 fn test_add_methods() {
218 let mut context = TemplateContext::new();
219
220 context.add_var("var1".to_string(), json!("value1"));
221 context.add_matrix_param("param1".to_string(), json!(42));
222 context.add_otel_config("config1".to_string(), json!(false));
223
224 assert_eq!(context.vars.get("var1"), Some(&json!("value1")));
225 assert_eq!(context.matrix.get("param1"), Some(&json!(42)));
226 assert_eq!(context.otel.get("config1"), Some(&json!(false)));
227 }
228
229 #[test]
230 fn test_chaining() {
231 let mut vars = HashMap::new();
232 vars.insert("a".to_string(), json!(1));
233
234 let mut matrix = HashMap::new();
235 matrix.insert("b".to_string(), json!(2));
236
237 let mut otel = HashMap::new();
238 otel.insert("c".to_string(), json!(3));
239
240 let context = TemplateContext::new()
241 .with_vars(vars)
242 .with_matrix(matrix)
243 .with_otel(otel);
244
245 assert_eq!(context.vars.get("a"), Some(&json!(1)));
246 assert_eq!(context.matrix.get("b"), Some(&json!(2)));
247 assert_eq!(context.otel.get("c"), Some(&json!(3)));
248 }
249
250 #[test]
251 fn test_with_defaults_creates_standard_vars() {
252 let context = TemplateContext::with_defaults();
253
254 assert!(context.vars.contains_key("svc"));
256 assert!(context.vars.contains_key("env"));
257 assert!(context.vars.contains_key("endpoint"));
258 assert!(context.vars.contains_key("exporter"));
259 assert!(context.vars.contains_key("image"));
260 assert!(context.vars.contains_key("freeze_clock"));
261 assert!(context.vars.contains_key("token"));
262 }
263
264 #[test]
265 fn test_with_defaults_uses_default_values() {
266 std::env::remove_var("SERVICE_NAME");
268 std::env::remove_var("ENV");
269
270 let context = TemplateContext::with_defaults();
271
272 assert_eq!(context.vars.get("svc").unwrap().as_str().unwrap(), "clnrm");
273 assert_eq!(context.vars.get("env").unwrap().as_str().unwrap(), "ci");
274 assert_eq!(
275 context.vars.get("endpoint").unwrap().as_str().unwrap(),
276 "http://localhost:4318"
277 );
278 }
279
280 #[test]
281 fn test_precedence_env_over_default() {
282 std::env::set_var("SERVICE_NAME", "my-service");
284
285 let context = TemplateContext::with_defaults();
286
287 assert_eq!(
289 context.vars.get("svc").unwrap().as_str().unwrap(),
290 "my-service"
291 );
292
293 std::env::remove_var("SERVICE_NAME");
295 }
296
297 #[test]
298 fn test_precedence_template_var_over_env() {
299 std::env::set_var("SERVICE_NAME", "env-service");
301
302 let mut context = TemplateContext::new();
303 context.add_var("svc".to_string(), json!("template-service"));
305
306 context.add_var_with_precedence("svc", "SERVICE_NAME", "default-service");
308
309 assert_eq!(
311 context.vars.get("svc").unwrap().as_str().unwrap(),
312 "template-service"
313 );
314
315 std::env::remove_var("SERVICE_NAME");
317 }
318
319 #[test]
320 fn test_merge_user_vars() {
321 let mut context = TemplateContext::with_defaults();
322
323 let mut user_vars = HashMap::new();
324 user_vars.insert("svc".to_string(), json!("user-override"));
325 user_vars.insert("custom".to_string(), json!("custom-value"));
326
327 context.merge_user_vars(user_vars);
328
329 assert_eq!(
331 context.vars.get("svc").unwrap().as_str().unwrap(),
332 "user-override"
333 );
334 assert_eq!(
336 context.vars.get("custom").unwrap().as_str().unwrap(),
337 "custom-value"
338 );
339 }
340
341 #[test]
342 fn test_to_tera_context_top_level_injection() {
343 let mut context = TemplateContext::new();
344 context.add_var("name".to_string(), json!("test"));
345
346 let tera_ctx = context.to_tera_context().unwrap();
347
348 assert!(tera_ctx.get("name").is_some());
350 assert!(tera_ctx.get("vars").is_some());
352 }
353
354 #[test]
355 fn test_full_precedence_chain() {
356 std::env::set_var("TEST_VAR_PRECEDENCE", "from-env");
358
359 let mut context = TemplateContext::new();
360
361 context.add_var_with_precedence("test_key", "TEST_VAR_PRECEDENCE", "from-default");
363 assert_eq!(
364 context.vars.get("test_key").unwrap().as_str().unwrap(),
365 "from-env"
366 );
367
368 let mut user_vars = HashMap::new();
370 user_vars.insert("test_key".to_string(), json!("from-user"));
371 context.merge_user_vars(user_vars);
372
373 assert_eq!(
374 context.vars.get("test_key").unwrap().as_str().unwrap(),
375 "from-user"
376 );
377
378 std::env::remove_var("TEST_VAR_PRECEDENCE");
380 }
381}