wrkflw_parser/
workflow.rs1use serde::{Deserialize, Deserializer, Serialize};
2use std::collections::HashMap;
3use std::fs;
4use std::path::Path;
5use wrkflw_matrix::MatrixConfig;
6
7use super::schema::SchemaValidator;
8
9fn deserialize_needs<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
11where
12 D: Deserializer<'de>,
13{
14 #[derive(Deserialize)]
15 #[serde(untagged)]
16 enum StringOrVec {
17 String(String),
18 Vec(Vec<String>),
19 }
20
21 let value = Option::<StringOrVec>::deserialize(deserializer)?;
22 match value {
23 Some(StringOrVec::String(s)) => Ok(Some(vec![s])),
24 Some(StringOrVec::Vec(v)) => Ok(Some(v)),
25 None => Ok(None),
26 }
27}
28
29#[derive(Debug, Deserialize, Serialize)]
30pub struct WorkflowDefinition {
31 pub name: String,
32 #[serde(skip, default)] pub on: Vec<String>,
34 #[serde(rename = "on")] pub on_raw: serde_yaml::Value,
36 pub jobs: HashMap<String, Job>,
37}
38
39#[derive(Debug, Deserialize, Serialize)]
40pub struct Job {
41 #[serde(rename = "runs-on")]
42 pub runs_on: String,
43 #[serde(default, deserialize_with = "deserialize_needs")]
44 pub needs: Option<Vec<String>>,
45 pub steps: Vec<Step>,
46 #[serde(default)]
47 pub env: HashMap<String, String>,
48 #[serde(default)]
49 pub matrix: Option<MatrixConfig>,
50 #[serde(default)]
51 pub services: HashMap<String, Service>,
52 #[serde(default, rename = "if")]
53 pub if_condition: Option<String>,
54 #[serde(default)]
55 pub outputs: Option<HashMap<String, String>>,
56 #[serde(default)]
57 pub permissions: Option<HashMap<String, String>>,
58}
59
60#[derive(Debug, Deserialize, Serialize)]
61pub struct Service {
62 pub image: String,
63 #[serde(default)]
64 pub ports: Option<Vec<String>>,
65 #[serde(default)]
66 pub env: HashMap<String, String>,
67 #[serde(default)]
68 pub volumes: Option<Vec<String>>,
69 #[serde(default)]
70 pub options: Option<String>,
71}
72
73#[derive(Debug, Deserialize, Serialize)]
74pub struct Step {
75 #[serde(default)]
76 pub name: Option<String>,
77 #[serde(default)]
78 pub uses: Option<String>,
79 #[serde(default)]
80 pub run: Option<String>,
81 #[serde(default)]
82 pub with: Option<HashMap<String, String>>,
83 #[serde(default)]
84 pub env: HashMap<String, String>,
85 #[serde(default)]
86 pub continue_on_error: Option<bool>,
87}
88
89impl WorkflowDefinition {
90 pub fn resolve_action(&self, action_ref: &str) -> ActionInfo {
91 let parts: Vec<&str> = action_ref.split('@').collect();
93
94 let (repo, _) = if parts.len() > 1 {
95 (parts[0], parts[1])
96 } else {
97 (parts[0], "main") };
99
100 ActionInfo {
101 repository: repo.to_string(),
102 is_docker: repo.starts_with("docker://"),
103 is_local: repo.starts_with("./"),
104 }
105 }
106}
107
108#[derive(Debug, Clone)]
109pub struct ActionInfo {
110 pub repository: String,
111 pub is_docker: bool,
112 pub is_local: bool,
113}
114
115pub fn parse_workflow(path: &Path) -> Result<WorkflowDefinition, String> {
116 let validator = SchemaValidator::new()?;
118 validator.validate_workflow(path)?;
119
120 let content =
122 fs::read_to_string(path).map_err(|e| format!("Failed to read workflow file: {}", e))?;
123
124 let mut workflow: WorkflowDefinition = serde_yaml::from_str(&content)
126 .map_err(|e| format!("Failed to parse workflow structure: {}", e))?;
127
128 workflow.on = normalize_triggers(&workflow.on_raw)?;
130
131 Ok(workflow)
132}
133
134fn normalize_triggers(on_value: &serde_yaml::Value) -> Result<Vec<String>, String> {
135 let mut triggers = Vec::new();
136
137 match on_value {
138 serde_yaml::Value::String(event) => {
140 triggers.push(event.clone());
141 }
142 serde_yaml::Value::Sequence(events) => {
144 for event in events {
145 if let Some(event_str) = event.as_str() {
146 triggers.push(event_str.to_string());
147 }
148 }
149 }
150 serde_yaml::Value::Mapping(events_map) => {
152 for (event, _) in events_map {
153 if let Some(event_str) = event.as_str() {
154 triggers.push(event_str.to_string());
155 }
156 }
157 }
158 _ => {
159 return Err("'on' section has invalid format".to_string());
160 }
161 }
162
163 Ok(triggers)
164}