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
29fn deserialize_runs_on<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
31where
32 D: Deserializer<'de>,
33{
34 #[derive(Deserialize)]
35 #[serde(untagged)]
36 enum StringOrVec {
37 String(String),
38 Vec(Vec<String>),
39 }
40
41 let value = Option::<StringOrVec>::deserialize(deserializer)?;
42 match value {
43 Some(StringOrVec::String(s)) => Ok(Some(vec![s])),
44 Some(StringOrVec::Vec(v)) => Ok(Some(v)),
45 None => Ok(None),
46 }
47}
48
49#[derive(Debug, Deserialize, Serialize)]
50pub struct WorkflowDefinition {
51 pub name: String,
52 #[serde(skip, default)] pub on: Vec<String>,
54 #[serde(rename = "on")] pub on_raw: serde_yaml::Value,
56 pub jobs: HashMap<String, Job>,
57}
58
59#[derive(Debug, Deserialize, Serialize)]
60pub struct Job {
61 #[serde(rename = "runs-on", default, deserialize_with = "deserialize_runs_on")]
62 pub runs_on: Option<Vec<String>>,
63 #[serde(default, deserialize_with = "deserialize_needs")]
64 pub needs: Option<Vec<String>>,
65 #[serde(default)]
66 pub steps: Vec<Step>,
67 #[serde(default)]
68 pub env: HashMap<String, String>,
69 #[serde(default)]
70 pub matrix: Option<MatrixConfig>,
71 #[serde(default)]
72 pub services: HashMap<String, Service>,
73 #[serde(default, rename = "if")]
74 pub if_condition: Option<String>,
75 #[serde(default)]
76 pub outputs: Option<HashMap<String, String>>,
77 #[serde(default)]
78 pub permissions: Option<HashMap<String, String>>,
79 #[serde(default)]
81 pub uses: Option<String>,
82 #[serde(default)]
83 pub with: Option<HashMap<String, String>>,
84 #[serde(default)]
85 pub secrets: Option<serde_yaml::Value>,
86}
87
88#[derive(Debug, Deserialize, Serialize)]
89pub struct Service {
90 pub image: String,
91 #[serde(default)]
92 pub ports: Option<Vec<String>>,
93 #[serde(default)]
94 pub env: HashMap<String, String>,
95 #[serde(default)]
96 pub volumes: Option<Vec<String>>,
97 #[serde(default)]
98 pub options: Option<String>,
99}
100
101#[derive(Debug, Deserialize, Serialize)]
102pub struct Step {
103 #[serde(default)]
104 pub name: Option<String>,
105 #[serde(default)]
106 pub uses: Option<String>,
107 #[serde(default)]
108 pub run: Option<String>,
109 #[serde(default)]
110 pub with: Option<HashMap<String, String>>,
111 #[serde(default)]
112 pub env: HashMap<String, String>,
113 #[serde(default)]
114 pub continue_on_error: Option<bool>,
115}
116
117impl WorkflowDefinition {
118 pub fn resolve_action(&self, action_ref: &str) -> ActionInfo {
119 let parts: Vec<&str> = action_ref.split('@').collect();
121
122 let (repo, _) = if parts.len() > 1 {
123 (parts[0], parts[1])
124 } else {
125 (parts[0], "main") };
127
128 ActionInfo {
129 repository: repo.to_string(),
130 is_docker: repo.starts_with("docker://"),
131 is_local: repo.starts_with("./"),
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
137pub struct ActionInfo {
138 pub repository: String,
139 pub is_docker: bool,
140 pub is_local: bool,
141}
142
143pub fn parse_workflow(path: &Path) -> Result<WorkflowDefinition, String> {
144 let validator = SchemaValidator::new()?;
146 validator.validate_workflow(path)?;
147
148 let content =
150 fs::read_to_string(path).map_err(|e| format!("Failed to read workflow file: {}", e))?;
151
152 let mut workflow: WorkflowDefinition = serde_yaml::from_str(&content)
154 .map_err(|e| format!("Failed to parse workflow structure: {}", e))?;
155
156 workflow.on = normalize_triggers(&workflow.on_raw)?;
158
159 Ok(workflow)
160}
161
162fn normalize_triggers(on_value: &serde_yaml::Value) -> Result<Vec<String>, String> {
163 let mut triggers = Vec::new();
164
165 match on_value {
166 serde_yaml::Value::String(event) => {
168 triggers.push(event.clone());
169 }
170 serde_yaml::Value::Sequence(events) => {
172 for event in events {
173 if let Some(event_str) = event.as_str() {
174 triggers.push(event_str.to_string());
175 }
176 }
177 }
178 serde_yaml::Value::Mapping(events_map) => {
180 for (event, _) in events_map {
181 if let Some(event_str) = event.as_str() {
182 triggers.push(event_str.to_string());
183 }
184 }
185 }
186 _ => {
187 return Err("'on' section has invalid format".to_string());
188 }
189 }
190
191 Ok(triggers)
192}