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
18pub struct StormchaserParser;
20
21impl Default for StormchaserParser {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl StormchaserParser {
28 pub fn new() -> Self {
30 Self
31 }
32
33 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
588fn 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
600fn 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
617fn 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 Ok(Value::String(format!("${{{}}}", expr)))
713 }
714 Expression::Variable(_) => {
715 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 Ok(Value::String(s[1..s.len() - 1].to_string()))
723 } else if s.starts_with("<<") {
724 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 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}