Skip to main content

erio_workflow/
step.rs

1//! Step trait for workflow execution units.
2
3use crate::WorkflowError;
4use crate::context::WorkflowContext;
5
6/// A single execution unit in a workflow.
7#[async_trait::async_trait]
8pub trait Step: Send + Sync {
9    /// Returns the unique identifier for this step.
10    fn id(&self) -> &str;
11
12    /// Executes the step with the given workflow context.
13    async fn execute(&self, ctx: &mut WorkflowContext) -> Result<StepOutput, WorkflowError>;
14}
15
16/// The output produced by a step execution.
17#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
18pub struct StepOutput {
19    value: String,
20    metadata: Option<serde_json::Value>,
21    skipped: bool,
22}
23
24impl StepOutput {
25    /// Creates a new step output with the given value.
26    pub fn new(value: &str) -> Self {
27        Self {
28            value: value.into(),
29            metadata: None,
30            skipped: false,
31        }
32    }
33
34    /// Creates a skipped step output (condition was false).
35    pub fn skipped() -> Self {
36        Self {
37            value: String::new(),
38            metadata: None,
39            skipped: true,
40        }
41    }
42
43    /// Returns `true` if this output represents a skipped step.
44    pub fn is_skipped(&self) -> bool {
45        self.skipped
46    }
47
48    /// Returns the output value.
49    pub fn value(&self) -> &str {
50        &self.value
51    }
52
53    /// Returns the metadata, if any.
54    pub fn metadata(&self) -> Option<&serde_json::Value> {
55        self.metadata.as_ref()
56    }
57
58    /// Attaches metadata to the output.
59    #[must_use]
60    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
61        self.metadata = Some(metadata);
62        self
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    // === Mock Step ===
71
72    struct AddStep {
73        id: String,
74        value: String,
75    }
76
77    impl AddStep {
78        fn new(id: &str, value: &str) -> Self {
79            Self {
80                id: id.into(),
81                value: value.into(),
82            }
83        }
84    }
85
86    #[async_trait::async_trait]
87    impl Step for AddStep {
88        fn id(&self) -> &str {
89            &self.id
90        }
91
92        async fn execute(&self, _ctx: &mut WorkflowContext) -> Result<StepOutput, WorkflowError> {
93            Ok(StepOutput::new(&self.value))
94        }
95    }
96
97    // === Trait Tests ===
98
99    #[test]
100    fn step_returns_id() {
101        let step = AddStep::new("build", "result");
102        assert_eq!(step.id(), "build");
103    }
104
105    #[tokio::test]
106    async fn step_executes_and_returns_output() {
107        let step = AddStep::new("build", "compiled");
108        let mut ctx = WorkflowContext::new();
109        let output = step.execute(&mut ctx).await.unwrap();
110        assert_eq!(output.value(), "compiled");
111    }
112
113    // === StepOutput Tests ===
114
115    #[test]
116    fn step_output_stores_value() {
117        let output = StepOutput::new("hello");
118        assert_eq!(output.value(), "hello");
119    }
120
121    #[test]
122    fn step_output_with_metadata() {
123        let output = StepOutput::new("result").with_metadata(serde_json::json!({"exit_code": 0}));
124        assert_eq!(output.value(), "result");
125        assert!(output.metadata().is_some());
126    }
127}