1#![deny(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub mod services;
5
6pub 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 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 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 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_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}