Skip to main content

stormchaser_dsl/
parser.rs

1use anyhow::{Context, Result};
2use hcl::{Block, Body, Expression};
3use serde_json::{json, Map, Value};
4use std::collections::HashMap;
5
6use crate::ast::{Step, Workflow};
7use stormchaser_model::dsl;
8
9type ParsedWorkflowBlocks = (
10    Vec<Step>,
11    Vec<dsl::Storage>,
12    Vec<dsl::Input>,
13    Vec<dsl::Output>,
14    Vec<dsl::StepLibrary>,
15    Vec<dsl::Include>,
16);
17
18/// A parser for translating Stormchaser HCL DSL into an executable Workflow model.
19pub struct StormchaserParser;
20
21impl Default for StormchaserParser {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl StormchaserParser {
28    /// Creates a new instance of the `StormchaserParser`.
29    pub fn new() -> Self {
30        Self
31    }
32
33    /// Parses the provided HCL DSL string and returns a `Workflow` instance.
34    pub fn parse(&self, dsl: &str) -> Result<Workflow> {
35        let body: Body = hcl::from_str(dsl)?;
36
37        let mut dsl_version = String::new();
38        for attribute in body.attributes() {
39            if attribute.key() == "stormchaser_dsl_version" {
40                dsl_version = expr_to_string(attribute.expr())?;
41            }
42        }
43
44        let workflow_block = body
45            .blocks()
46            .find(|b| b.identifier() == "workflow" || b.identifier() == "workflow_template")
47            .context("Missing 'workflow' or 'workflow_template' block")?;
48
49        let is_template = workflow_block.identifier() == "workflow_template";
50
51        let name = workflow_block
52            .labels()
53            .first()
54            .map(|l| l.as_str().to_string())
55            .context("Workflow block must have a name label")?;
56
57        let mut description = None;
58        let mut cron = None;
59
60        for attribute in workflow_block.body().attributes() {
61            if attribute.key() == "description" {
62                description = Some(expr_to_string(attribute.expr())?);
63            } else if attribute.key() == "cron" {
64                cron = Some(expr_to_string(attribute.expr())?);
65            }
66        }
67
68        let (steps, storage, inputs, outputs, step_libraries, includes) =
69            self.parse_workflow_blocks(workflow_block.body())?;
70
71        Ok(Workflow {
72            is_template,
73            dsl_version,
74            name,
75            description,
76            cron,
77            libraries: vec![],
78            step_libraries,
79            includes,
80            strategy: None,
81            quotas: None,
82            storage,
83            inputs,
84            outputs,
85            handlers: vec![],
86            steps,
87            on_failure: None,
88            finally: None,
89        })
90    }
91
92    fn parse_workflow_blocks(&self, body: &Body) -> Result<ParsedWorkflowBlocks> {
93        let mut steps = Vec::new();
94        let mut storage = Vec::new();
95        let mut inputs = Vec::new();
96        let mut outputs = Vec::new();
97        let mut step_libraries = Vec::new();
98        let mut includes = Vec::new();
99
100        for block in body.blocks() {
101            match block.identifier() {
102                "steps" => {
103                    steps = self.parse_steps(block.body())?;
104                }
105                "storage" => {
106                    storage.push(self.parse_storage_block(block)?);
107                }
108                "input" => {
109                    inputs.push(self.parse_input_block(block)?);
110                }
111                "output" => {
112                    outputs.push(self.parse_output_block(block)?);
113                }
114                "step_library" => {
115                    step_libraries.push(self.parse_step_library_block(block)?);
116                }
117                "include" => {
118                    includes.push(self.parse_include_block(block)?);
119                }
120                _ => {}
121            }
122        }
123
124        Ok((steps, storage, inputs, outputs, step_libraries, includes))
125    }
126
127    fn parse_storage_block(&self, block: &Block) -> Result<dsl::Storage> {
128        let name = block
129            .labels()
130            .first()
131            .map(|l| l.as_str().to_string())
132            .context("Storage block must have a name label")?;
133        let mut size = "1Gi".to_string();
134        let mut backend = None;
135        let mut provision = Vec::new();
136        let mut artifacts = Vec::new();
137
138        for attr in block.body().attributes() {
139            if attr.key() == "size" {
140                size = expr_to_string(attr.expr())?;
141            } else if attr.key() == "backend" {
142                backend = Some(expr_to_string(attr.expr())?);
143            }
144        }
145
146        for inner in block.body().blocks() {
147            if inner.identifier() == "artifact" {
148                let art_name = inner
149                    .labels()
150                    .first()
151                    .map(|l| l.as_str().to_string())
152                    .context("Artifact block must have a name label")?;
153                let mut path = String::new();
154                let mut retention = "on_success".to_string();
155                for attr in inner.body().attributes() {
156                    if attr.key() == "path" {
157                        path = expr_to_string(attr.expr())?;
158                    } else if attr.key() == "retention" {
159                        retention = expr_to_string(attr.expr())?;
160                    }
161                }
162                artifacts.push(dsl::Artifact {
163                    name: art_name,
164                    path,
165                    retention,
166                });
167            } else if inner.identifier() == "provision" {
168                if inner.labels().is_empty() {
169                    for prov_block in inner.body().blocks() {
170                        provision.push(parse_provision_sub_block(prov_block)?);
171                    }
172                } else {
173                    provision.push(parse_provision_legacy_block(inner)?);
174                }
175            }
176        }
177
178        Ok(dsl::Storage {
179            name,
180            backend,
181            size,
182            provision,
183            preserve: vec![],
184            artifacts,
185            retainment: None,
186        })
187    }
188
189    fn parse_input_block(&self, block: &Block) -> Result<dsl::Input> {
190        let name = block
191            .labels()
192            .first()
193            .map(|l| l.as_str().to_string())
194            .context("Input block must have a name label")?;
195        let mut r#type = "string".to_string();
196        let mut default = None;
197        for attr in block.body().attributes() {
198            if attr.key() == "type" {
199                r#type = expr_to_string(attr.expr())?;
200            } else if attr.key() == "default" {
201                default = Some(expr_to_value(attr.expr())?);
202            }
203        }
204        Ok(dsl::Input {
205            name,
206            r#type,
207            description: None,
208            default,
209            validation: None,
210            options: None,
211            query: None,
212        })
213    }
214
215    fn parse_output_block(&self, block: &Block) -> Result<dsl::Output> {
216        let name = block
217            .labels()
218            .first()
219            .map(|l| l.as_str().to_string())
220            .context("Output block must have a name label")?;
221        let mut value = String::new();
222        for attr in block.body().attributes() {
223            if attr.key() == "value" {
224                value = expr_to_string(attr.expr())?;
225            }
226        }
227        Ok(dsl::Output { name, value })
228    }
229
230    fn parse_step_library_block(&self, block: &Block) -> Result<dsl::StepLibrary> {
231        let name = block
232            .labels()
233            .first()
234            .map(|l| l.as_str().to_string())
235            .context("step_library block must have a name label")?;
236        let mut r#type = String::new();
237        let mut params = HashMap::new();
238        let mut spec_map = Map::new();
239        let mut timeout = None;
240        let mut allow_failure = None;
241
242        for attr in block.body().attributes() {
243            match attr.key() {
244                "type" => r#type = expr_to_string(attr.expr())?,
245                "params" => {
246                    if let Value::Object(obj) = expr_to_value(attr.expr())? {
247                        for (k, v) in obj {
248                            if let Value::String(s) = v {
249                                params.insert(k, s);
250                            }
251                        }
252                    }
253                }
254                "timeout" => timeout = Some(expr_to_string(attr.expr())?),
255                "allow_failure" => {
256                    if let Value::Bool(b) = expr_to_value(attr.expr())? {
257                        allow_failure = Some(b);
258                    }
259                }
260                _ => {}
261            }
262        }
263
264        for inner in block.body().blocks() {
265            if inner.identifier() == "spec" {
266                for attr in inner.body().attributes() {
267                    spec_map.insert(attr.key().to_string(), expr_to_value(attr.expr())?);
268                }
269                for inner_block in inner.body().blocks() {
270                    let key = inner_block.identifier().to_string();
271                    let value = block_to_value(inner_block)?;
272                    spec_map.insert(key, value);
273                }
274            } else if inner.identifier() == "params" {
275                for attr in inner.body().attributes() {
276                    params.insert(attr.key().to_string(), expr_to_string(attr.expr())?);
277                }
278            }
279        }
280
281        Ok(dsl::StepLibrary {
282            name,
283            r#type,
284            params,
285            spec: Value::Object(spec_map),
286            timeout,
287            allow_failure,
288            retry: None,
289        })
290    }
291
292    fn parse_include_block(&self, block: &Block) -> Result<dsl::Include> {
293        let name = block
294            .labels()
295            .first()
296            .map(|l| l.as_str().to_string())
297            .context("include block must have a name label")?;
298        let mut workflow = String::new();
299        let mut inputs_map = HashMap::new();
300
301        for attr in block.body().attributes() {
302            if attr.key() == "workflow" {
303                workflow = expr_to_string(attr.expr())?;
304            } else if attr.key() == "inputs" {
305                if let Value::Object(obj) = expr_to_value(attr.expr())? {
306                    for (k, v) in obj {
307                        if let Value::String(s) = v {
308                            inputs_map.insert(k, s);
309                        } else {
310                            inputs_map.insert(k, v.to_string());
311                        }
312                    }
313                }
314            }
315        }
316
317        Ok(dsl::Include {
318            name,
319            workflow,
320            inputs: inputs_map,
321        })
322    }
323
324    fn parse_step_strategy_block(&self, inner_block: &Block) -> Result<dsl::Strategy> {
325        let mut s = dsl::Strategy {
326            affinity: None,
327            fail_fast: None,
328            max_parallel: None,
329            process_allow_list: None,
330        };
331        for attr in inner_block.body().attributes() {
332            match attr.key() {
333                "affinity" => s.affinity = Some(expr_to_string(attr.expr())?),
334                "fail_fast" => {
335                    if let Value::Bool(b) = expr_to_value(attr.expr())? {
336                        s.fail_fast = Some(b);
337                    }
338                }
339                "max_parallel" => {
340                    if let Value::Number(n) = expr_to_value(attr.expr())? {
341                        s.max_parallel = n.as_u64().map(|v| v as u32);
342                    }
343                }
344                _ => {}
345            }
346        }
347        Ok(s)
348    }
349
350    fn parse_step_reports_block(&self, inner_block: &Block) -> Result<Vec<dsl::TestReport>> {
351        let mut reports = Vec::new();
352        for report_block in inner_block.body().blocks() {
353            if report_block.identifier() == "report" {
354                let art_name = report_block
355                    .labels()
356                    .first()
357                    .map(|l| l.as_str().to_string())
358                    .context("Report block must have a name label")?;
359
360                let mut path = String::new();
361                let mut format = "junit".to_string();
362                for attr in report_block.body().attributes() {
363                    if attr.key() == "path" {
364                        path = expr_to_string(attr.expr())?;
365                    } else if attr.key() == "format" {
366                        format = expr_to_string(attr.expr())?;
367                    }
368                }
369                reports.push(dsl::TestReport {
370                    name: art_name,
371                    path,
372                    format,
373                });
374            }
375        }
376        Ok(reports)
377    }
378
379    fn parse_step_outputs_block(
380        &self,
381        inner_block: &Block,
382    ) -> Result<(Option<String>, Option<String>, Vec<dsl::OutputExtraction>)> {
383        let mut start_marker = None;
384        let mut end_marker = None;
385        let mut outputs = Vec::new();
386
387        for attr in inner_block.body().attributes() {
388            if attr.key() == "start_marker" {
389                start_marker = Some(expr_to_string(attr.expr())?);
390            } else if attr.key() == "end_marker" {
391                end_marker = Some(expr_to_string(attr.expr())?);
392            }
393        }
394        for output_block in inner_block.body().blocks() {
395            if output_block.identifier() == "output" {
396                let name = output_block
397                    .labels()
398                    .first()
399                    .map(|l| l.as_str().to_string())
400                    .context("Output block must have a name label")?;
401
402                let mut source = "logs".to_string();
403                let mut marker = None;
404                let mut format = None;
405                let mut regex = None;
406                let mut group = None;
407                let mut sensitive = None;
408
409                for attr in output_block.body().attributes() {
410                    match attr.key() {
411                        "source" => {
412                            source = expr_to_string(attr.expr())?;
413                        }
414                        "marker" => {
415                            marker = Some(expr_to_string(attr.expr())?);
416                        }
417                        "format" => {
418                            format = Some(expr_to_string(attr.expr())?);
419                        }
420                        "regex" => {
421                            regex = Some(expr_to_string(attr.expr())?);
422                        }
423                        "group" => {
424                            if let Value::Number(n) = expr_to_value(attr.expr())? {
425                                group = n.as_u64().map(|v| v as u32);
426                            }
427                        }
428                        "sensitive" => {
429                            if let Value::Bool(b) = expr_to_value(attr.expr())? {
430                                sensitive = Some(b);
431                            }
432                        }
433                        _ => {}
434                    }
435                }
436
437                outputs.push(dsl::OutputExtraction {
438                    name,
439                    source,
440                    marker,
441                    format,
442                    regex,
443                    group,
444                    sensitive,
445                });
446            }
447        }
448        Ok((start_marker, end_marker, outputs))
449    }
450
451    pub fn parse_steps(&self, body: &Body) -> Result<Vec<Step>> {
452        let mut steps = Vec::new();
453        for block in body.blocks() {
454            if block.identifier() == "step" {
455                steps.push(self.parse_single_step(block)?);
456            }
457        }
458        Ok(steps)
459    }
460
461    fn parse_single_step(&self, block: &Block) -> Result<Step> {
462        let name = block
463            .labels()
464            .first()
465            .map(|l| l.as_str().to_string())
466            .context("Step block must have a name label")?;
467        let r#type = block
468            .labels()
469            .get(1)
470            .map(|l| l.as_str().to_string())
471            .context("Step block must have a type label")?;
472
473        let mut step = Step {
474            name,
475            r#type,
476            condition: None,
477            params: HashMap::new(),
478            spec: Value::Null,
479            strategy: None,
480            aggregation: vec![],
481            iterate: None,
482            iterate_as: None,
483            steps: None,
484            next: vec![],
485            on_failure: None,
486            retry: None,
487            timeout: None,
488            allow_failure: None,
489            start_marker: None,
490            end_marker: None,
491            outputs: vec![],
492            reports: vec![],
493            artifacts: None,
494        };
495
496        let mut spec_map = Map::new();
497
498        self.apply_step_attributes(block, &mut step, &mut spec_map)?;
499        self.apply_step_inner_blocks(block, &mut step, &mut spec_map)?;
500
501        step.spec = Value::Object(spec_map);
502        Ok(step)
503    }
504
505    fn apply_step_attributes(
506        &self,
507        block: &Block,
508        step: &mut Step,
509        spec_map: &mut Map<String, Value>,
510    ) -> Result<()> {
511        for attr in block.body().attributes() {
512            match attr.key() {
513                "condition" => step.condition = Some(expr_to_string(attr.expr())?),
514                "next" => step.next = expr_to_string_vec(attr.expr())?,
515                "iterate" => step.iterate = Some(expr_to_string(attr.expr())?),
516                "iterate_as" | "as" => step.iterate_as = Some(expr_to_string(attr.expr())?),
517                "allow_failure" => {
518                    if let Value::Bool(b) = expr_to_value(attr.expr())? {
519                        step.allow_failure = Some(b);
520                    }
521                }
522                "timeout" => step.timeout = Some(expr_to_string(attr.expr())?),
523                "artifacts" => step.artifacts = Some(expr_to_string_vec(attr.expr())?),
524                _ => {
525                    spec_map.insert(attr.key().to_string(), expr_to_value(attr.expr())?);
526                }
527            }
528        }
529        Ok(())
530    }
531
532    fn apply_step_inner_blocks(
533        &self,
534        block: &Block,
535        step: &mut Step,
536        spec_map: &mut Map<String, Value>,
537    ) -> Result<()> {
538        for inner_block in block.body().blocks() {
539            match inner_block.identifier() {
540                "params" => {
541                    for attr in inner_block.body().attributes() {
542                        step.params
543                            .insert(attr.key().to_string(), expr_to_string(attr.expr())?);
544                    }
545                }
546                "steps" => {
547                    step.steps = Some(self.parse_steps(inner_block.body())?);
548                }
549                "strategy" => {
550                    step.strategy = Some(self.parse_step_strategy_block(inner_block)?);
551                }
552                "reports" => {
553                    step.reports
554                        .extend(self.parse_step_reports_block(inner_block)?);
555                }
556                "outputs" => {
557                    let (sm, em, out) = self.parse_step_outputs_block(inner_block)?;
558                    if sm.is_some() {
559                        step.start_marker = sm;
560                    }
561                    if em.is_some() {
562                        step.end_marker = em;
563                    }
564                    step.outputs.extend(out);
565                }
566                "spec" => {
567                    for attr in inner_block.body().attributes() {
568                        spec_map.insert(attr.key().to_string(), expr_to_value(attr.expr())?);
569                    }
570                    for nested_block in inner_block.body().blocks() {
571                        let key = nested_block.identifier().to_string();
572                        let value = block_to_value(nested_block)?;
573                        spec_map.insert(key, value);
574                    }
575                }
576                _ => {
577                    spec_map.insert(
578                        inner_block.identifier().to_string(),
579                        block_to_value(inner_block)?,
580                    );
581                }
582            }
583        }
584        Ok(())
585    }
586}
587
588/// Parses a provision sub-block in the new syntax:
589/// `provision { <resource_type> "name" { ... } }`
590fn parse_provision_sub_block(prov_block: &Block) -> Result<dsl::Provision> {
591    let resource_type = prov_block.identifier().to_string();
592    let name = prov_block
593        .labels()
594        .first()
595        .map(|l| l.as_str().to_string())
596        .context("Provision sub-block must have a name label")?;
597    parse_provision_attributes(name, resource_type, prov_block.body())
598}
599
600/// Parses a provision block in the legacy syntax:
601/// `provision "name" { resource_type = "download" ... }`
602fn parse_provision_legacy_block(block: &Block) -> Result<dsl::Provision> {
603    let name = block
604        .labels()
605        .first()
606        .map(|l| l.as_str().to_string())
607        .context("Legacy provision block must have a name label")?;
608    let mut resource_type = "download".to_string();
609    for attr in block.body().attributes() {
610        if attr.key() == "resource_type" {
611            resource_type = expr_to_string(attr.expr())?;
612        }
613    }
614    parse_provision_attributes(name, resource_type, block.body())
615}
616
617/// Extracts the common provision fields from a block body, for both syntaxes.
618fn parse_provision_attributes(
619    name: String,
620    resource_type: String,
621    body: &Body,
622) -> Result<dsl::Provision> {
623    let mut source = None;
624    let mut url = None;
625    let mut destination = "/".to_string();
626    let mut mode = None;
627    let mut checksum = None;
628    let mut from = None;
629
630    for attr in body.attributes() {
631        match attr.key() {
632            "source" => source = Some(expr_to_string(attr.expr())?),
633            "url" => url = Some(expr_to_string(attr.expr())?),
634            "destination" => destination = expr_to_string(attr.expr())?,
635            "mode" => mode = Some(expr_to_string(attr.expr())?),
636            "checksum" => checksum = Some(expr_to_string(attr.expr())?),
637            "from" => from = Some(expr_to_string(attr.expr())?),
638            _ => {}
639        }
640    }
641
642    Ok(dsl::Provision {
643        name,
644        resource_type,
645        source,
646        url,
647        destination,
648        mode,
649        checksum,
650        from,
651    })
652}
653
654fn expr_to_string(expr: &Expression) -> Result<String> {
655    match expr_to_value(expr)? {
656        Value::String(s) => Ok(s),
657        Value::Number(n) => Ok(n.to_string()),
658        Value::Bool(b) => Ok(b.to_string()),
659        other => Ok(other.to_string()),
660    }
661}
662
663fn expr_to_string_vec(expr: &Expression) -> Result<Vec<String>> {
664    match expr {
665        Expression::Array(arr) => {
666            let mut result = Vec::new();
667            for item in arr {
668                if let Expression::String(s) = item {
669                    result.push(s.clone());
670                } else if let Ok(s) = expr_to_string(item) {
671                    result.push(s);
672                }
673            }
674            Ok(result)
675        }
676        _ => Ok(vec![]),
677    }
678}
679
680fn expr_to_value(expr: &Expression) -> Result<Value> {
681    match expr {
682        Expression::String(s) => Ok(Value::String(s.clone())),
683        Expression::Number(n) => {
684            if let Some(i) = n.as_i64() {
685                Ok(json!(i))
686            } else if let Some(u) = n.as_u64() {
687                Ok(json!(u))
688            } else if let Some(f) = n.as_f64() {
689                Ok(json!(f))
690            } else {
691                Ok(json!(0))
692            }
693        }
694        Expression::Bool(b) => Ok(Value::Bool(*b)),
695        Expression::Null => Ok(Value::Null),
696        Expression::Array(arr) => {
697            let mut vals = Vec::new();
698            for e in arr {
699                vals.push(expr_to_value(e)?);
700            }
701            Ok(Value::Array(vals))
702        }
703        Expression::Object(obj) => {
704            let mut map = Map::new();
705            for (k, v) in obj {
706                map.insert(k.to_string(), expr_to_value(v)?);
707            }
708            Ok(Value::Object(map))
709        }
710        Expression::Traversal(_) => {
711            // Convert HCL traversal (e.g. inputs.repo_url) to ${inputs.repo_url}
712            Ok(Value::String(format!("${{{}}}", expr)))
713        }
714        Expression::Variable(_) => {
715            // Convert HCL variable (e.g. var_name) to ${var_name}
716            Ok(Value::String(format!("${{{}}}", expr)))
717        }
718        Expression::TemplateExpr(t) => {
719            let s = t.to_string();
720            if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
721                // Strip outer quotes from quoted template
722                Ok(Value::String(s[1..s.len() - 1].to_string()))
723            } else if s.starts_with("<<") {
724                // Heuristic for heredoc: find first newline and last newline
725                let lines: Vec<&str> = s.lines().collect();
726                if lines.len() >= 2 {
727                    let content = lines[1..lines.len() - 1].join("\n");
728                    Ok(Value::String(content.trim().to_string()))
729                } else {
730                    Ok(Value::String(s))
731                }
732            } else {
733                Ok(Value::String(s))
734            }
735        }
736        _ => {
737            // Fallback for more complex expressions like function calls if needed.
738            let expr_str = expr.to_string();
739            Ok(serde_json::from_str(&expr_str).unwrap_or(Value::String(expr_str)))
740        }
741    }
742}
743
744fn block_to_value(block: &Block) -> Result<Value> {
745    let mut map = Map::new();
746    for attr in block.body().attributes() {
747        map.insert(attr.key().to_string(), expr_to_value(attr.expr())?);
748    }
749    for inner in block.body().blocks() {
750        map.insert(inner.identifier().to_string(), block_to_value(inner)?);
751    }
752    Ok(Value::Object(map))
753}