use crate::common::reusable::ReusableOr;
use crate::v1_1::criterion::Criterion;
use crate::v1_1::parameter::Parameter;
use crate::v1_1::success_action::validate_goto_target;
use crate::validation::{Context, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum FailureActionType {
#[default]
End,
Goto,
Retry,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct FailureAction {
pub name: String,
#[serde(rename = "type")]
pub type_: FailureActionType,
#[serde(rename = "workflowId", skip_serializing_if = "Option::is_none")]
pub workflow_id: Option<String>,
#[serde(rename = "stepId", skip_serializing_if = "Option::is_none")]
pub step_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parameters: Vec<ReusableOr<Parameter>>,
#[serde(rename = "retryAfter", skip_serializing_if = "Option::is_none")]
pub retry_after: Option<f64>,
#[serde(rename = "retryLimit", skip_serializing_if = "Option::is_none")]
pub retry_limit: Option<u64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub criteria: Vec<Criterion>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
impl ValidateWithContext for FailureAction {
fn validate_with_context(&self, ctx: &mut Context) {
ctx.require_non_empty("name", &self.name);
validate_goto_target(
self.type_ == FailureActionType::Goto,
self.workflow_id.is_some(),
self.step_id.is_some(),
ctx,
);
if !self.parameters.is_empty() && self.workflow_id.is_none() {
ctx.error_field("parameters", "are only valid when `workflowId` is set");
}
if self.retry_after.is_some_and(|n| n < 0.0 || n.is_nan()) {
ctx.error_field("retryAfter", "must not be negative");
}
for (i, parameter) in self.parameters.iter().enumerate() {
ctx.in_index("parameters", i, |ctx| parameter.validate_with_context(ctx));
}
for (i, criterion) in self.criteria.iter().enumerate() {
ctx.in_index("criteria", i, |ctx| criterion.validate_with_context(ctx));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use enumset::EnumSet;
use serde_json::json;
fn validate(a: &FailureAction) -> Vec<String> {
let mut ctx = Context::with_path(EnumSet::empty(), "#.a");
a.validate_with_context(&mut ctx);
ctx.errors.iter().map(ToString::to_string).collect()
}
#[test]
fn retry_action_round_trips() {
let a: FailureAction = serde_json::from_value(json!({
"name": "retryStep", "type": "retry", "retryAfter": 1.5, "retryLimit": 3,
}))
.unwrap();
assert_eq!(a.type_, FailureActionType::Retry);
assert!(validate(&a).is_empty());
}
#[test]
fn parameters_require_workflow_id() {
let a = FailureAction {
name: "g".into(),
type_: FailureActionType::Goto,
step_id: Some("next".into()),
parameters: vec![ReusableOr::Reusable(Default::default())],
..Default::default()
};
assert!(
validate(&a)
.iter()
.any(|e| e == "#.a.parameters: are only valid when `workflowId` is set")
);
}
#[test]
fn negative_retry_after_fails_validation() {
let a = FailureAction {
name: "n".into(),
type_: FailureActionType::Retry,
retry_after: Some(-1.0),
..Default::default()
};
assert!(
validate(&a)
.iter()
.any(|e| e == "#.a.retryAfter: must not be negative")
);
}
#[test]
fn criteria_are_recursed() {
let a = FailureAction {
name: "n".into(),
criteria: vec![Criterion::default()],
..Default::default()
};
assert!(
validate(&a)
.iter()
.any(|e| e == "#.a.criteria[0].condition: must not be empty")
);
}
}