use std::collections::HashMap;
use serde_with::{DefaultOnNull, serde_as};
use super::Component;
use crate::ValueExpr;
#[serde_as]
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Step {
pub id: String,
pub component: Component,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub on_error: Option<ErrorAction>,
#[serde(default, skip_serializing_if = "ValueExpr::is_null")]
pub input: ValueExpr,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub must_execute: Option<bool>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
#[serde_as(as = "DefaultOnNull")]
pub metadata: HashMap<String, serde_json::Value>,
}
impl Step {
pub fn on_error(&self) -> Option<&ErrorAction> {
self.on_error.as_ref()
}
pub fn on_error_or_default(&self) -> ErrorAction {
self.on_error().cloned().unwrap_or_default()
}
pub fn must_execute(&self) -> bool {
self.must_execute.unwrap_or(false)
}
}
#[derive(
Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
#[serde(tag = "action", rename_all = "camelCase")]
#[schemars(transform = crate::discriminator_schema::AddDiscriminator::new("action"))]
pub enum ErrorAction {
#[default]
#[schemars(title = "OnErrorFail")]
Fail,
#[serde(rename_all = "camelCase")]
#[schemars(title = "OnErrorDefault")]
UseDefault {
#[serde(skip_serializing_if = "Option::is_none")]
default_value: Option<serde_json::Value>,
},
#[serde(rename_all = "camelCase")]
#[schemars(title = "OnErrorRetry")]
Retry {
#[serde(default, skip_serializing_if = "Option::is_none")]
max_retries: Option<u32>,
},
}
impl ErrorAction {
pub const DEFAULT_MAX_RETRIES: u32 = 3;
pub fn is_default(&self) -> bool {
matches!(self, Self::Fail)
}
pub fn max_retries(&self) -> Option<u32> {
match self {
Self::Retry { max_retries } => Some(max_retries.unwrap_or(Self::DEFAULT_MAX_RETRIES)),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::workflow::StepBuilder;
#[test]
fn test_error_action_serialization() {
let fail = ErrorAction::Fail;
assert_eq!(serde_yaml_ng::to_string(&fail).unwrap(), "action: fail\n");
let retry = ErrorAction::Retry { max_retries: None };
assert_eq!(serde_yaml_ng::to_string(&retry).unwrap(), "action: retry\n");
let retry_with_max = ErrorAction::Retry {
max_retries: Some(5),
};
assert_eq!(
serde_yaml_ng::to_string(&retry_with_max).unwrap(),
"action: retry\nmaxRetries: 5\n"
);
let use_default = ErrorAction::UseDefault {
default_value: Some(serde_json::json!("test_default")),
};
assert_eq!(
serde_yaml_ng::to_string(&use_default).unwrap(),
"action: useDefault\ndefaultValue: test_default\n"
);
let use_default_none = ErrorAction::UseDefault {
default_value: None,
};
assert_eq!(
serde_yaml_ng::to_string(&use_default_none).unwrap(),
"action: useDefault\n"
);
}
#[test]
fn test_error_action_deserialization() {
let fail: ErrorAction = serde_yaml_ng::from_str("action: fail").unwrap();
assert_eq!(fail, ErrorAction::Fail);
let retry: ErrorAction = serde_yaml_ng::from_str("action: retry").unwrap();
assert_eq!(retry, ErrorAction::Retry { max_retries: None });
let retry_with_max: ErrorAction =
serde_yaml_ng::from_str("action: retry\nmaxRetries: 5").unwrap();
assert_eq!(
retry_with_max,
ErrorAction::Retry {
max_retries: Some(5)
}
);
let use_default: ErrorAction =
serde_yaml_ng::from_str("action: useDefault\ndefaultValue: test_default").unwrap();
assert_eq!(
use_default,
ErrorAction::UseDefault {
default_value: Some(serde_json::json!("test_default"))
}
);
}
#[test]
fn test_error_action_default() {
assert_eq!(ErrorAction::default(), ErrorAction::Fail);
assert!(ErrorAction::Fail.is_default());
assert!(!ErrorAction::Retry { max_retries: None }.is_default());
assert!(
!ErrorAction::UseDefault {
default_value: Some(serde_json::json!("test"))
}
.is_default()
);
}
#[test]
fn test_error_action_max_retries() {
assert_eq!(ErrorAction::Fail.max_retries(), None);
assert_eq!(
ErrorAction::Retry { max_retries: None }.max_retries(),
Some(ErrorAction::DEFAULT_MAX_RETRIES)
);
assert_eq!(
ErrorAction::Retry {
max_retries: Some(5)
}
.max_retries(),
Some(5)
);
}
#[test]
fn test_step_serialization_with_error_action() {
let step = StepBuilder::new("test_step")
.component("/mock/test_component")
.on_error(ErrorAction::UseDefault {
default_value: Some(serde_json::json!("fallback")),
})
.input(ValueExpr::null())
.build();
let yaml = serde_yaml_ng::to_string(&step).unwrap();
assert!(yaml.contains("onError:"));
assert!(yaml.contains("action: useDefault"));
assert!(yaml.contains("defaultValue: fallback"));
}
#[test]
fn test_step_default_error_action_not_serialized() {
let step = StepBuilder::new("test_step")
.component("/mock/test_component")
.input(ValueExpr::null())
.build();
let yaml = serde_yaml_ng::to_string(&step).unwrap();
assert!(!yaml.contains("onError:"));
}
#[test]
fn test_step_all_optional_null() {
let json = serde_json::json!({
"id": "test_step",
"component": "/mock/test_component",
"onError": null,
"input": null,
"mustExecute": null,
"metadata": null,
});
let step: Step = serde_json::from_value(json).unwrap();
assert_eq!(step.id, "test_step");
assert!(step.on_error.is_none());
assert_eq!(step.on_error_or_default(), ErrorAction::Fail);
assert!(step.input.is_null());
assert!(step.must_execute.is_none());
assert!(step.metadata.is_empty());
}
#[test]
fn test_error_action_use_default_null_value() {
let json = serde_json::json!({"action": "useDefault", "defaultValue": null});
let action: ErrorAction = serde_json::from_value(json).unwrap();
assert!(matches!(
action,
ErrorAction::UseDefault {
default_value: None
}
));
}
#[test]
fn test_error_action_retry_null_max_retries() {
let json = serde_json::json!({"action": "retry", "maxRetries": null});
let action: ErrorAction = serde_json::from_value(json).unwrap();
assert!(matches!(action, ErrorAction::Retry { max_retries: None }));
}
}