sherpack_convert/
parser.rs

1//! Go template parser
2//!
3//! Parses Go/Helm template syntax into an AST using pest.
4
5use 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/// Parser error
16#[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
39/// Parse a Go template string into an AST
40pub 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            // Remove /* and */
115            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            // Try to parse as pipeline
209            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                        // Try parsing as command directly
266                        if let Ok(cmd) = parse_command(inner) {
267                            commands.push(cmd);
268                        }
269                    }
270                }
271            }
272        }
273        _ => {
274            // Try to parse as a single command
275            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            // Unwrap the command and parse its inner content
286            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            // Convert method call to function call
338            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            // Bare identifier is a function call with no args (like "now" or "fail" or "quote")
366            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    // Check for root marker ($.)
388    let is_root = text.starts_with("$.");
389
390    // Remove leading $. or .
391    let path_str = text
392        .trim_start_matches("$.")
393        .trim_start_matches('$')
394        .trim_start_matches('.');
395
396    // Split by dots
397    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    // Handle backtick strings
492    if text.starts_with('`') {
493        return Ok(text.trim_matches('`').to_string());
494    }
495
496    // Handle quoted strings
497    let inner = text
498        .strip_prefix('"')
499        .and_then(|s| s.strip_suffix('"'))
500        .unwrap_or(text);
501
502    // Process escape sequences
503    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        // Simple and
706        let result = parse("{{ and .Values.a .Values.b }}");
707        assert!(result.is_ok(), "Simple and failed: {:?}", result);
708
709        // And with parenthesized eq
710        let result = parse("{{ and (eq .Values.a \"x\") .Values.b }}");
711        assert!(result.is_ok(), "And with eq failed: {:?}", result);
712
713        // Full nested
714        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}