From aff3df6fcfed94bbce732156246e40ee0464a5cf Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Thu, 26 Mar 2026 17:05:14 +0000 Subject: [PATCH] feat(wfe-core): add StepCondition types and PointerStatus::Skipped --- wfe-core/src/models/condition.rs | 209 +++++++++++++++++++++++++++++++ wfe-core/src/models/mod.rs | 2 + wfe-core/src/models/status.rs | 2 + 3 files changed, 213 insertions(+) create mode 100644 wfe-core/src/models/condition.rs diff --git a/wfe-core/src/models/condition.rs b/wfe-core/src/models/condition.rs new file mode 100644 index 0000000..ef818ba --- /dev/null +++ b/wfe-core/src/models/condition.rs @@ -0,0 +1,209 @@ +use serde::{Deserialize, Serialize}; + +/// A condition that determines whether a workflow step should execute. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum StepCondition { + /// All sub-conditions must be true (AND). + All(Vec), + /// At least one sub-condition must be true (OR). + Any(Vec), + /// No sub-conditions may be true (NOR). + None(Vec), + /// Exactly one sub-condition must be true (XOR). + OneOf(Vec), + /// Negation of a single condition (NOT). + Not(Box), + /// A leaf comparison against a field in workflow data. + Comparison(FieldComparison), +} + +/// A comparison of a workflow data field against an expected value. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FieldComparison { + /// Dot-separated field path, e.g. ".outputs.docker_started". + pub field: String, + /// The comparison operator. + pub operator: ComparisonOp, + /// The value to compare against. Required for all operators except IsNull/IsNotNull. + pub value: Option, +} + +/// Comparison operators for field conditions. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ComparisonOp { + Equals, + NotEquals, + Gt, + Gte, + Lt, + Lte, + Contains, + IsNull, + IsNotNull, +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[test] + fn comparison_op_serde_round_trip() { + for op in [ + ComparisonOp::Equals, + ComparisonOp::NotEquals, + ComparisonOp::Gt, + ComparisonOp::Gte, + ComparisonOp::Lt, + ComparisonOp::Lte, + ComparisonOp::Contains, + ComparisonOp::IsNull, + ComparisonOp::IsNotNull, + ] { + let json_str = serde_json::to_string(&op).unwrap(); + let deserialized: ComparisonOp = serde_json::from_str(&json_str).unwrap(); + assert_eq!(op, deserialized); + } + } + + #[test] + fn field_comparison_serde_round_trip() { + let comp = FieldComparison { + field: ".outputs.status".to_string(), + operator: ComparisonOp::Equals, + value: Some(json!("success")), + }; + let json_str = serde_json::to_string(&comp).unwrap(); + let deserialized: FieldComparison = serde_json::from_str(&json_str).unwrap(); + assert_eq!(comp, deserialized); + } + + #[test] + fn field_comparison_without_value_serde_round_trip() { + let comp = FieldComparison { + field: ".outputs.result".to_string(), + operator: ComparisonOp::IsNull, + value: None, + }; + let json_str = serde_json::to_string(&comp).unwrap(); + let deserialized: FieldComparison = serde_json::from_str(&json_str).unwrap(); + assert_eq!(comp, deserialized); + } + + #[test] + fn step_condition_comparison_serde_round_trip() { + let condition = StepCondition::Comparison(FieldComparison { + field: ".count".to_string(), + operator: ComparisonOp::Gt, + value: Some(json!(5)), + }); + let json_str = serde_json::to_string(&condition).unwrap(); + let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap(); + assert_eq!(condition, deserialized); + } + + #[test] + fn step_condition_not_serde_round_trip() { + let condition = StepCondition::Not(Box::new(StepCondition::Comparison(FieldComparison { + field: ".active".to_string(), + operator: ComparisonOp::Equals, + value: Some(json!(false)), + }))); + let json_str = serde_json::to_string(&condition).unwrap(); + let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap(); + assert_eq!(condition, deserialized); + } + + #[test] + fn step_condition_all_serde_round_trip() { + let condition = StepCondition::All(vec![ + StepCondition::Comparison(FieldComparison { + field: ".a".to_string(), + operator: ComparisonOp::Equals, + value: Some(json!(1)), + }), + StepCondition::Comparison(FieldComparison { + field: ".b".to_string(), + operator: ComparisonOp::Equals, + value: Some(json!(2)), + }), + ]); + let json_str = serde_json::to_string(&condition).unwrap(); + let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap(); + assert_eq!(condition, deserialized); + } + + #[test] + fn step_condition_any_serde_round_trip() { + let condition = StepCondition::Any(vec![ + StepCondition::Comparison(FieldComparison { + field: ".x".to_string(), + operator: ComparisonOp::IsNull, + value: None, + }), + ]); + let json_str = serde_json::to_string(&condition).unwrap(); + let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap(); + assert_eq!(condition, deserialized); + } + + #[test] + fn step_condition_none_serde_round_trip() { + let condition = StepCondition::None(vec![ + StepCondition::Comparison(FieldComparison { + field: ".err".to_string(), + operator: ComparisonOp::IsNotNull, + value: None, + }), + ]); + let json_str = serde_json::to_string(&condition).unwrap(); + let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap(); + assert_eq!(condition, deserialized); + } + + #[test] + fn step_condition_one_of_serde_round_trip() { + let condition = StepCondition::OneOf(vec![ + StepCondition::Comparison(FieldComparison { + field: ".mode".to_string(), + operator: ComparisonOp::Equals, + value: Some(json!("fast")), + }), + StepCondition::Comparison(FieldComparison { + field: ".mode".to_string(), + operator: ComparisonOp::Equals, + value: Some(json!("slow")), + }), + ]); + let json_str = serde_json::to_string(&condition).unwrap(); + let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap(); + assert_eq!(condition, deserialized); + } + + #[test] + fn nested_combinator_serde_round_trip() { + let condition = StepCondition::All(vec![ + StepCondition::Any(vec![ + StepCondition::Comparison(FieldComparison { + field: ".a".to_string(), + operator: ComparisonOp::Equals, + value: Some(json!(1)), + }), + StepCondition::Comparison(FieldComparison { + field: ".b".to_string(), + operator: ComparisonOp::Equals, + value: Some(json!(2)), + }), + ]), + StepCondition::Not(Box::new(StepCondition::Comparison(FieldComparison { + field: ".c".to_string(), + operator: ComparisonOp::IsNull, + value: None, + }))), + ]); + let json_str = serde_json::to_string(&condition).unwrap(); + let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap(); + assert_eq!(condition, deserialized); + } +} diff --git a/wfe-core/src/models/mod.rs b/wfe-core/src/models/mod.rs index 070f61c..a0eb83b 100644 --- a/wfe-core/src/models/mod.rs +++ b/wfe-core/src/models/mod.rs @@ -1,3 +1,4 @@ +pub mod condition; pub mod error_behavior; pub mod event; pub mod execution_error; @@ -12,6 +13,7 @@ pub mod status; pub mod workflow_definition; pub mod workflow_instance; +pub use condition::{ComparisonOp, FieldComparison, StepCondition}; pub use error_behavior::ErrorBehavior; pub use event::{Event, EventSubscription}; pub use execution_error::ExecutionError; diff --git a/wfe-core/src/models/status.rs b/wfe-core/src/models/status.rs index c76aa3e..900eb07 100644 --- a/wfe-core/src/models/status.rs +++ b/wfe-core/src/models/status.rs @@ -15,6 +15,7 @@ pub enum PointerStatus { Pending, Running, Complete, + Skipped, Sleeping, WaitingForEvent, Failed, @@ -58,6 +59,7 @@ mod tests { PointerStatus::Pending, PointerStatus::Running, PointerStatus::Complete, + PointerStatus::Skipped, PointerStatus::Sleeping, PointerStatus::WaitingForEvent, PointerStatus::Failed,