wrkflw_parser/
workflow.rs

1use 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
9// Custom deserializer for needs field that handles both string and array formats
10fn 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)] // Skip deserialization of the 'on' field directly
33    pub on: Vec<String>,
34    #[serde(rename = "on")] // Raw access to the 'on' field for custom handling
35    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        // Parse GitHub action reference like "actions/checkout@v3"
92        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") // Default to main if no version specified
98        };
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    // First validate against schema
117    let validator = SchemaValidator::new()?;
118    validator.validate_workflow(path)?;
119
120    // If validation passes, parse the workflow
121    let content =
122        fs::read_to_string(path).map_err(|e| format!("Failed to read workflow file: {}", e))?;
123
124    // Parse the YAML content
125    let mut workflow: WorkflowDefinition = serde_yaml::from_str(&content)
126        .map_err(|e| format!("Failed to parse workflow structure: {}", e))?;
127
128    // Normalize the trigger events
129    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        // Simple string trigger: on: push
139        serde_yaml::Value::String(event) => {
140            triggers.push(event.clone());
141        }
142        // Array of triggers: on: [push, pull_request]
143        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        // Map of triggers with configuration: on: {push: {branches: [main]}}
151        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}