sherpack_convert/
ast.rs

1//! AST (Abstract Syntax Tree) for Go templates
2//!
3//! These structures represent the parsed Go template syntax,
4//! which will be transformed into Jinja2 syntax.
5
6use std::fmt;
7
8/// A complete Go template
9#[derive(Debug, Clone, PartialEq)]
10pub struct Template {
11    pub elements: Vec<Element>,
12}
13
14/// An element in a template: either raw text or an action
15#[derive(Debug, Clone, PartialEq)]
16pub enum Element {
17    /// Raw text (not inside {{ }})
18    RawText(String),
19    /// An action (inside {{ }})
20    Action(Action),
21}
22
23/// An action (directive inside {{ }})
24#[derive(Debug, Clone, PartialEq)]
25pub struct Action {
26    /// Whether the action has left whitespace trimming ({{-)
27    pub trim_left: bool,
28    /// Whether the action has right whitespace trimming (-}})
29    pub trim_right: bool,
30    /// The action body
31    pub body: ActionBody,
32}
33
34/// The body of an action
35#[derive(Debug, Clone, PartialEq)]
36pub enum ActionBody {
37    /// Comment: {{/* comment */}}
38    Comment(String),
39    /// If: {{- if .X }}
40    If(Pipeline),
41    /// Else if: {{- else if .X }}
42    ElseIf(Pipeline),
43    /// Else: {{- else }}
44    Else,
45    /// End: {{- end }}
46    End,
47    /// Range: {{- range .X }} or {{- range $i, $v := .X }}
48    Range {
49        /// Optional variable declarations ($i, $v)
50        vars: Option<RangeVars>,
51        /// The pipeline to iterate over
52        pipeline: Pipeline,
53    },
54    /// With: {{- with .X }}
55    With(Pipeline),
56    /// Define: {{- define "name" }}
57    Define(String),
58    /// Template: {{ template "name" . }}
59    Template {
60        name: String,
61        pipeline: Option<Pipeline>,
62    },
63    /// Block: {{- block "name" . }}
64    Block { name: String, pipeline: Pipeline },
65    /// A pipeline expression (variable access, function call, etc.)
66    Pipeline(Pipeline),
67}
68
69/// Variables in a range clause: $i, $v := ...
70#[derive(Debug, Clone, PartialEq)]
71pub struct RangeVars {
72    /// Index variable (optional): $i in `range $i, $v := .X`
73    pub index_var: Option<String>,
74    /// Value variable: $v in `range $v := .X` or `range $i, $v := .X`
75    pub value_var: String,
76}
77
78/// A pipeline: a sequence of commands separated by |
79#[derive(Debug, Clone, PartialEq)]
80pub struct Pipeline {
81    /// Optional variable declaration: $x := ...
82    pub decl: Option<String>,
83    /// The commands in the pipeline
84    pub commands: Vec<Command>,
85}
86
87impl Pipeline {
88    /// Create a simple pipeline with one command
89    pub fn simple(cmd: Command) -> Self {
90        Self {
91            decl: None,
92            commands: vec![cmd],
93        }
94    }
95
96    /// Create a pipeline with a declaration
97    pub fn with_decl(var: String, commands: Vec<Command>) -> Self {
98        Self {
99            decl: Some(var),
100            commands,
101        }
102    }
103}
104
105/// A command in a pipeline
106#[derive(Debug, Clone, PartialEq)]
107pub enum Command {
108    /// Field access: .Values.x or $.Values.x
109    Field(FieldAccess),
110    /// Variable: $x
111    Variable(String),
112    /// Function call: funcName arg1 arg2
113    Function { name: String, args: Vec<Argument> },
114    /// Literal value
115    Literal(Literal),
116    /// Parenthesized pipeline
117    Parenthesized(Box<Pipeline>),
118}
119
120/// Field access: .Values.image.tag
121#[derive(Debug, Clone, PartialEq)]
122pub struct FieldAccess {
123    /// Whether this is a root access ($.Values vs .Values)
124    pub is_root: bool,
125    /// The path components: ["Values", "image", "tag"]
126    pub path: Vec<String>,
127}
128
129impl FieldAccess {
130    pub fn new(path: Vec<String>) -> Self {
131        Self {
132            is_root: false,
133            path,
134        }
135    }
136
137    pub fn root(path: Vec<String>) -> Self {
138        Self {
139            is_root: true,
140            path,
141        }
142    }
143
144    /// Get the full path as a dot-separated string
145    pub fn full_path(&self) -> String {
146        self.path.join(".")
147    }
148}
149
150/// An argument to a function
151#[derive(Debug, Clone, PartialEq)]
152pub enum Argument {
153    Field(FieldAccess),
154    Variable(String),
155    Literal(Literal),
156    Pipeline(Box<Pipeline>),
157}
158
159/// A literal value
160#[derive(Debug, Clone, PartialEq)]
161pub enum Literal {
162    String(String),
163    Char(char),
164    Int(i64),
165    Float(f64),
166    Bool(bool),
167    Nil,
168}
169
170impl fmt::Display for Literal {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        match self {
173            Literal::String(s) => write!(f, "\"{}\"", s),
174            Literal::Char(c) => write!(f, "'{}'", c),
175            Literal::Int(n) => write!(f, "{}", n),
176            Literal::Float(n) => write!(f, "{}", n),
177            Literal::Bool(b) => write!(f, "{}", b),
178            Literal::Nil => write!(f, "nil"),
179        }
180    }
181}
182
183/// Location in source for error reporting
184#[derive(Debug, Clone, PartialEq)]
185pub struct SourceLocation {
186    pub line: usize,
187    pub column: usize,
188    pub offset: usize,
189}
190
191impl Default for SourceLocation {
192    fn default() -> Self {
193        Self {
194            line: 1,
195            column: 1,
196            offset: 0,
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_field_access() {
207        let field = FieldAccess::new(vec!["Values".into(), "image".into(), "tag".into()]);
208        assert_eq!(field.full_path(), "Values.image.tag");
209        assert!(!field.is_root);
210    }
211
212    #[test]
213    fn test_field_access_root() {
214        let field = FieldAccess::root(vec!["Values".into(), "x".into()]);
215        assert!(field.is_root);
216    }
217
218    #[test]
219    fn test_pipeline_simple() {
220        let pipeline = Pipeline::simple(Command::Variable("x".into()));
221        assert!(pipeline.decl.is_none());
222        assert_eq!(pipeline.commands.len(), 1);
223    }
224
225    #[test]
226    fn test_literal_display() {
227        assert_eq!(format!("{}", Literal::String("hello".into())), "\"hello\"");
228        assert_eq!(format!("{}", Literal::Int(42)), "42");
229        assert_eq!(format!("{}", Literal::Bool(true)), "true");
230        assert_eq!(format!("{}", Literal::Nil), "nil");
231    }
232}