1use crate::ast::*;
20use crate::type_inference::{InferredType, TypeContext, TypeHeuristics};
21use phf::phf_map;
22
23static FILTER_MAP: phf::Map<&'static str, &'static str> = phf_map! {
28 "toYaml" => "toyaml",
30 "toJson" => "tojson",
31 "toPrettyJson" => "tojson_pretty",
32 "fromYaml" => "fromyaml",
33 "fromJson" => "fromjson",
34
35 "b64enc" => "b64encode",
37 "b64dec" => "b64decode",
38
39 "quote" => "quote",
41 "squote" => "squote",
42
43 "upper" => "upper",
45 "lower" => "lower",
46 "title" => "title",
47 "camelcase" => "camelcase",
48 "snakecase" => "snakecase",
49 "kebabcase" => "kebabcase",
50 "swapcase" => "swapcase",
51
52 "trim" => "trim",
54 "trimPrefix" => "trimprefix",
55 "trimSuffix" => "trimsuffix",
56 "trimAll" => "trim",
57 "trunc" => "trunc",
58 "abbrev" => "trunc",
59 "repeat" => "repeat",
60 "replace" => "replace",
61 "wrap" => "wordwrap",
62 "wrapWith" => "wordwrap",
63
64 "hasPrefix" => "startswith",
66 "hasSuffix" => "endswith",
67
68 "indent" => "indent",
70 "nindent" => "nindent",
71
72 "first" => "first",
74 "last" => "last",
75 "rest" => "list[1:]",
76 "initial" => "list[:-1]",
77 "reverse" => "reverse",
78 "uniq" => "unique",
79 "sortAlpha" => "sort",
80
81 "hasKey" => "haskey",
83 "keys" => "keys",
84 "values" => "values",
85 "merge" => "merge",
86 "mergeOverwrite" => "merge",
87 "deepCopy" => "deepcopy",
88
89 "toString" => "string",
91 "toStrings" => "tostrings",
92 "int" => "int",
93 "int64" => "int",
94 "float64" => "float",
95
96 "required" => "required",
98 "empty" => "empty",
99
100 "sha256sum" => "sha256",
102 "sha1sum" => "sha1",
103 "adler32sum" => "adler32",
104
105 "regexMatch" => "regex_match",
107 "regexFind" => "regex_search",
108 "regexFindAll" => "regex_findall",
109 "regexReplaceAll" => "regex_replace",
110 "regexSplit" => "split",
111};
112
113static NATIVE_OPERATORS: phf::Map<&'static str, &'static str> = phf_map! {
119 "eq" => "==",
121 "ne" => "!=",
122 "lt" => "<",
123 "le" => "<=",
124 "gt" => ">",
125 "ge" => ">=",
126 "and" => "and",
128 "or" => "or",
129 "add" => "+",
131 "sub" => "-",
132 "mul" => "*",
133 "div" => "/",
134 "mod" => "%",
135};
136
137static UNSUPPORTED_FEATURES: phf::Map<&'static str, &'static str> = phf_map! {
139 "genCA" => "Use cert-manager or pre-generated certificates in values",
141 "genSelfSignedCert" => "Use cert-manager or pre-generated certificates",
142 "genSignedCert" => "Use cert-manager for certificate management",
143 "genPrivateKey" => "Use external secret management",
144 "htpasswd" => "Use external secret management or pre-computed values",
145 "derivePassword" => "Use external secret management",
146 "encryptAES" => "Use external secret management",
147 "decryptAES" => "Use external secret management",
148 "randBytes" => "Use external secret management for random data",
149 "randAscii" => "Use external secret management",
151
152 "getHostByName" => "Use explicit IP/hostname in values (GitOps compatible)",
154
155 "Files.Get" => "Embed file content in values.yaml or use ConfigMap",
157 "Files.GetBytes" => "Embed base64 content in values.yaml",
158 "Files.Glob" => "List files explicitly in values.yaml",
159 "Files.Lines" => "Embed content as list in values.yaml",
160 "Files.AsConfig" => "Use native ConfigMap in templates",
161 "Files.AsSecrets" => "Use native Secret in templates",
162
163 "lookup" => "Returns {} in template mode - use explicit values for GitOps",
165};
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum WarningSeverity {
174 Info,
176 Warning,
178 Unsupported,
180}
181
182#[derive(Debug, Clone)]
184pub struct TransformWarning {
185 pub severity: WarningSeverity,
187 pub pattern: String,
189 pub message: String,
191 pub suggestion: Option<String>,
193 pub doc_link: Option<String>,
195}
196
197impl TransformWarning {
198 pub fn info(pattern: &str, message: &str) -> Self {
199 Self {
200 severity: WarningSeverity::Info,
201 pattern: pattern.to_string(),
202 message: message.to_string(),
203 suggestion: None,
204 doc_link: None,
205 }
206 }
207
208 pub fn warning(pattern: &str, message: &str) -> Self {
209 Self {
210 severity: WarningSeverity::Warning,
211 pattern: pattern.to_string(),
212 message: message.to_string(),
213 suggestion: None,
214 doc_link: None,
215 }
216 }
217
218 pub fn unsupported(pattern: &str, alternative: &str) -> Self {
219 Self {
220 severity: WarningSeverity::Unsupported,
221 pattern: pattern.to_string(),
222 message: format!("'{}' is not supported in Sherpack", pattern),
223 suggestion: Some(alternative.to_string()),
224 doc_link: Some("https://sherpack.dev/docs/helm-migration".to_string()),
225 }
226 }
227
228 pub fn with_suggestion(mut self, suggestion: &str) -> Self {
229 self.suggestion = Some(suggestion.to_string());
230 self
231 }
232}
233
234#[derive(Debug, Clone)]
240enum BlockType {
241 If,
242 Range,
243 With,
244 Define,
245}
246
247pub struct Transformer {
249 block_stack: Vec<BlockType>,
251 #[allow(dead_code)]
253 warnings: Vec<TransformWarning>,
254 chart_prefix: Option<String>,
256 context_var: Option<String>,
258 type_context: Option<TypeContext>,
260 secret_counter: std::cell::Cell<usize>,
262}
263
264impl Default for Transformer {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270impl Transformer {
271 pub fn new() -> Self {
272 Self {
273 block_stack: Vec::new(),
274 warnings: Vec::new(),
275 chart_prefix: None,
276 context_var: None,
277 type_context: None,
278 secret_counter: std::cell::Cell::new(0),
279 }
280 }
281
282 pub fn with_chart_prefix(mut self, prefix: &str) -> Self {
284 self.chart_prefix = Some(format!("{}.", prefix));
285 self
286 }
287
288 pub fn with_type_context(mut self, ctx: TypeContext) -> Self {
290 self.type_context = Some(ctx);
291 self
292 }
293
294 pub fn warnings(&self) -> &[TransformWarning] {
296 &self.warnings
297 }
298
299 #[allow(dead_code)]
300 fn add_warning(&mut self, warning: TransformWarning) {
301 self.warnings.push(warning);
302 }
303
304 fn next_secret_name(&self) -> String {
306 let n = self.secret_counter.get();
307 self.secret_counter.set(n + 1);
308 format!("auto-secret-{}", n + 1)
309 }
310
311 pub fn transform(&mut self, template: &Template) -> String {
313 template
314 .elements
315 .iter()
316 .map(|e| self.transform_element(e))
317 .collect()
318 }
319
320 fn transform_element(&mut self, element: &Element) -> String {
321 match element {
322 Element::RawText(text) => text.clone(),
323 Element::Action(action) => self.transform_action(action),
324 }
325 }
326
327 fn transform_action(&mut self, action: &Action) -> String {
328 let trim_left = if action.trim_left { "-" } else { "" };
329 let trim_right = if action.trim_right { "-" } else { "" };
330
331 match &action.body {
332 ActionBody::Comment(text) => {
334 format!("{{# {} #}}", text.trim())
335 }
336
337 ActionBody::If(pipeline) => {
339 self.block_stack.push(BlockType::If);
340 format!(
341 "{{%{} if {} %}}",
342 trim_left,
343 self.transform_pipeline(pipeline)
344 )
345 }
346
347 ActionBody::ElseIf(pipeline) => {
349 format!(
350 "{{%{} elif {} %}}",
351 trim_left,
352 self.transform_pipeline(pipeline)
353 )
354 }
355
356 ActionBody::Else => {
358 format!("{{%{} else %}}", trim_left)
359 }
360
361 ActionBody::End => {
363 let block = self.block_stack.pop();
364 let end_tag = match &block {
365 Some(BlockType::If) => "endif",
366 Some(BlockType::Range) => "endfor",
367 Some(BlockType::With) => "endif",
368 Some(BlockType::Define) => "endmacro",
369 None => "endif",
370 };
371
372 if let Some(BlockType::With) = &block {
374 self.context_var = None;
375 }
376
377 if matches!(block, Some(BlockType::Define)) {
379 format!("{{%{} {} %}}", trim_left, end_tag)
380 } else if trim_right == "-" {
381 format!("{{%{} {} -%}}", trim_left, end_tag)
382 } else {
383 format!("{{%{} {} %}}", trim_left, end_tag)
384 }
385 }
386
387 ActionBody::Range { vars, pipeline } => {
390 let value_var = vars
391 .as_ref()
392 .map(|v| v.value_var.trim_start_matches('$').to_string())
393 .unwrap_or_else(|| "item".to_string());
394
395 let index_var = vars.as_ref().and_then(|v| {
396 v.index_var
397 .as_ref()
398 .map(|i| i.trim_start_matches('$').to_string())
399 });
400
401 self.block_stack.push(BlockType::Range);
402
403 let collection = self.transform_pipeline(pipeline);
404
405 let is_dict = self.is_dict_type(&collection);
407
408 match (&index_var, is_dict) {
409 (Some(key_var), true) => {
411 format!(
412 "{{%{} for {}, {} in {} | dictsort %}}",
413 trim_left, key_var, value_var, collection
414 )
415 }
416 (Some(idx), false) => {
418 format!(
419 "{{%{} for {} in {} %}}{{#- {} = loop.index0 #}}",
420 trim_left, value_var, collection, idx
421 )
422 }
423 (None, _) => {
425 format!("{{%{} for {} in {} %}}", trim_left, value_var, collection)
426 }
427 }
428 }
429
430 ActionBody::With(pipeline) => {
432 let ctx_value = self.transform_pipeline(pipeline);
433 let ctx_var = "_with_ctx".to_string();
434
435 self.block_stack.push(BlockType::With);
436 self.context_var = Some(ctx_var.clone());
437
438 format!(
440 "{{%{} if {} %}}{{%- set {} = {} %}}",
441 trim_left, ctx_value, ctx_var, ctx_value
442 )
443 }
444
445 ActionBody::Define(name) => {
447 let macro_name = self.strip_chart_prefix(name);
448 self.block_stack.push(BlockType::Define);
449 format!("{{%{} macro {}() %}}", trim_left, macro_name)
450 }
451
452 ActionBody::Template { name, .. } => {
454 let macro_name = self.strip_chart_prefix(name);
455 format!("{{{{ {}() }}}}", macro_name)
456 }
457
458 ActionBody::Block { name, .. } => {
460 let block_name = self.strip_chart_prefix(name);
461 self.block_stack.push(BlockType::Define);
462 format!("{{%{} block {} %}}", trim_left, block_name)
463 }
464
465 ActionBody::Pipeline(pipeline) => {
467 if let Some(ref var_name) = pipeline.decl {
469 let clean_var = var_name.trim_start_matches('$');
470 let value = if pipeline.commands.is_empty() {
471 "none".to_string()
472 } else {
473 let pipe_without_decl = Pipeline {
474 decl: None,
475 commands: pipeline.commands.clone(),
476 };
477 self.transform_pipeline(&pipe_without_decl)
478 };
479 format!("{{%{} set {} = {} %}}", trim_left, clean_var, value)
480 } else {
481 format!(
483 "{{{{{} {} {}}}}}",
484 trim_left,
485 self.transform_pipeline(pipeline),
486 trim_right
487 )
488 }
489 }
490 }
491 }
492
493 fn transform_pipeline(&self, pipeline: &Pipeline) -> String {
494 let mut parts = Vec::new();
495
496 for (i, cmd) in pipeline.commands.iter().enumerate() {
497 let is_filter = i > 0;
498 parts.push(self.transform_command(cmd, is_filter));
499 }
500
501 let result = parts.join(" | ");
502
503 self.post_process_pipeline(&result)
505 }
506
507 fn post_process_pipeline(&self, result: &str) -> String {
508 let mut output = result.to_string();
509
510 if let Some(idx) = output.find(" | _in_(") {
513 let haystack = &output[..idx];
514 let rest = &output[idx + 8..]; if let Some(end) = rest.find(')') {
516 let needle = &rest[..end];
517 return format!("({} in {})", needle, haystack);
518 }
519 }
520
521 while let Some(idx) = output.find(" | _or_(") {
525 let value = &output[..idx];
526 let rest = &output[idx + 8..]; if let Some(end) = rest.find(')') {
528 let default_val = &rest[..end];
529 let remaining = &rest[end + 1..];
530 output = format!("({} or {}){}", value, default_val, remaining);
531 } else {
532 break;
533 }
534 }
535
536 output
537 }
538
539 fn transform_command(&self, cmd: &Command, as_filter: bool) -> String {
540 match cmd {
541 Command::Field(field) => self.transform_field(field),
542
543 Command::Variable(name) => {
544 if name == "." || name.is_empty() {
546 if let Some(ref ctx) = self.context_var {
547 return ctx.clone();
548 }
549 return "item".to_string();
550 }
551 if name == "$" {
553 return "values".to_string(); }
555 name.trim_start_matches('$').to_string()
557 }
558
559 Command::Function { name, args } => self.transform_function(name, args, as_filter),
560
561 Command::Parenthesized(pipeline) => {
562 format!("({})", self.transform_pipeline(pipeline))
563 }
564
565 Command::Literal(lit) => self.transform_literal(lit),
566 }
567 }
568
569 fn transform_function(&self, name: &str, args: &[Argument], as_filter: bool) -> String {
571 if let Some(charset) = match name {
576 "randAlphaNum" => Some(None), "randAlpha" => Some(Some("alpha")), "randNumeric" => Some(Some("numeric")), _ => None,
580 } {
581 if let Some(length_arg) = args.first() {
582 let length = self.transform_argument(length_arg);
583 let secret_name = self.next_secret_name();
584 return match charset {
585 None => format!(
586 "generate_secret(\"{}\", {}) {{# RENAME: give meaningful name #}}",
587 secret_name, length
588 ),
589 Some(cs) => format!(
590 "generate_secret(\"{}\", {}, \"{}\") {{# RENAME: give meaningful name #}}",
591 secret_name, length, cs
592 ),
593 };
594 }
595 }
596
597 if let Some(alternative) = UNSUPPORTED_FEATURES.get(name) {
599 return format!(
601 "__UNSUPPORTED_{}__ {{# {} #}}",
602 name.to_uppercase(),
603 alternative
604 );
605 }
606
607 if let Some(result) = self.transform_to_native_operator(name, args) {
609 return result;
610 }
611
612 if let Some(result) = self.transform_special_function(name, args) {
614 return result;
615 }
616
617 if as_filter {
619 return self.transform_as_filter(name, args);
620 }
621
622 self.transform_as_function(name, args)
624 }
625
626 fn transform_to_native_operator(&self, name: &str, args: &[Argument]) -> Option<String> {
628 if let Some(&op) = NATIVE_OPERATORS.get(name) {
630 if args.len() >= 2 {
631 let left = self.transform_argument(&args[0]);
632 let right = self.transform_argument(&args[1]);
633 return Some(format!("({} {} {})", left, op, right));
634 } else if args.len() == 1 {
635 return Some(self.transform_argument(&args[0]));
637 }
638 }
639
640 if name == "not"
642 && let Some(arg) = args.first()
643 {
644 return Some(format!("not {}", self.transform_argument(arg)));
645 }
646
647 if name == "index" && args.len() >= 2 {
649 let base = self.transform_argument(&args[0]);
650 let indices: Vec<String> = args[1..]
651 .iter()
652 .map(|a| format!("[{}]", self.transform_argument(a)))
653 .collect();
654 return Some(format!("{}{}", base, indices.join("")));
655 }
656
657 if name == "printf" {
659 return Some(self.transform_printf(args));
660 }
661
662 if name == "ternary" && args.len() >= 3 {
664 let yes = self.transform_argument(&args[0]);
665 let no = self.transform_argument(&args[1]);
666 let cond = self.transform_argument(&args[2]);
667 return Some(format!("({} if {} else {})", yes, cond, no));
668 }
669
670 if name == "coalesce" && !args.is_empty() {
672 let parts: Vec<String> = args.iter().map(|a| self.transform_argument(a)).collect();
673 return Some(format!("({})", parts.join(" or ")));
674 }
675
676 if name == "contains" && args.len() >= 2 {
678 let needle = self.transform_argument(&args[0]);
679 let haystack = self.transform_argument(&args[1]);
680 return Some(format!("({} in {})", needle, haystack));
681 }
682
683 if name == "default" && args.len() >= 2 {
688 let default_val = self.transform_argument(&args[0]);
689 let actual_val = self.transform_argument(&args[1]);
690 return Some(format!("({} or {})", actual_val, default_val));
691 }
692
693 if name == "len" && args.len() == 1 {
695 let val = self.transform_argument(&args[0]);
696 return Some(format!("{} | length", val));
697 }
698
699 if name == "list" {
701 let items: Vec<String> = args.iter().map(|a| self.transform_argument(a)).collect();
702 return Some(format!("[{}]", items.join(", ")));
703 }
704
705 if name == "dict" {
707 return Some(self.transform_dict(args));
708 }
709
710 if name == "join" && args.len() >= 2 {
712 let sep = self.transform_argument(&args[0]);
713 let list = self.transform_argument(&args[1]);
714 return Some(format!("{} | join({})", list, sep));
715 }
716
717 if (name == "split" || name == "splitList") && args.len() >= 2 {
719 let sep = self.transform_argument(&args[0]);
720 let string = self.transform_argument(&args[1]);
721 return Some(format!("{} | split({})", string, sep));
722 }
723
724 if name == "until" && !args.is_empty() {
726 let n = self.transform_argument(&args[0]);
727 return Some(format!("range({})", n));
728 }
729
730 if name == "untilStep" && args.len() >= 3 {
732 let start = self.transform_argument(&args[0]);
733 let end = self.transform_argument(&args[1]);
734 let step = self.transform_argument(&args[2]);
735 return Some(format!("range({}, {}, {})", start, end, step));
736 }
737
738 if name == "seq" && !args.is_empty() {
740 let n = self.transform_argument(&args[0]);
741 return Some(format!("range(1, {} + 1)", n));
742 }
743
744 if name == "max" && args.len() >= 2 {
746 let vals: Vec<String> = args.iter().map(|a| self.transform_argument(a)).collect();
747 return Some(format!("[{}] | max", vals.join(", ")));
748 }
749 if name == "min" && args.len() >= 2 {
750 let vals: Vec<String> = args.iter().map(|a| self.transform_argument(a)).collect();
751 return Some(format!("[{}] | min", vals.join(", ")));
752 }
753
754 if name == "merge" && args.len() >= 2 {
756 let base = self.transform_argument(&args[0]);
757 let overlay = self.transform_argument(&args[1]);
758 return Some(format!("({} | merge({}))", base, overlay));
759 }
760
761 if name == "semverCompare" && args.len() >= 2 {
764 let constraint = self.transform_argument(&args[0]);
765 let version = self.transform_argument(&args[1]);
766 return Some(format!("({} | semver_match({}))", version, constraint));
767 }
768
769 if (name == "int" || name == "int64") && args.len() == 1 {
772 let val = self.transform_argument(&args[0]);
773 return Some(format!("({} | int)", val));
774 }
775 if name == "float64" && args.len() == 1 {
776 let val = self.transform_argument(&args[0]);
777 return Some(format!("({} | float)", val));
778 }
779
780 None
781 }
782
783 fn transform_special_function(&self, name: &str, args: &[Argument]) -> Option<String> {
785 if name == "include" || name == "template" {
787 return Some(self.transform_include(args));
788 }
789
790 if name == "toYaml" && args.len() == 1 {
792 let val = self.transform_argument(&args[0]);
793 return Some(format!("{} | toyaml", val));
794 }
795 if name == "toJson" && args.len() == 1 {
796 let val = self.transform_argument(&args[0]);
797 return Some(format!("{} | tojson", val));
798 }
799
800 if name == "tpl" && !args.is_empty() {
802 let template = self.transform_argument(&args[0]);
803 return Some(format!("tpl({})", template));
804 }
805
806 if name == "lookup" {
808 return Some("{}".to_string());
810 }
811
812 if name == "print" && args.len() == 1 {
814 return Some(self.transform_argument(&args[0]));
815 }
816
817 if name == "now" {
819 return Some("now()".to_string());
820 }
821
822 if name == "uuidv4" {
824 return Some("uuidv4()".to_string());
825 }
826
827 if name == "fail" && !args.is_empty() {
829 let msg = self.transform_argument(&args[0]);
830 return Some(format!("fail({})", msg));
831 }
832
833 if name == "get" && args.len() >= 2 {
835 let dict = self.transform_argument(&args[0]);
836 let key = self.transform_argument(&args[1]);
837 return Some(format!("{}[{}]", dict, key));
838 }
839
840 if name == "hasKey" && args.len() >= 2 {
842 let dict = self.transform_argument(&args[0]);
843 let key = self.transform_argument(&args[1]);
844 return Some(format!("({} in {})", key, dict));
845 }
846
847 if name == "dig" && args.len() >= 2 {
849 let dict = self.transform_argument(&args[args.len() - 1]);
851 let default = if args.len() >= 3 {
852 self.transform_argument(&args[args.len() - 2])
853 } else {
854 "none".to_string()
855 };
856 let keys: Vec<String> = args[..args.len().saturating_sub(2)]
857 .iter()
858 .filter_map(|a| {
859 if let Argument::Literal(Literal::String(s)) = a {
860 Some(s.clone())
861 } else {
862 None
863 }
864 })
865 .collect();
866 if keys.is_empty() {
867 return Some(format!("{} | default({})", dict, default));
868 }
869 return Some(format!(
870 "{}.{} | default({})",
871 dict,
872 keys.join("."),
873 default
874 ));
875 }
876
877 if name == "empty" && args.len() == 1 {
879 let val = self.transform_argument(&args[0]);
880 return Some(format!("{} | empty", val));
881 }
882
883 None
884 }
885
886 fn transform_printf(&self, args: &[Argument]) -> String {
888 if args.is_empty() {
889 return "\"\"".to_string();
890 }
891
892 let format_str = match &args[0] {
894 Argument::Literal(Literal::String(s)) => s.clone(),
895 _ => return self.transform_argument(&args[0]),
896 };
897
898 if !format_str.contains('%') {
900 return format!("\"{}\"", format_str);
901 }
902
903 let format_args: Vec<String> = args[1..]
905 .iter()
906 .map(|a| self.transform_argument(a))
907 .collect();
908
909 let mut result = String::new();
911 let mut arg_idx = 0;
912 let mut chars = format_str.chars().peekable();
913 let mut current_literal = String::new();
914
915 while let Some(c) = chars.next() {
916 if c == '%' {
917 if let Some(&next) = chars.peek() {
918 match next {
919 's' | 'd' | 'v' | 'f' | 'g' | 't' => {
920 chars.next(); if !current_literal.is_empty() {
924 if !result.is_empty() {
925 result.push_str(" ~ ");
926 }
927 result.push_str(&format!("\"{}\"", current_literal));
928 current_literal.clear();
929 }
930
931 if arg_idx < format_args.len() {
933 if !result.is_empty() {
934 result.push_str(" ~ ");
935 }
936 result.push_str(&format_args[arg_idx]);
937 arg_idx += 1;
938 }
939 }
940 '%' => {
941 chars.next();
942 current_literal.push('%');
943 }
944 _ => {
945 current_literal.push(c);
946 }
947 }
948 } else {
949 current_literal.push(c);
950 }
951 } else {
952 current_literal.push(c);
953 }
954 }
955
956 if !current_literal.is_empty() {
958 if !result.is_empty() {
959 result.push_str(" ~ ");
960 }
961 result.push_str(&format!("\"{}\"", current_literal));
962 }
963
964 if result.is_empty() {
965 "\"\"".to_string()
966 } else {
967 format!("({})", result)
968 }
969 }
970
971 fn transform_dict(&self, args: &[Argument]) -> String {
973 let mut pairs = Vec::new();
974 let mut i = 0;
975
976 while i + 1 < args.len() {
977 let key = self.transform_argument(&args[i]);
978 let value = self.transform_argument(&args[i + 1]);
979 pairs.push(format!("{}: {}", key, value));
980 i += 2;
981 }
982
983 format!("{{{}}}", pairs.join(", "))
984 }
985
986 fn transform_as_filter(&self, name: &str, args: &[Argument]) -> String {
988 if name == "contains"
990 && let Some(arg) = args.first()
991 {
992 let needle = self.transform_argument(arg);
993 return format!("_in_({})", needle);
994 }
995
996 if name == "default" {
1000 if let Some(arg) = args.first() {
1001 let default_val = self.transform_argument(arg);
1002 return format!("_or_({})", default_val);
1003 }
1004 }
1005
1006 let filter_name = FILTER_MAP.get(name).copied().unwrap_or(name);
1008
1009 if args.is_empty() {
1010 filter_name.to_string()
1011 } else {
1012 let args_str: Vec<String> = args.iter().map(|a| self.transform_argument(a)).collect();
1013 format!("{}({})", filter_name, args_str.join(", "))
1014 }
1015 }
1016
1017 fn transform_as_function(&self, name: &str, args: &[Argument]) -> String {
1019 let args_str: Vec<String> = args.iter().map(|a| self.transform_argument(a)).collect();
1020 format!("{}({})", name, args_str.join(", "))
1021 }
1022
1023 fn transform_argument(&self, arg: &Argument) -> String {
1024 match arg {
1025 Argument::Field(field) => self.transform_field(field),
1026 Argument::Variable(name) => {
1027 if name == "." || name.is_empty() {
1028 if let Some(ref ctx) = self.context_var {
1029 return ctx.clone();
1030 }
1031 return "item".to_string();
1032 }
1033 if name == "$" {
1034 return "values".to_string();
1035 }
1036 name.trim_start_matches('$').to_string()
1037 }
1038 Argument::Literal(lit) => self.transform_literal(lit),
1039 Argument::Pipeline(pipeline) => {
1040 format!("({})", self.transform_pipeline(pipeline))
1041 }
1042 }
1043 }
1044
1045 fn transform_field(&self, field: &FieldAccess) -> String {
1046 let is_root = field.is_root;
1048
1049 if field.path.is_empty() {
1050 if let Some(ref ctx) = self.context_var {
1052 return ctx.clone();
1053 }
1054 return "item".to_string();
1055 }
1056
1057 let first = field.path[0].as_str();
1058 let rest: Vec<&str> = field.path[1..].iter().map(|s| s.as_str()).collect();
1059
1060 let _ = is_root;
1062 let prefix = "";
1063
1064 match first {
1065 "Values" => {
1066 if rest.is_empty() {
1067 format!("{}values", prefix)
1068 } else {
1069 format!("{}values.{}", prefix, rest.join("."))
1070 }
1071 }
1072 "Release" => {
1073 let prop = rest.first().copied().unwrap_or("");
1074 match prop {
1075 "Name" => format!("{}release.name", prefix),
1076 "Namespace" => format!("{}release.namespace", prefix),
1077 "Service" => "\"Sherpack\"".to_string(),
1078 "IsInstall" => format!("{}release.is_install", prefix),
1079 "IsUpgrade" => format!("{}release.is_upgrade", prefix),
1080 "Revision" => format!("{}release.revision", prefix),
1081 _ if prop.is_empty() => format!("{}release", prefix),
1082 _ => format!("{}release.{}", prefix, to_snake_case(prop)),
1083 }
1084 }
1085 "Chart" => {
1086 let prop = rest.first().copied().unwrap_or("");
1087 match prop {
1088 "Name" => format!("{}pack.name", prefix),
1089 "Version" => format!("{}pack.version", prefix),
1090 "AppVersion" => format!("{}pack.appVersion", prefix),
1091 "Description" => format!("{}pack.description", prefix),
1092 _ if prop.is_empty() => format!("{}pack", prefix),
1093 _ => format!("{}pack.{}", prefix, to_snake_case(prop)),
1094 }
1095 }
1096 "Capabilities" => {
1097 let prop = rest.first().copied().unwrap_or("");
1098 match prop {
1099 "KubeVersion" => {
1100 if rest.len() > 1 {
1101 let sub = rest[1];
1102 match sub {
1103 "Version" | "GitVersion" => {
1104 format!("{}capabilities.kubeVersion.version", prefix)
1105 }
1106 "Major" => format!("{}capabilities.kubeVersion.major", prefix),
1107 "Minor" => format!("{}capabilities.kubeVersion.minor", prefix),
1108 _ => format!(
1109 "{}capabilities.kubeVersion.{}",
1110 prefix,
1111 to_snake_case(sub)
1112 ),
1113 }
1114 } else {
1115 format!("{}capabilities.kubeVersion", prefix)
1116 }
1117 }
1118 "APIVersions" => format!("{}capabilities.apiVersions", prefix),
1119 _ if prop.is_empty() => format!("{}capabilities", prefix),
1120 _ => format!("{}capabilities.{}", prefix, to_snake_case(prop)),
1121 }
1122 }
1123 "Template" => {
1124 let prop = rest.first().copied().unwrap_or("");
1125 match prop {
1126 "Name" => format!("{}template.name", prefix),
1127 "BasePath" => format!("{}template.basePath", prefix),
1128 _ if prop.is_empty() => format!("{}template", prefix),
1129 _ => format!("{}template.{}", prefix, prop),
1130 }
1131 }
1132 "Files" => {
1133 let full_path = std::iter::once(first)
1135 .chain(rest.iter().copied())
1136 .collect::<Vec<_>>()
1137 .join(".");
1138 format!("__UNSUPPORTED_FILES__ {{# {} #}}", full_path)
1139 }
1140 _ => {
1141 let full_path = std::iter::once(first)
1143 .chain(rest.iter().copied())
1144 .collect::<Vec<_>>()
1145 .join(".");
1146
1147 if let Some(ref ctx) = self.context_var {
1149 format!("{}.{}", ctx, full_path)
1150 } else {
1151 full_path
1152 }
1153 }
1154 }
1155 }
1156
1157 fn transform_literal(&self, lit: &Literal) -> String {
1158 match lit {
1159 Literal::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
1160 Literal::Char(c) => format!("\"{}\"", c),
1161 Literal::Int(n) => n.to_string(),
1162 Literal::Float(n) => n.to_string(),
1163 Literal::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1164 Literal::Nil => "none".to_string(),
1165 }
1166 }
1167
1168 fn transform_include(&self, args: &[Argument]) -> String {
1169 if args.is_empty() {
1170 return "MISSING_INCLUDE_NAME()".to_string();
1171 }
1172
1173 let name = match &args[0] {
1175 Argument::Literal(Literal::String(s)) => self.strip_chart_prefix(s),
1176 _ => "DYNAMIC_INCLUDE".to_string(),
1177 };
1178
1179 format!("{}()", name)
1180 }
1181
1182 fn strip_chart_prefix(&self, name: &str) -> String {
1183 let stripped = if let Some(ref prefix) = self.chart_prefix {
1184 name.strip_prefix(prefix.as_str()).unwrap_or(name)
1185 } else {
1186 name
1187 };
1188
1189 stripped.trim_matches('"').replace(['.', '-'], "_")
1191 }
1192
1193 fn is_dict_type(&self, collection: &str) -> bool {
1198 if let Some(ref ctx) = self.type_context {
1200 match ctx.get_type(collection) {
1201 InferredType::Dict => return true,
1202 InferredType::List => return false,
1203 InferredType::Scalar => return false,
1204 InferredType::Unknown => {
1205 }
1207 }
1208 }
1209
1210 TypeHeuristics::guess_type(collection)
1212 .map(|t| t == InferredType::Dict)
1213 .unwrap_or(false)
1214 }
1215}
1216
1217fn to_snake_case(s: &str) -> String {
1219 let mut result = String::new();
1220 for (i, c) in s.chars().enumerate() {
1221 if c.is_uppercase() {
1222 if i > 0 {
1223 result.push('_');
1224 }
1225 result.push(c.to_lowercase().next().unwrap());
1226 } else {
1227 result.push(c);
1228 }
1229 }
1230 result
1231}
1232
1233#[cfg(test)]
1238mod tests {
1239 use super::*;
1240 use crate::parser;
1241
1242 fn transform(input: &str) -> String {
1243 let ast = parser::parse(input).expect("Failed to parse");
1244 let mut transformer = Transformer::new();
1245 transformer.transform(&ast)
1246 }
1247
1248 fn transform_with_prefix(input: &str, prefix: &str) -> String {
1249 let ast = parser::parse(input).expect("Failed to parse");
1250 let mut transformer = Transformer::new().with_chart_prefix(prefix);
1251 transformer.transform(&ast)
1252 }
1253
1254 #[test]
1259 fn test_raw_text() {
1260 assert_eq!(transform("hello world"), "hello world");
1261 }
1262
1263 #[test]
1264 fn test_comment() {
1265 assert_eq!(
1266 transform("{{/* This is a comment */}}"),
1267 "{# This is a comment #}"
1268 );
1269 }
1270
1271 #[test]
1272 fn test_simple_variable() {
1273 assert_eq!(transform("{{ .Values.name }}"), "{{ values.name }}");
1274 }
1275
1276 #[test]
1277 fn test_trim_whitespace() {
1278 assert_eq!(transform("{{- .Values.name -}}"), "{{- values.name -}}");
1279 }
1280
1281 #[test]
1286 fn test_comparison_eq() {
1287 assert_eq!(
1288 transform("{{ eq .Values.a .Values.b }}"),
1289 "{{ (values.a == values.b) }}"
1290 );
1291 }
1292
1293 #[test]
1294 fn test_comparison_ne() {
1295 assert_eq!(
1296 transform("{{ ne .Values.a \"test\" }}"),
1297 "{{ (values.a != \"test\") }}"
1298 );
1299 }
1300
1301 #[test]
1302 fn test_math_add() {
1303 assert_eq!(transform("{{ add 1 2 }}"), "{{ (1 + 2) }}");
1304 }
1305
1306 #[test]
1307 fn test_math_operations() {
1308 assert_eq!(transform("{{ sub 10 5 }}"), "{{ (10 - 5) }}");
1309 assert_eq!(transform("{{ mul 3 4 }}"), "{{ (3 * 4) }}");
1310 assert_eq!(transform("{{ div 10 2 }}"), "{{ (10 / 2) }}");
1311 assert_eq!(transform("{{ mod 10 3 }}"), "{{ (10 % 3) }}");
1312 }
1313
1314 #[test]
1315 fn test_ternary() {
1316 assert_eq!(
1317 transform("{{ ternary \"yes\" \"no\" .Values.enabled }}"),
1318 "{{ (\"yes\" if values.enabled else \"no\") }}"
1319 );
1320 }
1321
1322 #[test]
1323 fn test_coalesce() {
1324 assert_eq!(
1325 transform("{{ coalesce .Values.a .Values.b \"default\" }}"),
1326 "{{ (values.a or values.b or \"default\") }}"
1327 );
1328 }
1329
1330 #[test]
1331 fn test_default_function() {
1332 assert_eq!(
1334 transform("{{ default \"fallback\" .Values.x }}"),
1335 "{{ (values.x or \"fallback\") }}"
1336 );
1337 }
1338
1339 #[test]
1340 fn test_default_filter() {
1341 assert_eq!(
1343 transform("{{ .Values.x | default \"fallback\" }}"),
1344 "{{ (values.x or \"fallback\") }}"
1345 );
1346 }
1347
1348 #[test]
1349 fn test_default_chained() {
1350 assert_eq!(
1352 transform("{{ .Values.a | default .Values.b | default \"c\" }}"),
1353 "{{ ((values.a or values.b) or \"c\") }}"
1354 );
1355 }
1356
1357 #[test]
1358 fn test_index_list() {
1359 assert_eq!(
1360 transform("{{ index .Values.list 0 }}"),
1361 "{{ values.list[0] }}"
1362 );
1363 }
1364
1365 #[test]
1366 fn test_index_map() {
1367 assert_eq!(
1368 transform("{{ index .Values.map \"key\" }}"),
1369 "{{ values.map[\"key\"] }}"
1370 );
1371 }
1372
1373 #[test]
1374 fn test_index_nested() {
1375 assert_eq!(
1376 transform("{{ index .Values.data \"a\" \"b\" }}"),
1377 "{{ values.data[\"a\"][\"b\"] }}"
1378 );
1379 }
1380
1381 #[test]
1386 fn test_printf_simple() {
1387 assert_eq!(
1388 transform("{{ printf \"%s-%s\" .Release.Name .Chart.Name }}"),
1389 "{{ (release.name ~ \"-\" ~ pack.name) }}"
1390 );
1391 }
1392
1393 #[test]
1394 fn test_printf_complex() {
1395 assert_eq!(
1396 transform("{{ printf \"prefix-%s-suffix\" .Values.name }}"),
1397 "{{ (\"prefix-\" ~ values.name ~ \"-suffix\") }}"
1398 );
1399 }
1400
1401 #[test]
1406 fn test_contains_function() {
1407 assert_eq!(
1408 transform("{{ contains \"needle\" .Values.haystack }}"),
1409 "{{ (\"needle\" in values.haystack) }}"
1410 );
1411 }
1412
1413 #[test]
1414 fn test_contains_in_if() {
1415 assert_eq!(
1416 transform("{{- if contains $name .Release.Name }}yes{{- end }}"),
1417 "{%- if (name in release.name) %}yes{%- endif %}"
1418 );
1419 }
1420
1421 #[test]
1426 fn test_if_else() {
1427 assert_eq!(
1428 transform("{{- if .Values.x }}yes{{- else }}no{{- end }}"),
1429 "{%- if values.x %}yes{%- else %}no{%- endif %}"
1430 );
1431 }
1432
1433 #[test]
1434 fn test_if_elif() {
1435 assert_eq!(
1436 transform("{{- if .Values.a }}a{{- else if .Values.b }}b{{- end }}"),
1437 "{%- if values.a %}a{%- elif values.b %}b{%- endif %}"
1438 );
1439 }
1440
1441 #[test]
1442 fn test_range() {
1443 assert_eq!(
1444 transform("{{- range .Values.items }}{{ . }}{{- end }}"),
1445 "{%- for item in values.items %}{{ item }}{%- endfor %}"
1446 );
1447 }
1448
1449 #[test]
1450 fn test_range_with_variable() {
1451 assert_eq!(
1452 transform("{{- range $item := .Values.list }}{{ $item }}{{- end }}"),
1453 "{%- for item in values.list %}{{ item }}{%- endfor %}"
1454 );
1455 }
1456
1457 #[test]
1462 fn test_variable_declaration() {
1463 assert_eq!(
1464 transform("{{- $name := .Values.name }}{{ $name }}"),
1465 "{%- set name = values.name %}{{ name }}"
1466 );
1467 }
1468
1469 #[test]
1474 fn test_define() {
1475 assert_eq!(
1476 transform("{{- define \"myapp.name\" }}test{{- end }}"),
1477 "{%- macro myapp_name() %}test{%- endmacro %}"
1478 );
1479 }
1480
1481 #[test]
1482 fn test_include() {
1483 assert_eq!(
1484 transform_with_prefix("{{ include \"myapp.fullname\" . }}", "myapp"),
1485 "{{ fullname() }}"
1486 );
1487 }
1488
1489 #[test]
1494 fn test_filter_pipeline() {
1495 assert_eq!(
1496 transform("{{ .Values.name | quote }}"),
1497 "{{ values.name | quote }}"
1498 );
1499 }
1500
1501 #[test]
1502 fn test_filter_with_arg() {
1503 assert_eq!(
1504 transform("{{ .Values.text | indent 4 }}"),
1505 "{{ values.text | indent(4) }}"
1506 );
1507 }
1508
1509 #[test]
1510 fn test_filter_chain() {
1511 assert_eq!(
1512 transform("{{ .Values.config | toYaml | nindent 4 }}"),
1513 "{{ values.config | toyaml | nindent(4) }}"
1514 );
1515 }
1516
1517 #[test]
1522 fn test_list() {
1523 assert_eq!(transform("{{ list 1 2 3 }}"), "{{ [1, 2, 3] }}");
1524 }
1525
1526 #[test]
1527 fn test_dict() {
1528 assert_eq!(
1529 transform("{{ dict \"key1\" .Values.a \"key2\" .Values.b }}"),
1530 "{{ {\"key1\": values.a, \"key2\": values.b} }}"
1531 );
1532 }
1533
1534 #[test]
1539 fn test_until() {
1540 assert_eq!(transform("{{ until 5 }}"), "{{ range(5) }}");
1541 }
1542
1543 #[test]
1544 fn test_until_step() {
1545 assert_eq!(transform("{{ untilStep 0 10 2 }}"), "{{ range(0, 10, 2) }}");
1546 }
1547
1548 #[test]
1553 fn test_release_service() {
1554 assert_eq!(transform("{{ .Release.Service }}"), "{{ \"Sherpack\" }}");
1555 }
1556
1557 #[test]
1558 fn test_chart_appversion() {
1559 assert_eq!(
1560 transform("{{ .Chart.AppVersion }}"),
1561 "{{ pack.appVersion }}"
1562 );
1563 }
1564
1565 #[test]
1566 fn test_capabilities() {
1567 assert_eq!(
1568 transform("{{ .Capabilities.KubeVersion.Version }}"),
1569 "{{ capabilities.kubeVersion.version }}"
1570 );
1571 }
1572
1573 #[test]
1578 fn test_range_dict_with_type_context() {
1579 use crate::type_inference::TypeContext;
1580
1581 let yaml = r#"
1582controller:
1583 containerPort:
1584 http: 80
1585 https: 443
1586"#;
1587 let ctx = TypeContext::from_yaml(yaml).unwrap();
1588 let mut transformer = Transformer::new().with_type_context(ctx);
1589
1590 let input = crate::parser::parse("{{- range $key, $value := .Values.controller.containerPort }}{{ $key }}: {{ $value }}{{- end }}").unwrap();
1591 let result = transformer.transform(&input);
1592
1593 assert_eq!(
1594 result,
1595 "{%- for key, value in values.controller.containerPort | dictsort %}{{ key }}: {{ value }}{%- endfor %}"
1596 );
1597 }
1598
1599 #[test]
1600 fn test_range_list_with_type_context() {
1601 use crate::type_inference::TypeContext;
1602
1603 let yaml = r#"
1604controller:
1605 extraEnvs:
1606 - name: FOO
1607 value: bar
1608"#;
1609 let ctx = TypeContext::from_yaml(yaml).unwrap();
1610 let mut transformer = Transformer::new().with_type_context(ctx);
1611
1612 let input = crate::parser::parse(
1613 "{{- range $i, $env := .Values.controller.extraEnvs }}{{ $env }}{{- end }}",
1614 )
1615 .unwrap();
1616 let result = transformer.transform(&input);
1617
1618 assert_eq!(
1620 result,
1621 "{%- for env in values.controller.extraEnvs %}{#- i = loop.index0 #}{{ env }}{%- endfor %}"
1622 );
1623 }
1624
1625 #[test]
1626 fn test_range_dict_heuristic() {
1627 let mut transformer = Transformer::new();
1629
1630 let input = crate::parser::parse(
1632 "{{- range $key, $value := .Values.controller.containerPort }}{{ $key }}{{- end }}",
1633 )
1634 .unwrap();
1635 let result = transformer.transform(&input);
1636
1637 assert_eq!(
1638 result,
1639 "{%- for key, value in values.controller.containerPort | dictsort %}{{ key }}{%- endfor %}"
1640 );
1641 }
1642
1643 #[test]
1644 fn test_range_annotations_heuristic() {
1645 let mut transformer = Transformer::new();
1646
1647 let input = crate::parser::parse("{{- range $k, $v := .Values.podAnnotations }}{{- end }}")
1649 .unwrap();
1650 let result = transformer.transform(&input);
1651
1652 assert_eq!(
1653 result,
1654 "{%- for k, v in values.podAnnotations | dictsort %}{%- endfor %}"
1655 );
1656 }
1657}