use serde::{Deserialize, Deserializer, Serialize}; use std::collections::HashMap; use std::fs; use std::path::Path; use wrkflw_matrix::MatrixConfig; use super::schema::SchemaValidator; // Custom deserializer for needs field that handles both string and array formats fn deserialize_needs<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum StringOrVec { String(String), Vec(Vec), } let value = Option::::deserialize(deserializer)?; match value { Some(StringOrVec::String(s)) => Ok(Some(vec![s])), Some(StringOrVec::Vec(v)) => Ok(Some(v)), None => Ok(None), } } // Custom deserializer for runs-on field that handles both string and array formats fn deserialize_runs_on<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum StringOrVec { String(String), Vec(Vec), } let value = Option::::deserialize(deserializer)?; match value { Some(StringOrVec::String(s)) => Ok(Some(vec![s])), Some(StringOrVec::Vec(v)) => Ok(Some(v)), None => Ok(None), } } #[derive(Debug, Deserialize, Serialize)] pub struct WorkflowDefinition { pub name: String, #[serde(skip, default)] // Skip deserialization of the 'on' field directly pub on: Vec, #[serde(rename = "on")] // Raw access to the 'on' field for custom handling pub on_raw: serde_yaml::Value, pub jobs: HashMap, } #[derive(Debug, Deserialize, Serialize)] pub struct Job { #[serde(rename = "runs-on", default, deserialize_with = "deserialize_runs_on")] pub runs_on: Option>, #[serde(default, deserialize_with = "deserialize_needs")] pub needs: Option>, #[serde(default)] pub steps: Vec, #[serde(default)] pub env: HashMap, #[serde(default)] pub matrix: Option, #[serde(default)] pub services: HashMap, #[serde(default, rename = "if")] pub if_condition: Option, #[serde(default)] pub outputs: Option>, #[serde(default)] pub permissions: Option>, // Reusable workflow (job-level 'uses') support #[serde(default)] pub uses: Option, #[serde(default)] pub with: Option>, #[serde(default)] pub secrets: Option, } #[derive(Debug, Deserialize, Serialize)] pub struct Service { pub image: String, #[serde(default)] pub ports: Option>, #[serde(default)] pub env: HashMap, #[serde(default)] pub volumes: Option>, #[serde(default)] pub options: Option, } #[derive(Debug, Deserialize, Serialize)] pub struct Step { #[serde(default)] pub name: Option, #[serde(default)] pub uses: Option, #[serde(default)] pub run: Option, #[serde(default)] pub with: Option>, #[serde(default)] pub env: HashMap, #[serde(default)] pub continue_on_error: Option, } impl WorkflowDefinition { pub fn resolve_action(&self, action_ref: &str) -> ActionInfo { // Parse GitHub action reference like "actions/checkout@v3" let parts: Vec<&str> = action_ref.split('@').collect(); let (repo, _) = if parts.len() > 1 { (parts[0], parts[1]) } else { (parts[0], "main") // Default to main if no version specified }; ActionInfo { repository: repo.to_string(), is_docker: repo.starts_with("docker://"), is_local: repo.starts_with("./"), } } } #[derive(Debug, Clone)] pub struct ActionInfo { pub repository: String, pub is_docker: bool, pub is_local: bool, } pub fn parse_workflow(path: &Path) -> Result { // First validate against schema let validator = SchemaValidator::new()?; validator.validate_workflow(path)?; // If validation passes, parse the workflow let content = fs::read_to_string(path).map_err(|e| format!("Failed to read workflow file: {}", e))?; // Parse the YAML content let mut workflow: WorkflowDefinition = serde_yaml::from_str(&content) .map_err(|e| format!("Failed to parse workflow structure: {}", e))?; // Normalize the trigger events workflow.on = normalize_triggers(&workflow.on_raw)?; Ok(workflow) } fn normalize_triggers(on_value: &serde_yaml::Value) -> Result, String> { let mut triggers = Vec::new(); match on_value { // Simple string trigger: on: push serde_yaml::Value::String(event) => { triggers.push(event.clone()); } // Array of triggers: on: [push, pull_request] serde_yaml::Value::Sequence(events) => { for event in events { if let Some(event_str) = event.as_str() { triggers.push(event_str.to_string()); } } } // Map of triggers with configuration: on: {push: {branches: [main]}} serde_yaml::Value::Mapping(events_map) => { for (event, _) in events_map { if let Some(event_str) = event.as_str() { triggers.push(event_str.to_string()); } } } _ => { return Err("'on' section has invalid format".to_string()); } } Ok(triggers) }