diff --git a/wfe-yaml/src/compiler.rs b/wfe-yaml/src/compiler.rs index 52f721c..869cf58 100644 --- a/wfe-yaml/src/compiler.rs +++ b/wfe-yaml/src/compiler.rs @@ -14,7 +14,9 @@ use wfe_buildkit::{BuildkitConfig, BuildkitStep}; #[cfg(feature = "containerd")] use wfe_containerd::{ContainerdConfig, ContainerdStep}; use wfe_core::primitives::sub_workflow::SubWorkflowStep; -use crate::schema::{WorkflowSpec, YamlErrorBehavior, YamlStep}; +use wfe_core::models::condition::{ComparisonOp, FieldComparison, StepCondition}; + +use crate::schema::{WorkflowSpec, YamlCombinator, YamlComparison, YamlCondition, YamlErrorBehavior, YamlStep}; /// Configuration for a sub-workflow step. #[derive(Debug, Clone, Serialize)] @@ -82,6 +84,11 @@ fn compile_steps( compile_steps(parallel_children, definition, factories, next_id)?; container.children = child_ids; + // Compile condition if present. + if let Some(ref yaml_cond) = yaml_step.when { + container.when = Some(compile_condition(yaml_cond)?); + } + definition.steps.push(container); main_step_ids.push(container_id); } else { @@ -108,6 +115,11 @@ fn compile_steps( wf_step.error_behavior = Some(map_error_behavior(eb)?); } + // Compile condition if present. + if let Some(ref yaml_cond) = yaml_step.when { + wf_step.when = Some(compile_condition(yaml_cond)?); + } + // Handle on_failure: create compensation step. if let Some(ref on_failure) = yaml_step.on_failure { let comp_id = *next_id; @@ -230,6 +242,154 @@ fn compile_steps( Ok(main_step_ids) } +/// Convert a YAML condition tree into a `StepCondition` tree. +pub fn compile_condition(yaml_cond: &YamlCondition) -> Result { + match yaml_cond { + YamlCondition::Comparison(cmp) => compile_comparison(cmp.as_ref()), + YamlCondition::Combinator(combinator) => compile_combinator(combinator), + } +} + +fn compile_combinator(c: &YamlCombinator) -> Result { + // Count how many combinator keys are set to detect ambiguity. + let mut count = 0; + if c.all.is_some() { + count += 1; + } + if c.any.is_some() { + count += 1; + } + if c.none.is_some() { + count += 1; + } + if c.one_of.is_some() { + count += 1; + } + if c.not.is_some() { + count += 1; + } + + if count == 0 { + return Err(YamlWorkflowError::Compilation( + "Condition combinator must have at least one of: all, any, none, one_of, not" + .to_string(), + )); + } + if count > 1 { + return Err(YamlWorkflowError::Compilation( + "Condition combinator must have exactly one of: all, any, none, one_of, not" + .to_string(), + )); + } + + if let Some(ref children) = c.all { + let compiled: Result, _> = children.iter().map(compile_condition).collect(); + Ok(StepCondition::All(compiled?)) + } else if let Some(ref children) = c.any { + let compiled: Result, _> = children.iter().map(compile_condition).collect(); + Ok(StepCondition::Any(compiled?)) + } else if let Some(ref children) = c.none { + let compiled: Result, _> = children.iter().map(compile_condition).collect(); + Ok(StepCondition::None(compiled?)) + } else if let Some(ref children) = c.one_of { + let compiled: Result, _> = children.iter().map(compile_condition).collect(); + Ok(StepCondition::OneOf(compiled?)) + } else if let Some(ref inner) = c.not { + Ok(StepCondition::Not(Box::new(compile_condition(inner)?))) + } else { + unreachable!() + } +} + +fn compile_comparison(cmp: &YamlComparison) -> Result { + // Determine which operator is specified. Exactly one must be present. + let mut ops: Vec<(ComparisonOp, Option)> = Vec::new(); + + if let Some(ref v) = cmp.equals { + ops.push((ComparisonOp::Equals, Some(yaml_value_to_json(v)))); + } + if let Some(ref v) = cmp.not_equals { + ops.push((ComparisonOp::NotEquals, Some(yaml_value_to_json(v)))); + } + if let Some(ref v) = cmp.gt { + ops.push((ComparisonOp::Gt, Some(yaml_value_to_json(v)))); + } + if let Some(ref v) = cmp.gte { + ops.push((ComparisonOp::Gte, Some(yaml_value_to_json(v)))); + } + if let Some(ref v) = cmp.lt { + ops.push((ComparisonOp::Lt, Some(yaml_value_to_json(v)))); + } + if let Some(ref v) = cmp.lte { + ops.push((ComparisonOp::Lte, Some(yaml_value_to_json(v)))); + } + if let Some(ref v) = cmp.contains { + ops.push((ComparisonOp::Contains, Some(yaml_value_to_json(v)))); + } + if let Some(true) = cmp.is_null { + ops.push((ComparisonOp::IsNull, None)); + } + if let Some(true) = cmp.is_not_null { + ops.push((ComparisonOp::IsNotNull, None)); + } + + if ops.is_empty() { + return Err(YamlWorkflowError::Compilation(format!( + "Comparison on field '{}' must specify an operator (equals, gt, etc.)", + cmp.field + ))); + } + if ops.len() > 1 { + return Err(YamlWorkflowError::Compilation(format!( + "Comparison on field '{}' must specify exactly one operator, found {}", + cmp.field, + ops.len() + ))); + } + + let (operator, value) = ops.remove(0); + Ok(StepCondition::Comparison(FieldComparison { + field: cmp.field.clone(), + operator, + value, + })) +} + +/// Convert a serde_yaml::Value to serde_json::Value. +fn yaml_value_to_json(v: &serde_yaml::Value) -> serde_json::Value { + match v { + serde_yaml::Value::Null => serde_json::Value::Null, + serde_yaml::Value::Bool(b) => serde_json::Value::Bool(*b), + serde_yaml::Value::Number(n) => { + if let Some(i) = n.as_i64() { + serde_json::Value::Number(serde_json::Number::from(i)) + } else if let Some(u) = n.as_u64() { + serde_json::Value::Number(serde_json::Number::from(u)) + } else if let Some(f) = n.as_f64() { + serde_json::Number::from_f64(f) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null) + } else { + serde_json::Value::Null + } + } + serde_yaml::Value::String(s) => serde_json::Value::String(s.clone()), + serde_yaml::Value::Sequence(seq) => { + serde_json::Value::Array(seq.iter().map(yaml_value_to_json).collect()) + } + serde_yaml::Value::Mapping(map) => { + let mut obj = serde_json::Map::new(); + for (k, val) in map { + if let serde_yaml::Value::String(key) = k { + obj.insert(key.clone(), yaml_value_to_json(val)); + } + } + serde_json::Value::Object(obj) + } + serde_yaml::Value::Tagged(tagged) => yaml_value_to_json(&tagged.value), + } +} + fn build_step_config_and_factory( step: &YamlStep, step_type: &str,