1use pest::Parser;
6use pest_derive::Parser;
7use thiserror::Error;
8
9use crate::ast::*;
10
11#[derive(Parser)]
12#[grammar = "go_template.pest"]
13struct GoTemplateParser;
14
15#[derive(Debug, Error)]
17pub enum ParseError {
18 #[error("Parse error: {0}")]
19 Pest(Box<pest::error::Error<Rule>>),
20
21 #[error("Invalid number: {0}")]
22 InvalidNumber(String),
23
24 #[error("Invalid string: {0}")]
25 InvalidString(String),
26
27 #[error("Unexpected rule: {0:?}")]
28 UnexpectedRule(Rule),
29}
30
31impl From<pest::error::Error<Rule>> for ParseError {
32 fn from(e: pest::error::Error<Rule>) -> Self {
33 ParseError::Pest(Box::new(e))
34 }
35}
36
37pub type Result<T> = std::result::Result<T, ParseError>;
38
39pub fn parse(input: &str) -> Result<Template> {
41 let pairs = GoTemplateParser::parse(Rule::template, input)?;
42
43 let mut elements = Vec::new();
44
45 for pair in pairs {
46 match pair.as_rule() {
47 Rule::template => {
48 for inner in pair.into_inner() {
49 if let Some(elem) = parse_element(inner)? {
50 elements.push(elem);
51 }
52 }
53 }
54 Rule::EOI => {}
55 _ => {}
56 }
57 }
58
59 Ok(Template { elements })
60}
61
62fn parse_element(pair: pest::iterators::Pair<Rule>) -> Result<Option<Element>> {
63 match pair.as_rule() {
64 Rule::raw_text => {
65 let text = pair.as_str().to_string();
66 if text.is_empty() {
67 Ok(None)
68 } else {
69 Ok(Some(Element::RawText(text)))
70 }
71 }
72 Rule::action => {
73 let action = parse_action(pair)?;
74 Ok(Some(Element::Action(action)))
75 }
76 Rule::EOI => Ok(None),
77 _ => Ok(None),
78 }
79}
80
81fn parse_action(pair: pest::iterators::Pair<Rule>) -> Result<Action> {
82 let mut trim_left = false;
83 let mut trim_right = false;
84 let mut body = None;
85
86 for inner in pair.into_inner() {
87 match inner.as_rule() {
88 Rule::action_start => {
89 trim_left = inner.as_str().ends_with('-');
90 }
91 Rule::action_end => {
92 trim_right = inner.as_str().starts_with('-');
93 }
94 _ => {
95 body = Some(parse_action_body(inner)?);
96 }
97 }
98 }
99
100 Ok(Action {
101 trim_left,
102 trim_right,
103 body: body.unwrap_or(ActionBody::Pipeline(Pipeline {
104 decl: None,
105 commands: vec![],
106 })),
107 })
108}
109
110fn parse_action_body(pair: pest::iterators::Pair<Rule>) -> Result<ActionBody> {
111 match pair.as_rule() {
112 Rule::comment => {
113 let text = pair.as_str();
114 let content = text
116 .strip_prefix("/*")
117 .and_then(|s| s.strip_suffix("*/"))
118 .unwrap_or(text)
119 .to_string();
120 Ok(ActionBody::Comment(content))
121 }
122 Rule::if_action => {
123 let pipeline = parse_pipeline_from_inner(pair)?;
124 Ok(ActionBody::If(pipeline))
125 }
126 Rule::else_if_action => {
127 let pipeline = parse_pipeline_from_inner(pair)?;
128 Ok(ActionBody::ElseIf(pipeline))
129 }
130 Rule::else_action => Ok(ActionBody::Else),
131 Rule::end_action => Ok(ActionBody::End),
132 Rule::range_action => {
133 let mut vars = None;
134 let mut pipeline = None;
135
136 for inner in pair.into_inner() {
137 match inner.as_rule() {
138 Rule::range_clause => {
139 vars = Some(parse_range_clause(inner)?);
140 }
141 Rule::pipeline | Rule::pipeline_expr => {
142 pipeline = Some(parse_pipeline(inner)?);
143 }
144 _ => {}
145 }
146 }
147
148 Ok(ActionBody::Range {
149 vars,
150 pipeline: pipeline.unwrap_or_else(|| Pipeline {
151 decl: None,
152 commands: vec![],
153 }),
154 })
155 }
156 Rule::with_action => {
157 let pipeline = parse_pipeline_from_inner(pair)?;
158 Ok(ActionBody::With(pipeline))
159 }
160 Rule::define_action => {
161 let name = extract_string_literal(pair)?;
162 Ok(ActionBody::Define(name))
163 }
164 Rule::template_action => {
165 let mut name = String::new();
166 let mut pipeline = None;
167
168 for inner in pair.into_inner() {
169 match inner.as_rule() {
170 Rule::string_literal => {
171 name = parse_string_literal(inner)?;
172 }
173 Rule::pipeline | Rule::pipeline_expr => {
174 pipeline = Some(parse_pipeline(inner)?);
175 }
176 _ => {}
177 }
178 }
179
180 Ok(ActionBody::Template { name, pipeline })
181 }
182 Rule::block_action => {
183 let mut name = String::new();
184 let mut pipeline = Pipeline {
185 decl: None,
186 commands: vec![],
187 };
188
189 for inner in pair.into_inner() {
190 match inner.as_rule() {
191 Rule::string_literal => {
192 name = parse_string_literal(inner)?;
193 }
194 Rule::pipeline | Rule::pipeline_expr => {
195 pipeline = parse_pipeline(inner)?;
196 }
197 _ => {}
198 }
199 }
200
201 Ok(ActionBody::Block { name, pipeline })
202 }
203 Rule::pipeline | Rule::pipeline_expr | Rule::pipeline_decl => {
204 let pipeline = parse_pipeline(pair)?;
205 Ok(ActionBody::Pipeline(pipeline))
206 }
207 _ => {
208 let pipeline = parse_pipeline(pair)?;
210 Ok(ActionBody::Pipeline(pipeline))
211 }
212 }
213}
214
215fn parse_pipeline_from_inner(pair: pest::iterators::Pair<Rule>) -> Result<Pipeline> {
216 for inner in pair.into_inner() {
217 match inner.as_rule() {
218 Rule::pipeline | Rule::pipeline_expr | Rule::pipeline_decl => {
219 return parse_pipeline(inner);
220 }
221 _ => {}
222 }
223 }
224 Ok(Pipeline {
225 decl: None,
226 commands: vec![],
227 })
228}
229
230fn parse_pipeline(pair: pest::iterators::Pair<Rule>) -> Result<Pipeline> {
231 let mut decl = None;
232 let mut commands = Vec::new();
233
234 match pair.as_rule() {
235 Rule::pipeline_decl => {
236 for inner in pair.into_inner() {
237 match inner.as_rule() {
238 Rule::variable => {
239 decl = Some(inner.as_str().trim_start_matches('$').to_string());
240 }
241 Rule::pipeline_expr => {
242 let sub = parse_pipeline(inner)?;
243 commands = sub.commands;
244 }
245 _ => {}
246 }
247 }
248 }
249 Rule::pipeline | Rule::pipeline_expr => {
250 for inner in pair.into_inner() {
251 match inner.as_rule() {
252 Rule::command => {
253 commands.push(parse_command(inner)?);
254 }
255 Rule::pipeline_decl => {
256 let sub = parse_pipeline(inner)?;
257 decl = sub.decl;
258 commands.extend(sub.commands);
259 }
260 Rule::pipeline_expr => {
261 let sub = parse_pipeline(inner)?;
262 commands.extend(sub.commands);
263 }
264 _ => {
265 if let Ok(cmd) = parse_command(inner) {
267 commands.push(cmd);
268 }
269 }
270 }
271 }
272 }
273 _ => {
274 commands.push(parse_command(pair)?);
276 }
277 }
278
279 Ok(Pipeline { decl, commands })
280}
281
282fn parse_command(pair: pest::iterators::Pair<Rule>) -> Result<Command> {
283 match pair.as_rule() {
284 Rule::command => {
285 if let Some(inner) = pair.into_inner().next() {
287 return parse_command(inner);
288 }
289 Err(ParseError::UnexpectedRule(Rule::command))
290 }
291 Rule::parenthesized => {
292 for inner in pair.into_inner() {
293 if matches!(
294 inner.as_rule(),
295 Rule::pipeline | Rule::pipeline_expr | Rule::pipeline_decl
296 ) {
297 let pipeline = parse_pipeline(inner)?;
298 return Ok(Command::Parenthesized(Box::new(pipeline)));
299 }
300 }
301 Err(ParseError::UnexpectedRule(Rule::parenthesized))
302 }
303 Rule::function_call => {
304 let mut name = String::new();
305 let mut args = Vec::new();
306
307 for inner in pair.into_inner() {
308 match inner.as_rule() {
309 Rule::identifier => {
310 name = inner.as_str().to_string();
311 }
312 Rule::argument => {
313 args.push(parse_argument(inner)?);
314 }
315 _ => {}
316 }
317 }
318
319 Ok(Command::Function { name, args })
320 }
321 Rule::method_call => {
322 let mut field = None;
323 let mut args = Vec::new();
324
325 for inner in pair.into_inner() {
326 match inner.as_rule() {
327 Rule::field_chain => {
328 field = Some(parse_field_chain(inner)?);
329 }
330 Rule::argument => {
331 args.push(parse_argument(inner)?);
332 }
333 _ => {}
334 }
335 }
336
337 if let Some(f) = field {
339 let method_name = f.path.last().cloned().unwrap_or_default();
340 if args.is_empty() {
341 Ok(Command::Field(f))
342 } else {
343 Ok(Command::Function {
344 name: method_name,
345 args,
346 })
347 }
348 } else {
349 Err(ParseError::UnexpectedRule(Rule::method_call))
350 }
351 }
352 Rule::field_chain => {
353 let field = parse_field_chain(pair)?;
354 Ok(Command::Field(field))
355 }
356 Rule::variable => {
357 let name = pair.as_str().trim_start_matches('$').to_string();
358 Ok(Command::Variable(name))
359 }
360 Rule::literal => {
361 let lit = parse_literal(pair)?;
362 Ok(Command::Literal(lit))
363 }
364 Rule::identifier | Rule::bare_identifier => {
365 let name = match pair.as_rule() {
367 Rule::bare_identifier => pair
368 .into_inner()
369 .next()
370 .map(|p| p.as_str().to_string())
371 .unwrap_or_default(),
372 _ => pair.as_str().to_string(),
373 };
374 Ok(Command::Function { name, args: vec![] })
375 }
376 Rule::string_literal | Rule::number | Rule::boolean | Rule::nil => {
377 let lit = parse_literal(pair)?;
378 Ok(Command::Literal(lit))
379 }
380 _ => Err(ParseError::UnexpectedRule(pair.as_rule())),
381 }
382}
383
384fn parse_field_chain(pair: pest::iterators::Pair<Rule>) -> Result<FieldAccess> {
385 let text = pair.as_str();
386
387 let is_root = text.starts_with("$.");
389
390 let path_str = text
392 .trim_start_matches("$.")
393 .trim_start_matches('$')
394 .trim_start_matches('.');
395
396 let path: Vec<String> = path_str
398 .split('.')
399 .filter(|s| !s.is_empty())
400 .map(|s| s.to_string())
401 .collect();
402
403 Ok(FieldAccess { is_root, path })
404}
405
406fn parse_argument(pair: pest::iterators::Pair<Rule>) -> Result<Argument> {
407 for inner in pair.into_inner() {
408 match inner.as_rule() {
409 Rule::field_chain => {
410 let field = parse_field_chain(inner)?;
411 return Ok(Argument::Field(field));
412 }
413 Rule::variable => {
414 let name = inner.as_str().trim_start_matches('$').to_string();
415 return Ok(Argument::Variable(name));
416 }
417 Rule::literal | Rule::string_literal | Rule::number | Rule::boolean | Rule::nil => {
418 let lit = parse_literal(inner)?;
419 return Ok(Argument::Literal(lit));
420 }
421 Rule::parenthesized | Rule::pipeline | Rule::pipeline_expr => {
422 let pipeline = parse_pipeline(inner)?;
423 return Ok(Argument::Pipeline(Box::new(pipeline)));
424 }
425 _ => {}
426 }
427 }
428 Err(ParseError::UnexpectedRule(Rule::argument))
429}
430
431fn parse_literal(pair: pest::iterators::Pair<Rule>) -> Result<Literal> {
432 match pair.as_rule() {
433 Rule::literal => {
434 if let Some(inner) = pair.into_inner().next() {
435 return parse_literal(inner);
436 }
437 Err(ParseError::UnexpectedRule(Rule::literal))
438 }
439 Rule::string_literal => {
440 let s = parse_string_literal(pair)?;
441 Ok(Literal::String(s))
442 }
443 Rule::char_literal => {
444 let text = pair.as_str();
445 let c = text
446 .trim_start_matches('\'')
447 .trim_end_matches('\'')
448 .chars()
449 .next()
450 .unwrap_or(' ');
451 Ok(Literal::Char(c))
452 }
453 Rule::number => {
454 let text = pair.as_str();
455 if text.contains('.') || text.contains('e') || text.contains('E') {
456 let n: f64 = text
457 .parse()
458 .map_err(|_| ParseError::InvalidNumber(text.to_string()))?;
459 Ok(Literal::Float(n))
460 } else if text.starts_with("0x") || text.starts_with("0X") {
461 let n = i64::from_str_radix(&text[2..], 16)
462 .map_err(|_| ParseError::InvalidNumber(text.to_string()))?;
463 Ok(Literal::Int(n))
464 } else if text.starts_with("0o") || text.starts_with("0O") {
465 let n = i64::from_str_radix(&text[2..], 8)
466 .map_err(|_| ParseError::InvalidNumber(text.to_string()))?;
467 Ok(Literal::Int(n))
468 } else if text.starts_with("0b") || text.starts_with("0B") {
469 let n = i64::from_str_radix(&text[2..], 2)
470 .map_err(|_| ParseError::InvalidNumber(text.to_string()))?;
471 Ok(Literal::Int(n))
472 } else {
473 let n: i64 = text
474 .parse()
475 .map_err(|_| ParseError::InvalidNumber(text.to_string()))?;
476 Ok(Literal::Int(n))
477 }
478 }
479 Rule::boolean => {
480 let b = pair.as_str() == "true";
481 Ok(Literal::Bool(b))
482 }
483 Rule::nil => Ok(Literal::Nil),
484 _ => Err(ParseError::UnexpectedRule(pair.as_rule())),
485 }
486}
487
488fn parse_string_literal(pair: pest::iterators::Pair<Rule>) -> Result<String> {
489 let text = pair.as_str();
490
491 if text.starts_with('`') {
493 return Ok(text.trim_matches('`').to_string());
494 }
495
496 let inner = text
498 .strip_prefix('"')
499 .and_then(|s| s.strip_suffix('"'))
500 .unwrap_or(text);
501
502 let mut result = String::with_capacity(inner.len());
504 let mut chars = inner.chars().peekable();
505
506 while let Some(c) = chars.next() {
507 if c == '\\' {
508 match chars.next() {
509 Some('n') => result.push('\n'),
510 Some('r') => result.push('\r'),
511 Some('t') => result.push('\t'),
512 Some('\\') => result.push('\\'),
513 Some('"') => result.push('"'),
514 Some('\'') => result.push('\''),
515 Some(other) => {
516 result.push('\\');
517 result.push(other);
518 }
519 None => result.push('\\'),
520 }
521 } else {
522 result.push(c);
523 }
524 }
525
526 Ok(result)
527}
528
529fn extract_string_literal(pair: pest::iterators::Pair<Rule>) -> Result<String> {
530 for inner in pair.into_inner() {
531 if inner.as_rule() == Rule::string_literal {
532 return parse_string_literal(inner);
533 }
534 }
535 Err(ParseError::InvalidString(
536 "No string literal found".to_string(),
537 ))
538}
539
540fn parse_range_clause(pair: pest::iterators::Pair<Rule>) -> Result<RangeVars> {
541 let mut vars = Vec::new();
542
543 for inner in pair.into_inner() {
544 if inner.as_rule() == Rule::range_vars {
545 for var in inner.into_inner() {
546 if var.as_rule() == Rule::variable {
547 vars.push(var.as_str().trim_start_matches('$').to_string());
548 }
549 }
550 }
551 }
552
553 match vars.len() {
554 0 => Ok(RangeVars {
555 index_var: None,
556 value_var: "item".to_string(),
557 }),
558 1 => Ok(RangeVars {
559 index_var: None,
560 value_var: vars.remove(0),
561 }),
562 _ => Ok(RangeVars {
563 index_var: Some(vars.remove(0)),
564 value_var: vars.remove(0),
565 }),
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 #[test]
574 fn test_parse_simple_variable() {
575 let result = parse("{{ .Values.name }}").unwrap();
576 assert_eq!(result.elements.len(), 1);
577
578 if let Element::Action(action) = &result.elements[0] {
579 if let ActionBody::Pipeline(pipeline) = &action.body {
580 assert_eq!(pipeline.commands.len(), 1);
581 if let Command::Field(field) = &pipeline.commands[0] {
582 assert_eq!(field.path, vec!["Values", "name"]);
583 }
584 }
585 }
586 }
587
588 #[test]
589 fn test_parse_with_trim() {
590 let result = parse("{{- .Values.name -}}").unwrap();
591 if let Element::Action(action) = &result.elements[0] {
592 assert!(action.trim_left);
593 assert!(action.trim_right);
594 }
595 }
596
597 #[test]
598 fn test_parse_if() {
599 let result = parse("{{- if .Values.enabled }}yes{{- end }}").unwrap();
600 assert_eq!(result.elements.len(), 3);
601
602 if let Element::Action(action) = &result.elements[0] {
603 assert!(matches!(action.body, ActionBody::If(_)));
604 }
605 }
606
607 #[test]
608 fn test_parse_range() {
609 let result = parse("{{- range .Values.items }}{{ . }}{{- end }}").unwrap();
610
611 if let Element::Action(action) = &result.elements[0] {
612 if let ActionBody::Range { vars, pipeline } = &action.body {
613 assert!(vars.is_none());
614 assert!(!pipeline.commands.is_empty());
615 }
616 }
617 }
618
619 #[test]
620 fn test_parse_range_with_vars() {
621 let result = parse("{{- range $i, $v := .Values.items }}{{ $v }}{{- end }}").unwrap();
622
623 if let Element::Action(action) = &result.elements[0] {
624 if let ActionBody::Range { vars, .. } = &action.body {
625 let vars = vars.as_ref().unwrap();
626 assert_eq!(vars.index_var, Some("i".to_string()));
627 assert_eq!(vars.value_var, "v");
628 }
629 }
630 }
631
632 #[test]
633 fn test_parse_pipeline() {
634 let result = parse("{{ .Values.name | quote }}").unwrap();
635
636 if let Element::Action(action) = &result.elements[0] {
637 if let ActionBody::Pipeline(pipeline) = &action.body {
638 assert_eq!(pipeline.commands.len(), 2);
639 }
640 }
641 }
642
643 #[test]
644 fn test_parse_function_call() {
645 let result = parse("{{ printf \"%s-%s\" .Release.Name .Chart.Name }}").unwrap();
646
647 if let Element::Action(action) = &result.elements[0] {
648 if let ActionBody::Pipeline(pipeline) = &action.body {
649 if let Command::Function { name, args } = &pipeline.commands[0] {
650 assert_eq!(name, "printf");
651 assert_eq!(args.len(), 3);
652 }
653 }
654 }
655 }
656
657 #[test]
658 fn test_parse_define() {
659 let result = parse("{{- define \"myapp.name\" -}}test{{- end }}").unwrap();
660
661 if let Element::Action(action) = &result.elements[0] {
662 if let ActionBody::Define(name) = &action.body {
663 assert_eq!(name, "myapp.name");
664 }
665 }
666 }
667
668 #[test]
669 fn test_parse_include() {
670 let result = parse("{{ include \"myapp.name\" . }}").unwrap();
671
672 if let Element::Action(action) = &result.elements[0] {
673 if let ActionBody::Pipeline(pipeline) = &action.body {
674 if let Command::Function { name, args } = &pipeline.commands[0] {
675 assert_eq!(name, "include");
676 assert_eq!(args.len(), 2);
677 }
678 }
679 }
680 }
681
682 #[test]
683 fn test_parse_comment() {
684 let result = parse("{{/* This is a comment */}}").unwrap();
685
686 if let Element::Action(action) = &result.elements[0] {
687 if let ActionBody::Comment(text) = &action.body {
688 assert_eq!(text.trim(), "This is a comment");
689 }
690 }
691 }
692
693 #[test]
694 fn test_parse_raw_text() {
695 let result = parse("apiVersion: v1\nkind: ConfigMap").unwrap();
696 assert_eq!(result.elements.len(), 1);
697
698 if let Element::RawText(text) = &result.elements[0] {
699 assert!(text.contains("apiVersion: v1"));
700 }
701 }
702
703 #[test]
704 fn test_parse_nested_boolean() {
705 let result = parse("{{ and .Values.a .Values.b }}");
707 assert!(result.is_ok(), "Simple and failed: {:?}", result);
708
709 let result = parse("{{ and (eq .Values.a \"x\") .Values.b }}");
711 assert!(result.is_ok(), "And with eq failed: {:?}", result);
712
713 let result =
715 parse("{{- if and (eq .Values.a \"x\") (or .Values.b .Values.c) }}ok{{- end }}");
716 assert!(result.is_ok(), "Full nested failed: {:?}", result);
717 }
718
719 #[test]
720 fn test_parse_parenthesized_function() {
721 let result = parse("{{ (eq .Values.a \"x\") }}");
722 assert!(result.is_ok(), "Parenthesized eq failed: {:?}", result);
723 }
724}