stepflow_flow/workflow/
step.rs1use std::collections::HashMap;
14
15use serde_with::{DefaultOnNull, serde_as};
16
17use super::Component;
18use crate::ValueExpr;
19
20#[serde_as]
25#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, schemars::JsonSchema)]
26#[serde(rename_all = "camelCase")]
27pub struct Step {
28 pub id: String,
30
31 pub component: Component,
33
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub on_error: Option<ErrorAction>,
36
37 #[serde(default, skip_serializing_if = "ValueExpr::is_null")]
39 pub input: ValueExpr,
40
41 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub must_execute: Option<bool>,
45
46 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
48 #[serde_as(as = "DefaultOnNull")]
49 pub metadata: HashMap<String, serde_json::Value>,
50}
51
52impl Step {
53 pub fn on_error(&self) -> Option<&ErrorAction> {
54 self.on_error.as_ref()
55 }
56
57 pub fn on_error_or_default(&self) -> ErrorAction {
59 self.on_error().cloned().unwrap_or_default()
60 }
61
62 pub fn must_execute(&self) -> bool {
64 self.must_execute.unwrap_or(false)
65 }
66}
67
68#[derive(
70 Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
71)]
72#[serde(tag = "action", rename_all = "camelCase")]
73#[schemars(transform = crate::discriminator_schema::AddDiscriminator::new("action"))]
74pub enum ErrorAction {
75 #[default]
77 #[schemars(title = "OnErrorFail")]
78 Fail,
79 #[serde(rename_all = "camelCase")]
84 #[schemars(title = "OnErrorDefault")]
85 UseDefault {
86 #[serde(skip_serializing_if = "Option::is_none")]
87 default_value: Option<serde_json::Value>,
88 },
89 #[serde(rename_all = "camelCase")]
96 #[schemars(title = "OnErrorRetry")]
97 Retry {
98 #[serde(default, skip_serializing_if = "Option::is_none")]
102 max_retries: Option<u32>,
103 },
104}
105
106impl ErrorAction {
107 pub const DEFAULT_MAX_RETRIES: u32 = 3;
109
110 pub fn is_default(&self) -> bool {
111 matches!(self, Self::Fail)
112 }
113
114 pub fn max_retries(&self) -> Option<u32> {
116 match self {
117 Self::Retry { max_retries } => Some(max_retries.unwrap_or(Self::DEFAULT_MAX_RETRIES)),
118 _ => None,
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::workflow::StepBuilder;
127
128 #[test]
129 fn test_error_action_serialization() {
130 let fail = ErrorAction::Fail;
131 assert_eq!(serde_yaml_ng::to_string(&fail).unwrap(), "action: fail\n");
132
133 let retry = ErrorAction::Retry { max_retries: None };
135 assert_eq!(serde_yaml_ng::to_string(&retry).unwrap(), "action: retry\n");
136
137 let retry_with_max = ErrorAction::Retry {
139 max_retries: Some(5),
140 };
141 assert_eq!(
142 serde_yaml_ng::to_string(&retry_with_max).unwrap(),
143 "action: retry\nmaxRetries: 5\n"
144 );
145
146 let use_default = ErrorAction::UseDefault {
147 default_value: Some(serde_json::json!("test_default")),
148 };
149 assert_eq!(
150 serde_yaml_ng::to_string(&use_default).unwrap(),
151 "action: useDefault\ndefaultValue: test_default\n"
152 );
153
154 let use_default_none = ErrorAction::UseDefault {
156 default_value: None,
157 };
158 assert_eq!(
159 serde_yaml_ng::to_string(&use_default_none).unwrap(),
160 "action: useDefault\n"
161 );
162 }
163
164 #[test]
165 fn test_error_action_deserialization() {
166 let fail: ErrorAction = serde_yaml_ng::from_str("action: fail").unwrap();
167 assert_eq!(fail, ErrorAction::Fail);
168
169 let retry: ErrorAction = serde_yaml_ng::from_str("action: retry").unwrap();
171 assert_eq!(retry, ErrorAction::Retry { max_retries: None });
172
173 let retry_with_max: ErrorAction =
175 serde_yaml_ng::from_str("action: retry\nmaxRetries: 5").unwrap();
176 assert_eq!(
177 retry_with_max,
178 ErrorAction::Retry {
179 max_retries: Some(5)
180 }
181 );
182
183 let use_default: ErrorAction =
184 serde_yaml_ng::from_str("action: useDefault\ndefaultValue: test_default").unwrap();
185 assert_eq!(
186 use_default,
187 ErrorAction::UseDefault {
188 default_value: Some(serde_json::json!("test_default"))
189 }
190 );
191 }
192
193 #[test]
194 fn test_error_action_default() {
195 assert_eq!(ErrorAction::default(), ErrorAction::Fail);
196 assert!(ErrorAction::Fail.is_default());
197 assert!(!ErrorAction::Retry { max_retries: None }.is_default());
198 assert!(
199 !ErrorAction::UseDefault {
200 default_value: Some(serde_json::json!("test"))
201 }
202 .is_default()
203 );
204 }
205
206 #[test]
207 fn test_error_action_max_retries() {
208 assert_eq!(ErrorAction::Fail.max_retries(), None);
209 assert_eq!(
210 ErrorAction::Retry { max_retries: None }.max_retries(),
211 Some(ErrorAction::DEFAULT_MAX_RETRIES)
212 );
213 assert_eq!(
214 ErrorAction::Retry {
215 max_retries: Some(5)
216 }
217 .max_retries(),
218 Some(5)
219 );
220 }
221
222 #[test]
223 fn test_step_serialization_with_error_action() {
224 let step = StepBuilder::new("test_step")
225 .component("/mock/test_component")
226 .on_error(ErrorAction::UseDefault {
227 default_value: Some(serde_json::json!("fallback")),
228 })
229 .input(ValueExpr::null())
230 .build();
231
232 let yaml = serde_yaml_ng::to_string(&step).unwrap();
233 assert!(yaml.contains("onError:"));
234 assert!(yaml.contains("action: useDefault"));
235 assert!(yaml.contains("defaultValue: fallback"));
236 }
237
238 #[test]
239 fn test_step_default_error_action_not_serialized() {
240 let step = StepBuilder::new("test_step")
241 .component("/mock/test_component")
242 .input(ValueExpr::null())
243 .build();
244
245 let yaml = serde_yaml_ng::to_string(&step).unwrap();
246 assert!(!yaml.contains("onError:"));
247 }
248
249 #[test]
250 fn test_step_all_optional_null() {
251 let json = serde_json::json!({
254 "id": "test_step",
255 "component": "/mock/test_component",
256 "onError": null,
257 "input": null,
258 "mustExecute": null,
259 "metadata": null,
260 });
261 let step: Step = serde_json::from_value(json).unwrap();
262 assert_eq!(step.id, "test_step");
263 assert!(step.on_error.is_none());
264 assert_eq!(step.on_error_or_default(), ErrorAction::Fail);
265 assert!(step.input.is_null());
266 assert!(step.must_execute.is_none());
267 assert!(step.metadata.is_empty());
268 }
269
270 #[test]
271 fn test_error_action_use_default_null_value() {
272 let json = serde_json::json!({"action": "useDefault", "defaultValue": null});
274 let action: ErrorAction = serde_json::from_value(json).unwrap();
275 assert!(matches!(
276 action,
277 ErrorAction::UseDefault {
278 default_value: None
279 }
280 ));
281 }
282
283 #[test]
284 fn test_error_action_retry_null_max_retries() {
285 let json = serde_json::json!({"action": "retry", "maxRetries": null});
287 let action: ErrorAction = serde_json::from_value(json).unwrap();
288 assert!(matches!(action, ErrorAction::Retry { max_retries: None }));
289 }
290}