Skip to main content

swf_builders/
lib.rs

1#![deny(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub mod services;
5
6// Re-export the main builder type for convenience
7pub use services::workflow::WorkflowBuilder;
8
9#[cfg(test)]
10mod unit_tests {
11
12    use crate::services::workflow::WorkflowBuilder;
13    use serde_json::json;
14    use serde_json::Value;
15    use std::collections::HashMap;
16    use swf_core::models::duration::*;
17    use swf_core::models::error::OneOfErrorDefinitionOrReference;
18    use swf_core::models::task::*;
19    use swf_core::models::timeout::*;
20
21    #[test]
22    fn build_workflow_should_work() {
23        // Minimal test - fully chained
24        let workflow = WorkflowBuilder::new().use_dsl("1.0.0").build();
25        assert_eq!(workflow.document.dsl, "1.0.0");
26    }
27
28    #[test]
29    fn build_workflow_should_work_full() {
30        //arrange
31        let dsl_version = "1.0.0";
32        let namespace = "namespace";
33        let name = "fake-name";
34        let version = "1.0.0";
35        let title = "fake-title";
36        let summary = "fake-summary";
37        let tags: HashMap<String, String> = vec![
38            ("key1".to_string(), "value1".to_string()),
39            ("key2".to_string(), "value2".to_string()),
40        ]
41        .into_iter()
42        .collect();
43        let timeout_duration = Duration {
44            minutes: Some(69),
45            ..Duration::default()
46        };
47        let basic_name = "fake-basic";
48        let username = "fake-username";
49        let password = "fake-password";
50        let call_task_name = "call-task";
51        let call_function_name = "fake-function";
52        let call_task_with: HashMap<String, Value> = vec![
53            ("key1".to_string(), Value::String("value1".to_string())),
54            ("key2".to_string(), Value::String("value2".to_string())),
55        ]
56        .into_iter()
57        .collect();
58        let do_task_name = "do-task";
59        let emit_task_name = "emit-task";
60        let emit_event_attributes: HashMap<String, Value> = vec![
61            ("key1".to_string(), Value::String("value1".to_string())),
62            ("key2".to_string(), Value::String("value2".to_string())),
63        ]
64        .into_iter()
65        .collect();
66        let for_task_name = "for-task";
67        let for_each = "item";
68        let for_each_in = "items";
69        let for_each_at = "index";
70        let fork_task_name = "fork-task";
71        let listen_task_name = "listen-task";
72        let raise_task_name = "raise-task-name";
73        let raise_error_type = "error-type";
74        let raise_error_status = json!(400);
75        let raise_error_title = "error-title";
76        let raise_error_detail = "error-detail";
77        let raise_error_instance = "error-instance";
78        let run_container_task_name = "run-container-task-name";
79        let container_image = "container-image-name";
80        let container_command = "container-command";
81        let container_ports: HashMap<String, serde_json::Value> = vec![
82            ("8080".to_string(), json!(8081)),
83            ("8082".to_string(), json!(8083)),
84        ]
85        .into_iter()
86        .collect();
87        let container_volumes: HashMap<String, serde_json::Value> =
88            vec![("volume-1".to_string(), json!("/some/fake/path"))]
89                .into_iter()
90                .collect();
91        let container_environment: HashMap<String, String> = vec![
92            ("env1-name".to_string(), "env1-value".to_string()),
93            ("env2-name".to_string(), "env2-value".to_string()),
94        ]
95        .into_iter()
96        .collect();
97        let run_script_task_name = "run-script-task-name";
98        let script_code = "script-code";
99        let run_shell_task_name = "run-shell-task-name";
100        let shell_command_name = "run-shell-command";
101        let run_workflow_task_name = "run-workflow-task-name";
102        let workflow_namespace = "workflow-namespace";
103        let workflow_name = "workflow-name";
104        let workflow_version = "workflow-version";
105        let workflow_input = json!({"hello": "world"});
106        let set_task_name = "set-task-name";
107        let set_task_variables: HashMap<String, Value> = vec![
108            ("var1-name".to_string(), json!("var1-value".to_string())),
109            ("var2-name".to_string(), json!(69)),
110        ]
111        .into_iter()
112        .collect();
113        let switch_task_name = "switch-task-name";
114        let switch_case_name = "switch-case-name";
115        let switch_case_when = "true";
116        let switch_case_then = "continue";
117        let try_task_name = "try-task-name";
118        let catch_when = "catch-when";
119        let catch_error_type = "https://serverlessworkflow.io/spec/1.0.0/errors/communication";
120        let catch_error_status = json!(500);
121        let retry_except_when = "retry-except-when";
122        let wait_task_name = "wait-task";
123        let wait_duration = OneOfDurationOrIso8601Expression::Duration(Duration::from_days(3));
124
125        //act
126        let workflow = WorkflowBuilder::new()
127            .use_dsl(dsl_version)
128            .with_namespace(namespace)
129            .with_name(name)
130            .with_version(version)
131            .with_title(title)
132            .with_summary(summary)
133            .with_tags(tags.clone())
134            .with_timeout(|t| {
135                t.after(timeout_duration.clone());
136            })
137            .use_authentication(basic_name, |a| {
138                a.basic().with_username(username).with_password(password);
139            })
140            .do_(call_task_name, |task| {
141                task.call(call_function_name)
142                    .with_arguments(call_task_with.clone());
143            })
144            .do_(do_task_name, |task| {
145                task.do_().do_("fake-wait-task", |st| {
146                    st.wait(OneOfDurationOrIso8601Expression::Duration(
147                        Duration::from_seconds(25),
148                    ));
149                });
150            })
151            .do_(emit_task_name, |task| {
152                task.emit(|e| {
153                    e.with_attributes(emit_event_attributes.clone());
154                });
155            })
156            .do_(for_task_name, |task| {
157                task.for_()
158                    .each(for_each)
159                    .in_(for_each_in)
160                    .at(for_each_at)
161                    .do_("fake-wait-task", |st| {
162                        st.wait(OneOfDurationOrIso8601Expression::Duration(
163                            Duration::from_seconds(25),
164                        ));
165                    });
166            })
167            .do_(fork_task_name, |task| {
168                task.fork().branch(|b| {
169                    b.do_().do_("fake-wait-task", |st| {
170                        st.wait(OneOfDurationOrIso8601Expression::Duration(
171                            Duration::from_seconds(25),
172                        ));
173                    });
174                });
175            })
176            .do_(listen_task_name, |task| {
177                task.listen().to(|e| {
178                    e.with("key", Value::String("value".to_string()));
179                });
180            })
181            .do_(raise_task_name, |task| {
182                task.raise()
183                    .error()
184                    .with_type(raise_error_type)
185                    .with_status(raise_error_status)
186                    .with_title(raise_error_title)
187                    .with_detail(raise_error_detail)
188                    .with_instance(raise_error_instance);
189            })
190            .do_(run_container_task_name, |task| {
191                task.run()
192                    .container()
193                    .with_image(container_image)
194                    .with_command(container_command)
195                    .with_ports(container_ports.clone())
196                    .with_volumes(container_volumes.clone())
197                    .with_environment_variables(container_environment.clone());
198            })
199            .do_(run_script_task_name, |task| {
200                task.run().script().with_code(script_code);
201            })
202            .do_(run_shell_task_name, |task| {
203                task.run().shell().with_command(shell_command_name);
204            })
205            .do_(run_workflow_task_name, |task| {
206                task.run()
207                    .workflow()
208                    .with_namespace(workflow_namespace)
209                    .with_name(workflow_name)
210                    .with_version(workflow_version)
211                    .with_input(workflow_input.clone());
212            })
213            .do_(set_task_name, |task| {
214                task.set().variables(set_task_variables.clone());
215            })
216            .do_(switch_task_name, |task| {
217                task.switch().case_(switch_case_name, |case| {
218                    case.when(switch_case_when).then(switch_case_then);
219                });
220            })
221            .do_(try_task_name, |task| {
222                task.try_()
223                    .do_("fake-wait-task", |subtask| {
224                        subtask.wait(OneOfDurationOrIso8601Expression::Duration(
225                            Duration::from_seconds(5),
226                        ));
227                    })
228                    .catch(|catch| {
229                        catch
230                            .errors(|errors| {
231                                errors
232                                    .with_type(catch_error_type)
233                                    .with_status(catch_error_status.clone());
234                            })
235                            .when(catch_when)
236                            .retry(|retry| {
237                                retry
238                                    .except_when(retry_except_when)
239                                    .delay(Duration::from_seconds(1))
240                                    .backoff(|backoff| {
241                                        backoff
242                                            .linear()
243                                            .with_increment(Duration::from_milliseconds(500));
244                                    })
245                                    .jitter(|jitter| {
246                                        jitter
247                                            .from(Duration::from_seconds(1))
248                                            .to(Duration::from_seconds(3));
249                                    });
250                            });
251                    });
252            })
253            .do_(wait_task_name, |task| {
254                task.wait(wait_duration.clone());
255            })
256            .build();
257
258        //assert
259        assert_eq!(workflow.document.dsl, dsl_version);
260        assert_eq!(workflow.document.namespace, namespace);
261        assert_eq!(workflow.document.name, name);
262        assert_eq!(workflow.document.version, version);
263        assert_eq!(workflow.document.title, Some(title.to_string()));
264        assert_eq!(workflow.document.summary, Some(summary.to_string()));
265        assert_eq!(workflow.document.tags, Some(tags));
266        assert_eq!(
267            workflow.timeout.as_ref().and_then(|t| match t {
268                OneOfTimeoutDefinitionOrReference::Timeout(definition) => match &definition.after {
269                    OneOfDurationOrIso8601Expression::Duration(duration) => Some(duration),
270                    OneOfDurationOrIso8601Expression::Iso8601Expression(_) => None,
271                },
272                OneOfTimeoutDefinitionOrReference::Reference(_) => None,
273            }),
274            Some(&timeout_duration)
275        );
276        assert!(
277            workflow.use_.as_ref()
278                .and_then(|component_collection| component_collection.authentications.as_ref())
279                .and_then(|authentications| authentications.get(basic_name))
280                .map(|auth_policy| matches!(auth_policy, swf_core::models::authentication::ReferenceableAuthenticationPolicy::Policy(p) if p.basic.is_some()))
281                .unwrap_or(false),
282            "Expected authentications to contain an entry with the name '{}' and a non-null `basic` property.",
283            basic_name);
284        assert!(
285            workflow
286                .do_
287                .entries
288                .iter()
289                .any(|(name, task)| name == call_task_name && {
290                    if let TaskDefinition::Call(call_def) = task {
291                        if let swf_core::models::call::CallTaskDefinition::Function(ref f) =
292                            call_def.as_ref()
293                        {
294                            f.call == call_function_name && f.with == Some(call_task_with.clone())
295                        } else {
296                            false
297                        }
298                    } else {
299                        false
300                    }
301                }),
302            "Expected a task with key '{}' and a CallTaskDefinition with 'call'={} and 'with'={:?}",
303            call_task_name,
304            call_function_name,
305            call_task_with
306        );
307        assert!(
308            workflow
309                .do_
310                .entries
311                .iter()
312                .any(|(name, task)| name == do_task_name && matches!(task, TaskDefinition::Do(_))),
313            "Expected a do task with key '{}'",
314            do_task_name
315        );
316        assert!(
317            workflow.do_
318                .entries
319                .iter()
320                .any(|(name, task)| name == emit_task_name && {
321                    if let TaskDefinition::Emit(emit_task) = task {
322                        emit_task.emit.event.with == emit_event_attributes.clone()
323                    } else {
324                        false
325                    }
326                }),
327            "Expected a task with key '{}' and a EmitTaskDefinition with 'emit.event.with' matching supplied attributes",
328            emit_task_name);
329        assert!(
330            workflow.do_
331                .entries
332                .iter()
333                .any(|(name, task)| name == for_task_name && {
334                    if let TaskDefinition::For(for_task) = task {
335                        for_task.for_.each == for_each && for_task.for_.in_ == for_each_in && for_task.for_.at == Some(for_each_at.to_string())
336                    } else {
337                        false
338                    }
339                }),
340            "Expected a task with key '{}' and a ForTaskDefinition with 'for.each'={}, 'for.in'={}' and 'for.at'={}'",
341            for_task_name,
342            for_each,
343            for_each_in,
344            for_each_at);
345        assert!(
346            workflow
347                .do_
348                .entries
349                .iter()
350                .any(|(name, task)| name == fork_task_name
351                    && matches!(task, TaskDefinition::Fork(_))),
352            "Expected a fork task with key '{}'",
353            fork_task_name,
354        );
355        assert!(
356            workflow
357                .do_
358                .entries
359                .iter()
360                .any(|(name, task)| name == listen_task_name
361                    && matches!(task, TaskDefinition::Listen(_))),
362            "Expected a listen task with key '{}'",
363            listen_task_name
364        );
365        assert!(
366            workflow.do_
367                .entries
368                .iter()
369                .any(|(name, task)| name == raise_task_name && {
370                    if let TaskDefinition::Raise(raise_task) = task {
371                        if let OneOfErrorDefinitionOrReference::Error(error) = &raise_task.raise.error {
372                            error.type_.as_str() == raise_error_type
373                                && error.title == Some(raise_error_title.to_string())
374                                && error.detail == Some(raise_error_detail.to_string())
375                                && error.instance == Some(raise_error_instance.to_string())
376                        } else {
377                            false
378                        }
379                    } else {
380                        false
381                    }
382                }),
383            "Expected a task with key '{}' and a RaiseTaskDefinition with 'raise.error.type'={}, 'raise.error.title'={}, 'raise.error.detail'={} and 'raise.error.instance'={}",
384            raise_task_name,
385            raise_error_type,
386            raise_error_title,
387            raise_error_detail,
388            raise_error_instance);
389        assert!(
390            workflow
391                .do_
392                .entries
393                .iter()
394                .any(|(name, task)| name == run_container_task_name && {
395                    if let TaskDefinition::Run(run_task) = task {
396                        if let Some(container) = &run_task.run.container {
397                            container.image == container_image
398                                && container.command == Some(container_command.to_string())
399                                && container.ports == Some(container_ports.clone())
400                                && container.volumes == Some(container_volumes.clone())
401                                && container.environment == Some(container_environment.clone())
402                        } else {
403                            false
404                        }
405                    } else {
406                        false
407                    }
408                }),
409            "Expected a task with key '{}' and a RunTaskDefinition with 'container.image'={}, 'container.command'={}, 'container.ports'={:?}, 'container.volumes'={:?}, and 'container.environment'={:?}",
410            run_container_task_name,
411            container_image,
412            container_command,
413            container_ports,
414            container_volumes,
415            container_environment);
416        assert!(
417            workflow
418                .do_
419                .entries
420                .iter()
421                .any(|(name, task)| name == run_workflow_task_name && {
422                    if let TaskDefinition::Run(run_task) = task {
423                        if let Some(subflow) = &run_task.run.workflow{
424                            subflow.namespace == workflow_namespace
425                                && subflow.name == workflow_name
426                                && subflow.version == workflow_version
427                                && subflow.input == Some(workflow_input.clone())
428                        }
429                        else{
430                            false
431                        }
432                    } else {
433                        false
434                    }
435                }),
436            "Expected a task with key '{}' and a RunTaskDefinition with 'workflow.namespace'={}, 'workflow.name'={}, 'workflow.version'={}, and 'workflow.input'={:?}",
437            run_workflow_task_name,
438            workflow_namespace,
439            workflow_name,
440            workflow_version,
441            workflow_input);
442        assert!(
443            workflow
444                .do_
445                .entries
446                .iter()
447                .any(|(name, task)| name == set_task_name && {
448                    if let TaskDefinition::Set(set_task) = task {
449                        match &set_task.set {
450                            SetValue::Map(map) => map == &set_task_variables,
451                            _ => false,
452                        }
453                    } else {
454                        false
455                    }
456                }),
457            "Expected a task with key '{}' and a SetTaskDefinition with specified variables",
458            set_task_name
459        );
460        assert!(
461            workflow
462                .do_
463                .entries
464                .iter()
465                .any(|(name, task)| name == switch_task_name && {
466                    if let TaskDefinition::Switch(switch_task) = task {
467                        switch_task
468                            .switch
469                            .entries
470                            .iter()
471                            .any(|(case_name, _)| case_name == switch_case_name)
472                    } else {
473                        false
474                    }
475                }),
476            "Expected a task with key '{}' and a SwitchTaskDefinition with a case named '{}'",
477            switch_task_name,
478            switch_case_name
479        );
480        assert!(
481            workflow
482                .do_
483                .entries
484                .iter()
485                .any(|(name, task)| name == try_task_name && {
486                    if let TaskDefinition::Try(try_task) = task {
487                        try_task.catch.when == Some(catch_when.to_string())
488                    } else {
489                        false
490                    }
491                }),
492            "Expected a task with key '{}' and a TryTaskDefinition with 'catch.when'={}",
493            try_task_name,
494            catch_when
495        );
496        assert!(
497            workflow
498                .do_
499                .entries
500                .iter()
501                .any(|(name, task)| name == wait_task_name && {
502                    if let TaskDefinition::Wait(wait_task) = task {
503                        wait_task.wait == wait_duration
504                    } else {
505                        false
506                    }
507                }),
508            "Expected a task with key '{}' and a WaitTaskDefinition with 'wait'={}",
509            wait_task_name,
510            wait_duration
511        );
512    }
513}