1use minijinja::value::{Object, Rest};
4use minijinja::{Error, ErrorKind, State, Value};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicUsize, Ordering};
7
8const MAX_TPL_DEPTH: usize = 10;
10
11const TPL_DEPTH_KEY: &str = "__sherpack_tpl_depth";
13
14#[derive(Debug, Default)]
17struct TplDepthCounter(AtomicUsize);
18
19impl Object for TplDepthCounter {
20 fn repr(self: &Arc<Self>) -> minijinja::value::ObjectRepr {
21 minijinja::value::ObjectRepr::Plain
22 }
23}
24
25impl TplDepthCounter {
26 fn increment(&self) -> usize {
27 self.0.fetch_add(1, Ordering::SeqCst) + 1
28 }
29
30 fn decrement(&self) {
31 self.0.fetch_sub(1, Ordering::SeqCst);
32 }
33}
34
35pub fn fail(message: String) -> Result<Value, Error> {
39 Err(Error::new(ErrorKind::InvalidOperation, message))
40}
41
42pub fn dict(args: Vec<Value>) -> Result<Value, Error> {
46 if !args.len().is_multiple_of(2) {
47 return Err(Error::new(
48 ErrorKind::InvalidOperation,
49 "dict requires an even number of arguments (key-value pairs)",
50 ));
51 }
52
53 let mut map = serde_json::Map::new();
54
55 for chunk in args.chunks(2) {
56 let key = chunk[0]
57 .as_str()
58 .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "dict keys must be strings"))?;
59 let value: serde_json::Value = serde_json::to_value(&chunk[1])
60 .map_err(|e| Error::new(ErrorKind::InvalidOperation, e.to_string()))?;
61 map.insert(key.to_string(), value);
62 }
63
64 Ok(Value::from_serialize(serde_json::Value::Object(map)))
65}
66
67pub fn list(args: Vec<Value>) -> Value {
71 Value::from(args)
72}
73
74pub fn get(obj: Value, key: String, default: Option<Value>) -> Value {
78 match obj.get_attr(&key) {
79 Ok(v) if !v.is_undefined() => v,
80 _ => default.unwrap_or(Value::UNDEFINED),
81 }
82}
83
84pub fn set(dict: Value, key: String, val: Value) -> Result<Value, Error> {
88 use minijinja::value::ValueKind;
89
90 match dict.kind() {
91 ValueKind::Map => {
92 let mut result = indexmap::IndexMap::new();
93
94 if let Ok(iter) = dict.try_iter() {
96 for k in iter {
97 if let Some(k_str) = k.as_str()
98 && let Ok(v) = dict.get_item(&k)
99 {
100 result.insert(k_str.to_string(), v);
101 }
102 }
103 }
104
105 result.insert(key, val);
107 Ok(Value::from_iter(result))
108 }
109 _ => Err(Error::new(
110 ErrorKind::InvalidOperation,
111 format!("set requires a dict, got {:?}", dict.kind()),
112 )),
113 }
114}
115
116pub fn unset(dict: Value, key: String) -> Result<Value, Error> {
120 use minijinja::value::ValueKind;
121
122 match dict.kind() {
123 ValueKind::Map => {
124 let mut result = indexmap::IndexMap::new();
125
126 if let Ok(iter) = dict.try_iter() {
127 for k in iter {
128 if let Some(k_str) = k.as_str()
129 && k_str != key
130 && let Ok(v) = dict.get_item(&k)
131 {
132 result.insert(k_str.to_string(), v);
133 }
134 }
135 }
136
137 Ok(Value::from_iter(result))
138 }
139 _ => Err(Error::new(
140 ErrorKind::InvalidOperation,
141 format!("unset requires a dict, got {:?}", dict.kind()),
142 )),
143 }
144}
145
146pub fn dig(dict: Value, keys_and_default: Rest<Value>) -> Result<Value, Error> {
151 let args: &[Value] = &keys_and_default;
152
153 if args.is_empty() {
154 return Err(Error::new(
155 ErrorKind::InvalidOperation,
156 "dig requires at least one key and a default value",
157 ));
158 }
159
160 let (keys, default_slice) = args.split_at(args.len() - 1);
162 let default = default_slice.first().cloned().unwrap_or(Value::UNDEFINED);
163
164 if keys.is_empty() {
165 return Ok(dict);
167 }
168
169 let mut current = dict;
171 for key in keys {
172 match key.as_str() {
173 Some(k) => match current.get_attr(k) {
174 Ok(v) if !v.is_undefined() => current = v,
175 _ => return Ok(default),
176 },
177 None => {
178 if let Some(idx) = key.as_i64() {
180 match current.get_item(&Value::from(idx)) {
181 Ok(v) if !v.is_undefined() => current = v,
182 _ => return Ok(default),
183 }
184 } else {
185 return Ok(default);
186 }
187 }
188 }
189 }
190
191 Ok(current)
192}
193
194pub fn coalesce(args: Vec<Value>) -> Value {
198 for arg in args {
199 if !arg.is_undefined() && !arg.is_none() {
200 if let Some(s) = arg.as_str() {
201 if !s.is_empty() {
202 return arg;
203 }
204 } else {
205 return arg;
206 }
207 }
208 }
209 Value::UNDEFINED
210}
211
212pub fn ternary(true_val: Value, false_val: Value, condition: Value) -> Value {
216 if condition.is_true() {
217 true_val
218 } else {
219 false_val
220 }
221}
222
223pub fn uuidv4() -> String {
227 use std::time::{SystemTime, UNIX_EPOCH};
229
230 let timestamp = SystemTime::now()
231 .duration_since(UNIX_EPOCH)
232 .unwrap_or_default()
233 .as_nanos();
234
235 let random_part = timestamp ^ (timestamp >> 32);
236
237 format!(
238 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
239 (random_part & 0xFFFFFFFF) as u32,
240 ((random_part >> 32) & 0xFFFF) as u16,
241 ((random_part >> 48) & 0x0FFF) as u16,
242 (((random_part >> 60) & 0x3F) | 0x80) as u16 | ((random_part & 0xFF) << 8) as u16,
243 (random_part ^ (random_part >> 16)) & 0xFFFFFFFFFFFF
244 )
245}
246
247pub fn tostring(value: Value) -> String {
251 if let Some(s) = value.as_str() {
252 s.to_string()
253 } else {
254 value.to_string()
255 }
256}
257
258pub fn toint(value: Value) -> Result<i64, Error> {
262 if let Some(n) = value.as_i64() {
263 Ok(n)
264 } else if let Some(s) = value.as_str() {
265 s.parse::<i64>().map_err(|_| {
266 Error::new(
267 ErrorKind::InvalidOperation,
268 format!("cannot convert '{}' to int", s),
269 )
270 })
271 } else {
272 Err(Error::new(
273 ErrorKind::InvalidOperation,
274 format!("cannot convert {:?} to int", value),
275 ))
276 }
277}
278
279pub fn tofloat(value: Value) -> Result<f64, Error> {
283 if let Some(n) = value.as_i64() {
284 Ok(n as f64)
285 } else if let Some(s) = value.as_str() {
286 s.parse::<f64>().map_err(|_| {
287 Error::new(
288 ErrorKind::InvalidOperation,
289 format!("cannot convert '{}' to float", s),
290 )
291 })
292 } else {
293 Err(Error::new(
294 ErrorKind::InvalidOperation,
295 format!("cannot convert {:?} to float", value),
296 ))
297 }
298}
299
300pub fn now() -> String {
304 chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()
305}
306
307pub fn printf(format: String, args: Vec<Value>) -> Result<String, Error> {
313 let mut result = String::with_capacity(format.len() + args.len() * 10);
315 let mut chars = format.chars().peekable();
316 let mut arg_idx = 0;
317
318 while let Some(c) = chars.next() {
319 if c != '%' {
320 result.push(c);
321 continue;
322 }
323
324 let format_char = match chars.next() {
326 Some(fc) => fc,
327 None => {
328 result.push('%');
330 break;
331 }
332 };
333
334 if format_char == '%' {
336 result.push('%');
337 continue;
338 }
339
340 if arg_idx >= args.len() {
342 return Err(Error::new(
343 ErrorKind::InvalidOperation,
344 "not enough arguments for format string",
345 ));
346 }
347
348 let arg = &args[arg_idx];
349 match format_char {
350 's' | 'v' => result.push_str(&arg.to_string()),
351 'd' => {
352 if let Some(n) = arg.as_i64() {
353 result.push_str(&n.to_string());
354 } else {
355 result.push_str(&arg.to_string());
356 }
357 }
358 'f' => {
359 if let Some(n) = arg.as_i64() {
360 result.push_str(&(n as f64).to_string());
361 } else {
362 result.push_str(&arg.to_string());
363 }
364 }
365 _ => {
366 result.push_str(&arg.to_string());
368 }
369 }
370 arg_idx += 1;
371 }
372
373 Ok(result)
374}
375
376pub fn tpl(state: &State, template: String, context: Value) -> Result<String, Error> {
401 if !template.contains("{{") && !template.contains("{%") {
403 return Ok(template);
404 }
405
406 let depth = increment_tpl_depth(state)?;
408
409 let result = state.env().render_str(&template, context).map_err(|e| {
411 Error::new(
413 ErrorKind::InvalidOperation,
414 format!(
415 "tpl error (depth {}): {}\n Template: \"{}\"",
416 depth,
417 e,
418 truncate_for_error(&template, 60)
419 ),
420 )
421 });
422
423 decrement_tpl_depth(state);
425
426 result
427}
428
429fn increment_tpl_depth(state: &State) -> Result<usize, Error> {
431 let counter = state.get_or_set_temp_object(TPL_DEPTH_KEY, TplDepthCounter::default);
432 let depth = counter.increment();
433
434 if depth > MAX_TPL_DEPTH {
435 Err(Error::new(
436 ErrorKind::InvalidOperation,
437 format!(
438 "tpl recursion depth {} exceeded maximum {} - possible infinite loop in values. \
439 Check for circular references in template strings.",
440 depth, MAX_TPL_DEPTH
441 ),
442 ))
443 } else {
444 Ok(depth)
445 }
446}
447
448fn decrement_tpl_depth(state: &State) {
450 let counter = state.get_or_set_temp_object(TPL_DEPTH_KEY, TplDepthCounter::default);
451 counter.decrement();
452}
453
454fn truncate_for_error(s: &str, max_len: usize) -> String {
456 if s.len() <= max_len {
457 s.to_string()
458 } else {
459 format!("{}...", &s[..max_len])
460 }
461}
462
463pub fn lookup(api_version: String, kind: String, namespace: String, name: String) -> Value {
513 let _ = (api_version, kind, namespace, name); Value::from_serialize(serde_json::json!({}))
520}
521
522pub fn tpl_ctx(state: &State, template: String) -> Result<String, Error> {
534 if !template.contains("{{") && !template.contains("{%") {
536 return Ok(template);
537 }
538
539 let depth = increment_tpl_depth(state)?;
541
542 let mut ctx = serde_json::Map::new();
544
545 for var in ["values", "release", "pack", "capabilities", "template"] {
547 if let Some(v) = state.lookup(var)
548 && !v.is_undefined()
549 && let Ok(json_val) = serde_json::to_value(&v)
550 {
551 ctx.insert(var.to_string(), json_val);
552 }
553 }
554
555 let context = Value::from_serialize(serde_json::Value::Object(ctx));
556
557 let result = state.env().render_str(&template, context).map_err(|e| {
558 Error::new(
559 ErrorKind::InvalidOperation,
560 format!(
561 "tpl_ctx error (depth {}): {}\n Template: \"{}\"",
562 depth,
563 e,
564 truncate_for_error(&template, 60)
565 ),
566 )
567 });
568
569 decrement_tpl_depth(state);
571
572 result
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578
579 #[test]
580 fn test_dict() {
581 let result = dict(vec![
582 Value::from("key1"),
583 Value::from("value1"),
584 Value::from("key2"),
585 Value::from(42),
586 ])
587 .unwrap();
588
589 assert_eq!(result.get_attr("key1").unwrap().as_str(), Some("value1"));
590 }
591
592 #[test]
593 fn test_list() {
594 let result = list(vec![Value::from("a"), Value::from("b"), Value::from("c")]);
595 assert_eq!(result.len(), Some(3));
596 }
597
598 #[test]
599 fn test_ternary() {
600 assert_eq!(
601 ternary(Value::from("yes"), Value::from("no"), Value::from(true)).as_str(),
602 Some("yes")
603 );
604 assert_eq!(
605 ternary(Value::from("yes"), Value::from("no"), Value::from(false)).as_str(),
606 Some("no")
607 );
608 }
609
610 #[test]
611 fn test_printf() {
612 let result = printf(
613 "Hello %s, you have %d messages".to_string(),
614 vec![Value::from("Alice"), Value::from(5)],
615 )
616 .unwrap();
617 assert_eq!(result, "Hello Alice, you have 5 messages");
618 }
619
620 #[test]
621 fn test_tpl_integration() {
622 use minijinja::Environment;
623
624 let mut env = Environment::new();
626 env.add_function("tpl", super::tpl);
627
628 let template = r#"{{ tpl("Hello {{ name }}!", {"name": "World"}) }}"#;
629 let result = env.render_str(template, ()).unwrap();
630 assert_eq!(result, "Hello World!");
631 }
632
633 #[test]
634 fn test_tpl_no_markers() {
635 use minijinja::Environment;
636
637 let mut env = Environment::new();
639 env.add_function("tpl", super::tpl);
640
641 let template = r#"{{ tpl("plain text", {}) }}"#;
642 let result = env.render_str(template, ()).unwrap();
643 assert_eq!(result, "plain text");
644 }
645
646 #[test]
647 fn test_tpl_complex() {
648 use minijinja::Environment;
649
650 let mut env = Environment::new();
651 env.add_function("tpl", super::tpl);
652
653 let template =
655 r#"{{ tpl("{% if enabled %}yes{% else %}no{% endif %}", {"enabled": true}) }}"#;
656 let result = env.render_str(template, ()).unwrap();
657 assert_eq!(result, "yes");
658 }
659
660 #[test]
661 fn test_tpl_recursion_limit() {
662 use minijinja::Environment;
663
664 let mut env = Environment::new();
665 env.add_function("tpl", super::tpl);
666
667 let template = r#"{{ tpl("{{ tpl(\"{{ tpl(\\\"{{ tpl(\\\\\\\"{{ tpl(\\\\\\\\\\\\\\\"{{ tpl(\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"{{ tpl(\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"{{ tpl(\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"{{ tpl(\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"done\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\", {}) }}\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\", {}) }}\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\", {}) }}\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\", {}) }}\\\\\\\\\\\\\\\", {}) }}\\\\\\\" , {}) }}\\\\\\\", {}) }}\\\", {}) }}\", {}) }}", {}) }}"#;
670
671 let result = env.render_str(template, ());
672
673 assert!(result.is_err());
675 let err = result.unwrap_err();
676 assert!(
677 err.to_string().contains("recursion") || err.to_string().contains("depth"),
678 "Expected recursion error, got: {}",
679 err
680 );
681 }
682
683 #[test]
684 fn test_tpl_nested_valid() {
685 use minijinja::Environment;
686
687 let mut env = Environment::new();
688 env.add_function("tpl", super::tpl);
689
690 let template = r#"{{ tpl("{{ tpl(\"{{ tpl(\\\"level3\\\", {}) }}\", {}) }}", {}) }}"#;
692 let result = env.render_str(template, ()).unwrap();
693 assert_eq!(result, "level3");
694 }
695
696 #[test]
697 fn test_truncate_for_error() {
698 assert_eq!(truncate_for_error("short", 10), "short");
699 assert_eq!(
700 truncate_for_error("this is a longer string", 10),
701 "this is a ..."
702 );
703 }
704
705 #[test]
706 fn test_lookup_returns_empty() {
707 let result = lookup(
709 "v1".to_string(),
710 "Secret".to_string(),
711 "default".to_string(),
712 "my-secret".to_string(),
713 );
714
715 assert!(!result.is_undefined());
717 assert!(result.try_iter().is_ok());
719 }
720
721 #[test]
722 fn test_lookup_in_template() {
723 use minijinja::Environment;
724
725 let mut env = Environment::new();
726 env.add_function("lookup", super::lookup);
727
728 let template = r#"{% set secret = lookup("v1", "Secret", "default", "my-secret") %}{% if secret %}secret exists{% else %}no secret{% endif %}"#;
730 let result = env.render_str(template, ()).unwrap();
731 assert_eq!(result, "no secret");
733 }
734
735 #[test]
736 fn test_lookup_conditional_pattern() {
737 use minijinja::Environment;
738
739 let mut env = Environment::new();
740 env.add_function("lookup", super::lookup);
741
742 let template = r#"{% set secret = lookup("v1", "Secret", "ns", "s") %}{% if secret.data is defined %}{{ secret.data.password }}{% else %}generated{% endif %}"#;
744 let result = env.render_str(template, ()).unwrap();
745 assert_eq!(result, "generated");
746 }
747
748 #[test]
749 fn test_lookup_safe_pattern() {
750 use crate::filters::tojson;
751 use minijinja::Environment;
752
753 let mut env = Environment::new();
754 env.add_function("lookup", super::lookup);
755 env.add_function("get", super::get);
756 env.add_filter("tojson", tojson);
757
758 let template = r#"{% set secret = lookup("v1", "Secret", "ns", "s") %}{{ get(secret, "data", {}) | tojson }}"#;
760 let result = env.render_str(template, ()).unwrap();
761 assert_eq!(result, "{}");
762 }
763
764 #[test]
765 fn test_set_function() {
766 use minijinja::Environment;
767
768 let mut env = Environment::new();
769 env.add_function("set", super::set);
770
771 let template = r#"{% set d = {"a": 1} %}{{ set(d, "b", 2) }}"#;
772 let result = env.render_str(template, ()).unwrap();
773 assert!(result.contains("a") && result.contains("b"));
774 }
775
776 #[test]
777 fn test_unset_function() {
778 use minijinja::Environment;
779
780 let mut env = Environment::new();
781 env.add_function("unset", super::unset);
782
783 let template = r#"{% set d = {"a": 1, "b": 2} %}{{ unset(d, "a") }}"#;
784 let result = env.render_str(template, ()).unwrap();
785 assert!(!result.contains("a") && result.contains("b"));
786 }
787
788 #[test]
789 fn test_dig_function() {
790 use minijinja::Environment;
791
792 let mut env = Environment::new();
793 env.add_function("dig", super::dig);
794
795 let template =
797 r#"{% set d = {"a": {"b": {"c": "found"}}} %}{{ dig(d, "a", "b", "c", "default") }}"#;
798 let result = env.render_str(template, ()).unwrap();
799 assert_eq!(result, "found");
800
801 let template2 = r#"{% set d = {"a": {"b": {}}} %}{{ dig(d, "a", "b", "c", "default") }}"#;
803 let result2 = env.render_str(template2, ()).unwrap();
804 assert_eq!(result2, "default");
805 }
806}