use crate::WorkflowError;
use crate::context::WorkflowContext;
#[async_trait::async_trait]
pub trait Step: Send + Sync {
fn id(&self) -> &str;
async fn execute(&self, ctx: &mut WorkflowContext) -> Result<StepOutput, WorkflowError>;
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct StepOutput {
value: String,
metadata: Option<serde_json::Value>,
skipped: bool,
}
impl StepOutput {
pub fn new(value: &str) -> Self {
Self {
value: value.into(),
metadata: None,
skipped: false,
}
}
pub fn skipped() -> Self {
Self {
value: String::new(),
metadata: None,
skipped: true,
}
}
pub fn is_skipped(&self) -> bool {
self.skipped
}
pub fn value(&self) -> &str {
&self.value
}
pub fn metadata(&self) -> Option<&serde_json::Value> {
self.metadata.as_ref()
}
#[must_use]
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
struct AddStep {
id: String,
value: String,
}
impl AddStep {
fn new(id: &str, value: &str) -> Self {
Self {
id: id.into(),
value: value.into(),
}
}
}
#[async_trait::async_trait]
impl Step for AddStep {
fn id(&self) -> &str {
&self.id
}
async fn execute(&self, _ctx: &mut WorkflowContext) -> Result<StepOutput, WorkflowError> {
Ok(StepOutput::new(&self.value))
}
}
#[test]
fn step_returns_id() {
let step = AddStep::new("build", "result");
assert_eq!(step.id(), "build");
}
#[tokio::test]
async fn step_executes_and_returns_output() {
let step = AddStep::new("build", "compiled");
let mut ctx = WorkflowContext::new();
let output = step.execute(&mut ctx).await.unwrap();
assert_eq!(output.value(), "compiled");
}
#[test]
fn step_output_stores_value() {
let output = StepOutput::new("hello");
assert_eq!(output.value(), "hello");
}
#[test]
fn step_output_with_metadata() {
let output = StepOutput::new("result").with_metadata(serde_json::json!({"exit_code": 0}));
assert_eq!(output.value(), "result");
assert!(output.metadata().is_some());
}
}