use std::{collections::BTreeMap, fs, path::Path}; use kdl::{KdlDocument, KdlNode}; use miette::{IntoDiagnostic, Report, Result}; #[derive(Debug, Clone)] pub struct Workflow { pub name: Option, pub jobs: BTreeMap, } #[derive(Debug, Clone)] pub struct Job { pub id: String, pub runs_on: Option, pub steps: Vec, } #[derive(Debug, Clone)] pub struct Step { pub name: Option, pub run: String, } pub fn parse_workflow_file>(path: P) -> Result { let s = fs::read_to_string(path).into_diagnostic()?; parse_workflow_str(&s) } pub fn parse_workflow_str(s: &str) -> Result { let doc: KdlDocument = s.parse().into_diagnostic()?; // Expect a top-level `workflow` node let wf_node = doc .nodes() .iter() .find(|n| n.name().value() == "workflow") .ok_or_else(|| Report::msg("missing `workflow {}` root node"))?; let name = wf_node .get("name") .and_then(|e| e.value().as_string()) .map(|s| s.to_string()); // Child nodes are within the workflow node body let mut jobs = BTreeMap::new(); if let Some(children) = wf_node.children() { for node in children.nodes().iter() { if node.name().value() == "job" { let job = parse_job(node)?; jobs.insert(job.id.clone(), job); } } } Ok(Workflow { name, jobs }) } fn parse_job(node: &KdlNode) -> Result { let id = node .get("id") .and_then(|e| e.value().as_string()) .map(|s| s.to_string()) .ok_or_else(|| Report::msg("job missing string `id` property"))?; let runs_on = node .get("runs_on") .and_then(|e| e.value().as_string()) .map(|s| s.to_string()); let mut steps = Vec::new(); if let Some(children) = node.children() { for child in children.nodes() { if child.name().value() == "step" { steps.push(parse_step(child)?); } } } Ok(Job { id, runs_on, steps }) } fn parse_step(node: &KdlNode) -> Result { let run = node .get("run") .and_then(|e| e.value().as_string()) .map(|s| s.to_string()) .ok_or_else(|| Report::msg("step missing string `run` property"))?; let name = node .get("name") .and_then(|e| e.value().as_string()) .map(|s| s.to_string()); Ok(Step { name, run }) }