1use std::sync::Arc;
19
20use async_trait::async_trait;
21use http::StatusCode;
22use serde_json::{json, Map, Value};
23use tokio::sync::Mutex as AsyncMutex;
24
25use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
26use fakecloud_persistence::SnapshotStore;
27
28use crate::shared::{domain_arn, new_run_id, new_task_token, now_epoch};
29use crate::state::{
30 ActivityRecord, ActivityRef, ActivityState, Domain, Execution, SharedSwfState, SwfData,
31 TypeRecord,
32};
33
34pub const SWF_ACTIONS: &[&str] = &[
36 "CountClosedWorkflowExecutions",
37 "CountOpenWorkflowExecutions",
38 "CountPendingActivityTasks",
39 "CountPendingDecisionTasks",
40 "DeleteActivityType",
41 "DeleteWorkflowType",
42 "DeprecateActivityType",
43 "DeprecateDomain",
44 "DeprecateWorkflowType",
45 "DescribeActivityType",
46 "DescribeDomain",
47 "DescribeWorkflowExecution",
48 "DescribeWorkflowType",
49 "GetWorkflowExecutionHistory",
50 "ListActivityTypes",
51 "ListClosedWorkflowExecutions",
52 "ListDomains",
53 "ListOpenWorkflowExecutions",
54 "ListTagsForResource",
55 "ListWorkflowTypes",
56 "PollForActivityTask",
57 "PollForDecisionTask",
58 "RecordActivityTaskHeartbeat",
59 "RegisterActivityType",
60 "RegisterDomain",
61 "RegisterWorkflowType",
62 "RequestCancelWorkflowExecution",
63 "RespondActivityTaskCanceled",
64 "RespondActivityTaskCompleted",
65 "RespondActivityTaskFailed",
66 "RespondDecisionTaskCompleted",
67 "SignalWorkflowExecution",
68 "StartWorkflowExecution",
69 "TagResource",
70 "TerminateWorkflowExecution",
71 "UndeprecateActivityType",
72 "UndeprecateDomain",
73 "UndeprecateWorkflowType",
74 "UntagResource",
75];
76
77fn is_mutating_action(action: &str) -> bool {
81 const READ_PREFIXES: &[&str] = &["Describe", "List", "Count", "Get"];
82 !READ_PREFIXES.iter().any(|p| action.starts_with(p))
83}
84
85pub struct SwfService {
86 state: SharedSwfState,
87 snapshot_store: Option<Arc<dyn SnapshotStore>>,
88 snapshot_lock: Arc<AsyncMutex<()>>,
89}
90
91impl SwfService {
92 pub fn new(state: SharedSwfState) -> Self {
93 Self {
94 state,
95 snapshot_store: None,
96 snapshot_lock: Arc::new(AsyncMutex::new(())),
97 }
98 }
99
100 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
101 self.snapshot_store = Some(store);
102 self
103 }
104
105 async fn save(&self) {
106 crate::persistence::save_snapshot(
107 &self.state,
108 self.snapshot_store.clone(),
109 &self.snapshot_lock,
110 )
111 .await;
112 }
113
114 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
116 let store = self.snapshot_store.clone()?;
117 let state = self.state.clone();
118 let lock = self.snapshot_lock.clone();
119 Some(Arc::new(move || {
120 let state = state.clone();
121 let store = store.clone();
122 let lock = lock.clone();
123 Box::pin(async move {
124 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
125 })
126 }))
127 }
128
129 fn with_account_mut<R>(&self, req: &AwsRequest, f: impl FnOnce(&mut SwfData) -> R) -> R {
130 let mut guard = self.state.write();
131 let acct = guard.get_or_create(&req.account_id);
132 f(acct)
133 }
134}
135
136#[async_trait]
137impl AwsService for SwfService {
138 fn service_name(&self) -> &str {
139 "swf"
140 }
141
142 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
143 let action = req.action.clone();
144 if let Err(msg) = crate::validate::validate_input(&action, &req.json_body()) {
145 return Err(invalid_request(msg));
146 }
147 let result = self.dispatch(&action, &req);
148 if is_mutating_action(&action)
149 && matches!(result.as_ref(), Ok(resp) if resp.status.is_success())
150 {
151 self.save().await;
152 }
153 result
154 }
155
156 fn supported_actions(&self) -> &[&str] {
157 SWF_ACTIONS
158 }
159}
160
161impl SwfService {
162 fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
163 let body = req.json_body();
164 match action {
165 "RegisterDomain" => self.register_domain(req, &body),
167 "DeprecateDomain" => self.deprecate_domain(req, &body),
168 "UndeprecateDomain" => self.undeprecate_domain(req, &body),
169 "DescribeDomain" => self.describe_domain(req, &body),
170 "ListDomains" => self.list_domains(req, &body),
171 "RegisterActivityType" => self.register_type(req, &body, TypeKind::Activity),
173 "DeprecateActivityType" => self.deprecate_type(req, &body, TypeKind::Activity),
174 "UndeprecateActivityType" => self.undeprecate_type(req, &body, TypeKind::Activity),
175 "DeleteActivityType" => self.delete_type(req, &body, TypeKind::Activity),
176 "DescribeActivityType" => self.describe_type(req, &body, TypeKind::Activity),
177 "ListActivityTypes" => self.list_types(req, &body, TypeKind::Activity),
178 "RegisterWorkflowType" => self.register_type(req, &body, TypeKind::Workflow),
180 "DeprecateWorkflowType" => self.deprecate_type(req, &body, TypeKind::Workflow),
181 "UndeprecateWorkflowType" => self.undeprecate_type(req, &body, TypeKind::Workflow),
182 "DeleteWorkflowType" => self.delete_type(req, &body, TypeKind::Workflow),
183 "DescribeWorkflowType" => self.describe_type(req, &body, TypeKind::Workflow),
184 "ListWorkflowTypes" => self.list_types(req, &body, TypeKind::Workflow),
185 "StartWorkflowExecution" => self.start_workflow_execution(req, &body),
187 "DescribeWorkflowExecution" => self.describe_workflow_execution(req, &body),
188 "GetWorkflowExecutionHistory" => self.get_workflow_execution_history(req, &body),
189 "ListOpenWorkflowExecutions" => self.list_executions(req, &body, true),
190 "ListClosedWorkflowExecutions" => self.list_executions(req, &body, false),
191 "CountOpenWorkflowExecutions" => self.count_executions(req, &body, true),
192 "CountClosedWorkflowExecutions" => self.count_executions(req, &body, false),
193 "CountPendingActivityTasks" => self.count_pending_tasks(req, &body, true),
194 "CountPendingDecisionTasks" => self.count_pending_tasks(req, &body, false),
195 "SignalWorkflowExecution" => self.signal_workflow_execution(req, &body),
196 "RequestCancelWorkflowExecution" => self.request_cancel_workflow_execution(req, &body),
197 "TerminateWorkflowExecution" => self.terminate_workflow_execution(req, &body),
198 "PollForDecisionTask" => self.poll_for_decision_task(req, &body),
200 "PollForActivityTask" => self.poll_for_activity_task(req, &body),
201 "RespondDecisionTaskCompleted" => self.respond_decision_task_completed(req, &body),
202 "RespondActivityTaskCompleted" => {
203 self.respond_activity_task(req, &body, ActivityOutcome::Completed)
204 }
205 "RespondActivityTaskFailed" => {
206 self.respond_activity_task(req, &body, ActivityOutcome::Failed)
207 }
208 "RespondActivityTaskCanceled" => {
209 self.respond_activity_task(req, &body, ActivityOutcome::Canceled)
210 }
211 "RecordActivityTaskHeartbeat" => self.record_activity_task_heartbeat(req, &body),
212 "TagResource" => self.tag_resource(req, &body),
214 "UntagResource" => self.untag_resource(req, &body),
215 "ListTagsForResource" => self.list_tags_for_resource(req, &body),
216 _ => Err(AwsServiceError::action_not_implemented(
217 self.service_name(),
218 action,
219 )),
220 }
221 }
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225enum TypeKind {
226 Activity,
227 Workflow,
228}
229
230#[derive(Debug, Clone, Copy)]
231enum ActivityOutcome {
232 Completed,
233 Failed,
234 Canceled,
235}
236
237fn fault(code: &str, msg: impl Into<String>) -> AwsServiceError {
240 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg.into())
241}
242
243fn invalid_request(msg: impl Into<String>) -> AwsServiceError {
244 fault("ValidationException", msg)
245}
246
247fn unknown_resource(msg: impl Into<String>) -> AwsServiceError {
248 fault("UnknownResourceFault", msg)
249}
250
251fn ok(value: Value) -> Result<AwsResponse, AwsServiceError> {
252 Ok(AwsResponse::ok_json(value))
253}
254
255fn empty_ok() -> Result<AwsResponse, AwsServiceError> {
256 ok(json!({}))
257}
258
259fn str_member<'a>(body: &'a Value, name: &str) -> Option<&'a str> {
262 body.get(name).and_then(Value::as_str)
263}
264
265fn copy_members(body: &Value, obj: &mut Map<String, Value>, names: &[&str]) {
268 for name in names {
269 if let Some(v) = body.get(*name).filter(|v| !v.is_null()) {
270 obj.insert((*name).to_string(), v.clone());
271 }
272 }
273}
274
275fn task_list_name(v: Option<&Value>) -> Option<String> {
277 v.and_then(|t| t.get("name"))
278 .and_then(Value::as_str)
279 .map(str::to_string)
280}
281
282fn type_name_version(v: Option<&Value>) -> Option<(String, String)> {
284 let v = v?;
285 let name = v.get("name").and_then(Value::as_str)?;
286 let version = v.get("version").and_then(Value::as_str)?;
287 Some((name.to_string(), version.to_string()))
288}
289
290impl SwfService {
293 fn register_domain(
294 &self,
295 req: &AwsRequest,
296 body: &Value,
297 ) -> Result<AwsResponse, AwsServiceError> {
298 let name = str_member(body, "name").unwrap_or_default().to_string();
299 let retention = str_member(body, "workflowExecutionRetentionPeriodInDays")
300 .unwrap_or_default()
301 .to_string();
302 let description = str_member(body, "description").map(str::to_string);
303 let region = req.region.clone();
304 let account = req.account_id.clone();
305 let tags = body.get("tags").cloned();
306 self.with_account_mut(req, |data| {
307 if let Some(existing) = data.domains.get(&name) {
308 if existing.status == "REGISTERED" {
309 return Err(fault(
310 "DomainAlreadyExistsFault",
311 format!("Domain already exists: {name}"),
312 ));
313 }
314 }
315 let arn = domain_arn(®ion, &account, &name);
316 data.domains.insert(
317 name.clone(),
318 Domain {
319 name: name.clone(),
320 status: "REGISTERED".to_string(),
321 description,
322 retention_days: retention,
323 arn: arn.clone(),
324 },
325 );
326 if let Some(tag_list) = tags.as_ref() {
327 let map = resource_tags_to_map(tag_list);
328 if !map.is_empty() {
329 data.tags.insert(arn, map);
330 }
331 }
332 empty_ok()
333 })
334 }
335
336 fn deprecate_domain(
337 &self,
338 req: &AwsRequest,
339 body: &Value,
340 ) -> Result<AwsResponse, AwsServiceError> {
341 let name = str_member(body, "name").unwrap_or_default().to_string();
342 self.with_account_mut(req, |data| {
343 let Some(domain) = data.domains.get_mut(&name) else {
344 return Err(unknown_resource(format!("Unknown domain: {name}")));
345 };
346 if domain.status == "DEPRECATED" {
347 return Err(fault(
348 "DomainDeprecatedFault",
349 format!("Domain already deprecated: {name}"),
350 ));
351 }
352 domain.status = "DEPRECATED".to_string();
353 empty_ok()
354 })
355 }
356
357 fn undeprecate_domain(
358 &self,
359 req: &AwsRequest,
360 body: &Value,
361 ) -> Result<AwsResponse, AwsServiceError> {
362 let name = str_member(body, "name").unwrap_or_default().to_string();
363 self.with_account_mut(req, |data| {
364 let Some(domain) = data.domains.get_mut(&name) else {
365 return Err(unknown_resource(format!("Unknown domain: {name}")));
366 };
367 if domain.status == "REGISTERED" {
368 return Err(fault(
369 "DomainAlreadyExistsFault",
370 format!("Domain already registered: {name}"),
371 ));
372 }
373 domain.status = "REGISTERED".to_string();
374 empty_ok()
375 })
376 }
377
378 fn describe_domain(
379 &self,
380 req: &AwsRequest,
381 body: &Value,
382 ) -> Result<AwsResponse, AwsServiceError> {
383 let name = str_member(body, "name").unwrap_or_default().to_string();
384 self.with_account_mut(req, |data| {
385 let Some(domain) = data.domains.get(&name) else {
386 return Err(unknown_resource(format!("Unknown domain: {name}")));
387 };
388 ok(json!({
389 "domainInfo": wire_domain_info(domain),
390 "configuration": {
391 "workflowExecutionRetentionPeriodInDays": domain.retention_days,
392 },
393 }))
394 })
395 }
396
397 fn list_domains(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
398 let status = str_member(body, "registrationStatus")
399 .unwrap_or("REGISTERED")
400 .to_string();
401 let reverse = body
402 .get("reverseOrder")
403 .and_then(Value::as_bool)
404 .unwrap_or(false);
405 self.with_account_mut(req, |data| {
406 let mut infos: Vec<Value> = data
407 .domains
408 .values()
409 .filter(|d| d.status == status)
410 .map(wire_domain_info)
411 .collect();
412 if reverse {
413 infos.reverse();
414 }
415 ok(json!({ "domainInfos": infos }))
416 })
417 }
418}
419
420fn wire_domain_info(domain: &Domain) -> Value {
421 let mut obj = Map::new();
422 obj.insert("name".into(), json!(domain.name));
423 obj.insert("status".into(), json!(domain.status));
424 obj.insert("arn".into(), json!(domain.arn));
425 if let Some(desc) = &domain.description {
426 obj.insert("description".into(), json!(desc));
427 }
428 Value::Object(obj)
429}
430
431impl SwfService {
434 fn register_type(
435 &self,
436 req: &AwsRequest,
437 body: &Value,
438 kind: TypeKind,
439 ) -> Result<AwsResponse, AwsServiceError> {
440 let domain = str_member(body, "domain").unwrap_or_default().to_string();
441 let name = str_member(body, "name").unwrap_or_default().to_string();
442 let version = str_member(body, "version").unwrap_or_default().to_string();
443 let description = str_member(body, "description").map(str::to_string);
444 let config_members = config_member_names(kind);
445 let mut configuration = Map::new();
446 copy_members(body, &mut configuration, config_members);
447 self.with_account_mut(req, |data| {
448 if !data.domains.contains_key(&domain) {
449 return Err(unknown_resource(format!("Unknown domain: {domain}")));
450 }
451 let key = SwfData::type_map_key(&domain, &name, &version);
452 let map = type_map_mut(data, kind);
453 if let Some(existing) = map.get(&key) {
454 if existing.status == "REGISTERED" {
455 return Err(fault(
456 "TypeAlreadyExistsFault",
457 format!("Type already exists: {name} {version}"),
458 ));
459 }
460 }
461 map.insert(
462 key,
463 TypeRecord {
464 name,
465 version,
466 status: "REGISTERED".to_string(),
467 description,
468 creation_date: now_epoch(),
469 deprecation_date: None,
470 configuration,
471 },
472 );
473 empty_ok()
474 })
475 }
476
477 fn deprecate_type(
478 &self,
479 req: &AwsRequest,
480 body: &Value,
481 kind: TypeKind,
482 ) -> Result<AwsResponse, AwsServiceError> {
483 let domain = str_member(body, "domain").unwrap_or_default().to_string();
484 let member = type_member_name(kind);
485 let Some((name, version)) = type_name_version(body.get(member)) else {
486 return Err(invalid_request(format!("{member} is required")));
487 };
488 self.with_account_mut(req, |data| {
489 if !data.domains.contains_key(&domain) {
490 return Err(unknown_resource(format!("Unknown domain: {domain}")));
491 }
492 let key = SwfData::type_map_key(&domain, &name, &version);
493 let map = type_map_mut(data, kind);
494 let Some(rec) = map.get_mut(&key) else {
495 return Err(unknown_resource(format!("Unknown type: {name} {version}")));
496 };
497 if rec.status == "DEPRECATED" {
498 return Err(fault(
499 "TypeDeprecatedFault",
500 format!("Type already deprecated: {name} {version}"),
501 ));
502 }
503 rec.status = "DEPRECATED".to_string();
504 rec.deprecation_date = Some(now_epoch());
505 empty_ok()
506 })
507 }
508
509 fn undeprecate_type(
510 &self,
511 req: &AwsRequest,
512 body: &Value,
513 kind: TypeKind,
514 ) -> Result<AwsResponse, AwsServiceError> {
515 let domain = str_member(body, "domain").unwrap_or_default().to_string();
516 let member = type_member_name(kind);
517 let Some((name, version)) = type_name_version(body.get(member)) else {
518 return Err(invalid_request(format!("{member} is required")));
519 };
520 self.with_account_mut(req, |data| {
521 if !data.domains.contains_key(&domain) {
522 return Err(unknown_resource(format!("Unknown domain: {domain}")));
523 }
524 let key = SwfData::type_map_key(&domain, &name, &version);
525 let map = type_map_mut(data, kind);
526 let Some(rec) = map.get_mut(&key) else {
527 return Err(unknown_resource(format!("Unknown type: {name} {version}")));
528 };
529 if rec.status == "REGISTERED" {
530 return Err(fault(
531 "TypeAlreadyExistsFault",
532 format!("Type already registered: {name} {version}"),
533 ));
534 }
535 rec.status = "REGISTERED".to_string();
536 rec.deprecation_date = None;
537 empty_ok()
538 })
539 }
540
541 fn delete_type(
542 &self,
543 req: &AwsRequest,
544 body: &Value,
545 kind: TypeKind,
546 ) -> Result<AwsResponse, AwsServiceError> {
547 let domain = str_member(body, "domain").unwrap_or_default().to_string();
548 let member = type_member_name(kind);
549 let Some((name, version)) = type_name_version(body.get(member)) else {
550 return Err(invalid_request(format!("{member} is required")));
551 };
552 self.with_account_mut(req, |data| {
553 if !data.domains.contains_key(&domain) {
554 return Err(unknown_resource(format!("Unknown domain: {domain}")));
555 }
556 let key = SwfData::type_map_key(&domain, &name, &version);
557 let map = type_map_mut(data, kind);
558 let Some(rec) = map.get(&key) else {
559 return Err(unknown_resource(format!("Unknown type: {name} {version}")));
560 };
561 if rec.status != "DEPRECATED" {
562 return Err(fault(
563 "TypeNotDeprecatedFault",
564 format!("Type must be deprecated before deletion: {name} {version}"),
565 ));
566 }
567 map.remove(&key);
568 empty_ok()
569 })
570 }
571
572 fn describe_type(
573 &self,
574 req: &AwsRequest,
575 body: &Value,
576 kind: TypeKind,
577 ) -> Result<AwsResponse, AwsServiceError> {
578 let domain = str_member(body, "domain").unwrap_or_default().to_string();
579 let member = type_member_name(kind);
580 let Some((name, version)) = type_name_version(body.get(member)) else {
581 return Err(invalid_request(format!("{member} is required")));
582 };
583 self.with_account_mut(req, |data| {
584 if !data.domains.contains_key(&domain) {
585 return Err(unknown_resource(format!("Unknown domain: {domain}")));
586 }
587 let key = SwfData::type_map_key(&domain, &name, &version);
588 let map = type_map_mut(data, kind);
589 let Some(rec) = map.get(&key) else {
590 return Err(unknown_resource(format!("Unknown type: {name} {version}")));
591 };
592 ok(json!({
593 "typeInfo": wire_type_info(rec, kind),
594 "configuration": Value::Object(rec.configuration.clone()),
595 }))
596 })
597 }
598
599 fn list_types(
600 &self,
601 req: &AwsRequest,
602 body: &Value,
603 kind: TypeKind,
604 ) -> Result<AwsResponse, AwsServiceError> {
605 let domain = str_member(body, "domain").unwrap_or_default().to_string();
606 let status = str_member(body, "registrationStatus")
607 .unwrap_or("REGISTERED")
608 .to_string();
609 let name_filter = str_member(body, "name").map(str::to_string);
610 let reverse = body
611 .get("reverseOrder")
612 .and_then(Value::as_bool)
613 .unwrap_or(false);
614 let prefix = format!("{domain}\u{1}");
615 self.with_account_mut(req, |data| {
616 if !data.domains.contains_key(&domain) {
617 return Err(unknown_resource(format!("Unknown domain: {domain}")));
618 }
619 let map = type_map_mut(data, kind);
620 let mut infos: Vec<Value> = map
621 .iter()
622 .filter(|(k, _)| k.starts_with(&prefix))
623 .map(|(_, rec)| rec)
624 .filter(|rec| rec.status == status)
625 .filter(|rec| name_filter.as_ref().is_none_or(|n| &rec.name == n))
626 .map(|rec| wire_type_info(rec, kind))
627 .collect();
628 if reverse {
629 infos.reverse();
630 }
631 ok(json!({ "typeInfos": infos }))
632 })
633 }
634}
635
636fn wire_type_info(rec: &TypeRecord, kind: TypeKind) -> Value {
637 let type_member = match kind {
638 TypeKind::Activity => "activityType",
639 TypeKind::Workflow => "workflowType",
640 };
641 let mut obj = Map::new();
642 obj.insert(
643 type_member.into(),
644 json!({ "name": rec.name, "version": rec.version }),
645 );
646 obj.insert("status".into(), json!(rec.status));
647 obj.insert("creationDate".into(), json!(rec.creation_date));
648 if let Some(desc) = &rec.description {
649 obj.insert("description".into(), json!(desc));
650 }
651 if let Some(dep) = rec.deprecation_date {
652 obj.insert("deprecationDate".into(), json!(dep));
653 }
654 Value::Object(obj)
655}
656
657fn config_member_names(kind: TypeKind) -> &'static [&'static str] {
658 match kind {
659 TypeKind::Activity => &[
660 "defaultTaskStartToCloseTimeout",
661 "defaultTaskHeartbeatTimeout",
662 "defaultTaskList",
663 "defaultTaskPriority",
664 "defaultTaskScheduleToStartTimeout",
665 "defaultTaskScheduleToCloseTimeout",
666 ],
667 TypeKind::Workflow => &[
668 "defaultTaskStartToCloseTimeout",
669 "defaultExecutionStartToCloseTimeout",
670 "defaultTaskList",
671 "defaultTaskPriority",
672 "defaultChildPolicy",
673 "defaultLambdaRole",
674 ],
675 }
676}
677
678fn type_member_name(kind: TypeKind) -> &'static str {
679 match kind {
680 TypeKind::Activity => "activityType",
681 TypeKind::Workflow => "workflowType",
682 }
683}
684
685fn type_map_mut(
686 data: &mut SwfData,
687 kind: TypeKind,
688) -> &mut std::collections::BTreeMap<String, TypeRecord> {
689 match kind {
690 TypeKind::Activity => &mut data.activity_types,
691 TypeKind::Workflow => &mut data.workflow_types,
692 }
693}
694
695impl SwfService {
698 fn start_workflow_execution(
699 &self,
700 req: &AwsRequest,
701 body: &Value,
702 ) -> Result<AwsResponse, AwsServiceError> {
703 let domain = str_member(body, "domain").unwrap_or_default().to_string();
704 let workflow_id = str_member(body, "workflowId")
705 .unwrap_or_default()
706 .to_string();
707 let Some((wf_name, wf_version)) = type_name_version(body.get("workflowType")) else {
708 return Err(invalid_request("workflowType is required"));
709 };
710 let input = str_member(body, "input").map(str::to_string);
711 let lambda_role = str_member(body, "lambdaRole").map(str::to_string);
712 let task_priority = str_member(body, "taskPriority").map(str::to_string);
713 let tag_list: Vec<String> = body
714 .get("tagList")
715 .and_then(Value::as_array)
716 .map(|a| {
717 a.iter()
718 .filter_map(|v| v.as_str().map(str::to_string))
719 .collect()
720 })
721 .unwrap_or_default();
722
723 self.with_account_mut(req, |data| {
724 if !data.domains.contains_key(&domain) {
725 return Err(unknown_resource(format!("Unknown domain: {domain}")));
726 }
727 let key = SwfData::type_map_key(&domain, &wf_name, &wf_version);
728 let Some(wf_type) = data.workflow_types.get(&key) else {
729 return Err(unknown_resource(format!(
730 "Unknown workflow type: {wf_name} {wf_version}"
731 )));
732 };
733 if wf_type.status == "DEPRECATED" {
734 return Err(fault(
735 "TypeDeprecatedFault",
736 format!("Workflow type is deprecated: {wf_name} {wf_version}"),
737 ));
738 }
739 let cfg = &wf_type.configuration;
740 let task_list = task_list_name(body.get("taskList"))
744 .or_else(|| task_list_name(cfg.get("defaultTaskList")))
745 .ok_or_else(|| default_undefined("taskList"))?;
746 let child_policy = str_member(body, "childPolicy")
747 .map(str::to_string)
748 .or_else(|| {
749 cfg.get("defaultChildPolicy")
750 .and_then(Value::as_str)
751 .map(str::to_string)
752 })
753 .ok_or_else(|| default_undefined("childPolicy"))?;
754 let exec_timeout = str_member(body, "executionStartToCloseTimeout")
755 .map(str::to_string)
756 .or_else(|| {
757 cfg.get("defaultExecutionStartToCloseTimeout")
758 .and_then(Value::as_str)
759 .map(str::to_string)
760 })
761 .ok_or_else(|| default_undefined("executionStartToCloseTimeout"))?;
762 let task_timeout = str_member(body, "taskStartToCloseTimeout")
763 .map(str::to_string)
764 .or_else(|| {
765 cfg.get("defaultTaskStartToCloseTimeout")
766 .and_then(Value::as_str)
767 .map(str::to_string)
768 })
769 .ok_or_else(|| default_undefined("taskStartToCloseTimeout"))?;
770
771 if data
772 .executions
773 .values()
774 .any(|e| e.workflow_id == workflow_id && e.status == "OPEN")
775 {
776 return Err(fault(
777 "WorkflowExecutionAlreadyStartedFault",
778 format!("Workflow execution already started: {workflow_id}"),
779 ));
780 }
781
782 let run_id = new_run_id();
783 let mut exec = Execution {
784 domain: domain.clone(),
785 workflow_id: workflow_id.clone(),
786 run_id: run_id.clone(),
787 workflow_type_name: wf_name.clone(),
788 workflow_type_version: wf_version.clone(),
789 task_list: task_list.clone(),
790 task_priority: task_priority.clone(),
791 status: "OPEN".to_string(),
792 close_status: None,
793 start_timestamp: now_epoch(),
794 close_timestamp: None,
795 tag_list: tag_list.clone(),
796 input: input.clone(),
797 child_policy: child_policy.clone(),
798 task_start_to_close_timeout: task_timeout.clone(),
799 execution_start_to_close_timeout: exec_timeout.clone(),
800 lambda_role: lambda_role.clone(),
801 cancel_requested: false,
802 latest_execution_context: None,
803 events: Vec::new(),
804 next_event_id: 1,
805 decision_scheduled: false,
806 decision_scheduled_event_id: None,
807 decision_started_event_id: None,
808 previous_started_event_id: 0,
809 decision_task_token: None,
810 activities: Vec::new(),
811 };
812
813 let mut started_attrs = Map::new();
816 started_attrs.insert(
817 "workflowType".into(),
818 json!({ "name": wf_name, "version": wf_version }),
819 );
820 started_attrs.insert("taskList".into(), json!({ "name": task_list }));
821 started_attrs.insert("childPolicy".into(), json!(child_policy));
822 started_attrs.insert("executionStartToCloseTimeout".into(), json!(exec_timeout));
823 started_attrs.insert("taskStartToCloseTimeout".into(), json!(task_timeout));
824 if let Some(i) = &input {
825 started_attrs.insert("input".into(), json!(i));
826 }
827 if !tag_list.is_empty() {
828 started_attrs.insert("tagList".into(), json!(tag_list));
829 }
830 if let Some(lr) = &lambda_role {
831 started_attrs.insert("lambdaRole".into(), json!(lr));
832 }
833 push_event(
834 &mut exec,
835 "WorkflowExecutionStarted",
836 "workflowExecutionStartedEventAttributes",
837 Value::Object(started_attrs),
838 );
839 schedule_decision_task(&mut exec);
840
841 data.executions.insert(run_id.clone(), exec);
842 ok(json!({ "runId": run_id }))
843 })
844 }
845
846 fn describe_workflow_execution(
847 &self,
848 req: &AwsRequest,
849 body: &Value,
850 ) -> Result<AwsResponse, AwsServiceError> {
851 let domain = str_member(body, "domain").unwrap_or_default().to_string();
852 let workflow_id = execution_workflow_id(body);
853 let run_id = execution_run_id(body);
854 self.with_account_mut(req, |data| {
855 if !data.domains.contains_key(&domain) {
856 return Err(unknown_resource(format!("Unknown domain: {domain}")));
857 }
858 let Some(exec) = data.find_execution(&workflow_id, run_id.as_deref()) else {
859 return Err(unknown_resource(format!(
860 "Unknown execution: {workflow_id}"
861 )));
862 };
863 let mut out = Map::new();
864 out.insert("executionInfo".into(), wire_execution_info(exec));
865 out.insert("executionConfiguration".into(), wire_execution_config(exec));
866 out.insert("openCounts".into(), wire_open_counts(exec));
867 if let Some(ctx) = &exec.latest_execution_context {
868 out.insert("latestExecutionContext".into(), json!(ctx));
869 }
870 ok(Value::Object(out))
871 })
872 }
873
874 fn get_workflow_execution_history(
875 &self,
876 req: &AwsRequest,
877 body: &Value,
878 ) -> Result<AwsResponse, AwsServiceError> {
879 let domain = str_member(body, "domain").unwrap_or_default().to_string();
880 let workflow_id = execution_workflow_id(body);
881 let run_id = execution_run_id(body);
882 let reverse = body
883 .get("reverseOrder")
884 .and_then(Value::as_bool)
885 .unwrap_or(false);
886 self.with_account_mut(req, |data| {
887 if !data.domains.contains_key(&domain) {
888 return Err(unknown_resource(format!("Unknown domain: {domain}")));
889 }
890 let Some(exec) = data.find_execution(&workflow_id, run_id.as_deref()) else {
891 return Err(unknown_resource(format!(
892 "Unknown execution: {workflow_id}"
893 )));
894 };
895 let mut events = exec.events.clone();
896 if reverse {
897 events.reverse();
898 }
899 ok(json!({ "events": events }))
900 })
901 }
902
903 fn list_executions(
904 &self,
905 req: &AwsRequest,
906 body: &Value,
907 open: bool,
908 ) -> Result<AwsResponse, AwsServiceError> {
909 let domain = str_member(body, "domain").unwrap_or_default().to_string();
910 let reverse = body
911 .get("reverseOrder")
912 .and_then(Value::as_bool)
913 .unwrap_or(false);
914 self.with_account_mut(req, |data| {
915 if !data.domains.contains_key(&domain) {
916 return Err(unknown_resource(format!("Unknown domain: {domain}")));
917 }
918 let want = if open { "OPEN" } else { "CLOSED" };
919 let mut infos: Vec<Value> = data
920 .executions
921 .values()
922 .filter(|e| e.domain == domain && e.status == want)
923 .map(wire_execution_info)
924 .collect();
925 if reverse {
926 infos.reverse();
927 }
928 ok(json!({ "executionInfos": infos }))
929 })
930 }
931
932 fn count_executions(
933 &self,
934 req: &AwsRequest,
935 body: &Value,
936 open: bool,
937 ) -> Result<AwsResponse, AwsServiceError> {
938 let domain = str_member(body, "domain").unwrap_or_default().to_string();
939 self.with_account_mut(req, |data| {
940 if !data.domains.contains_key(&domain) {
941 return Err(unknown_resource(format!("Unknown domain: {domain}")));
942 }
943 let want = if open { "OPEN" } else { "CLOSED" };
944 let count = data
945 .executions
946 .values()
947 .filter(|e| e.domain == domain && e.status == want)
948 .count();
949 ok(json!({ "count": count, "truncated": false }))
950 })
951 }
952
953 fn count_pending_tasks(
954 &self,
955 req: &AwsRequest,
956 body: &Value,
957 activity: bool,
958 ) -> Result<AwsResponse, AwsServiceError> {
959 let domain = str_member(body, "domain").unwrap_or_default().to_string();
960 let task_list = task_list_name(body.get("taskList")).unwrap_or_default();
961 self.with_account_mut(req, |data| {
962 if !data.domains.contains_key(&domain) {
963 return Err(unknown_resource(format!("Unknown domain: {domain}")));
964 }
965 let count = if activity {
966 data.executions
967 .values()
968 .filter(|e| e.domain == domain)
969 .flat_map(|e| e.activities.iter())
970 .filter(|a| a.state == ActivityState::Scheduled && a.task_list == task_list)
971 .count()
972 } else {
973 data.executions
974 .values()
975 .filter(|e| {
976 e.domain == domain
977 && e.task_list == task_list
978 && e.decision_scheduled
979 && e.decision_task_token.is_none()
980 })
981 .count()
982 };
983 ok(json!({ "count": count, "truncated": false }))
984 })
985 }
986
987 fn signal_workflow_execution(
988 &self,
989 req: &AwsRequest,
990 body: &Value,
991 ) -> Result<AwsResponse, AwsServiceError> {
992 let domain = str_member(body, "domain").unwrap_or_default().to_string();
993 let workflow_id = str_member(body, "workflowId")
994 .unwrap_or_default()
995 .to_string();
996 let run_id = str_member(body, "runId").map(str::to_string);
997 let signal_name = str_member(body, "signalName")
998 .unwrap_or_default()
999 .to_string();
1000 let input = str_member(body, "input").map(str::to_string);
1001 self.with_account_mut(req, |data| {
1002 if !data.domains.contains_key(&domain) {
1003 return Err(unknown_resource(format!("Unknown domain: {domain}")));
1004 }
1005 let Some(exec) = data.find_execution_mut(&workflow_id, run_id.as_deref()) else {
1006 return Err(unknown_resource(format!(
1007 "Unknown execution: {workflow_id}"
1008 )));
1009 };
1010 let mut attrs = Map::new();
1011 attrs.insert("signalName".into(), json!(signal_name));
1012 if let Some(i) = &input {
1013 attrs.insert("input".into(), json!(i));
1014 }
1015 push_event(
1016 exec,
1017 "WorkflowExecutionSignaled",
1018 "workflowExecutionSignaledEventAttributes",
1019 Value::Object(attrs),
1020 );
1021 schedule_decision_task(exec);
1022 empty_ok()
1023 })
1024 }
1025
1026 fn request_cancel_workflow_execution(
1027 &self,
1028 req: &AwsRequest,
1029 body: &Value,
1030 ) -> Result<AwsResponse, AwsServiceError> {
1031 let domain = str_member(body, "domain").unwrap_or_default().to_string();
1032 let workflow_id = str_member(body, "workflowId")
1033 .unwrap_or_default()
1034 .to_string();
1035 let run_id = str_member(body, "runId").map(str::to_string);
1036 self.with_account_mut(req, |data| {
1037 if !data.domains.contains_key(&domain) {
1038 return Err(unknown_resource(format!("Unknown domain: {domain}")));
1039 }
1040 let Some(exec) = data.find_execution_mut(&workflow_id, run_id.as_deref()) else {
1041 return Err(unknown_resource(format!(
1042 "Unknown execution: {workflow_id}"
1043 )));
1044 };
1045 exec.cancel_requested = true;
1046 push_event(
1047 exec,
1048 "WorkflowExecutionCancelRequested",
1049 "workflowExecutionCancelRequestedEventAttributes",
1050 json!({ "cause": "CHILD_POLICY_APPLIED" }),
1051 );
1052 schedule_decision_task(exec);
1053 empty_ok()
1054 })
1055 }
1056
1057 fn terminate_workflow_execution(
1058 &self,
1059 req: &AwsRequest,
1060 body: &Value,
1061 ) -> Result<AwsResponse, AwsServiceError> {
1062 let domain = str_member(body, "domain").unwrap_or_default().to_string();
1063 let workflow_id = str_member(body, "workflowId")
1064 .unwrap_or_default()
1065 .to_string();
1066 let run_id = str_member(body, "runId").map(str::to_string);
1067 let reason = str_member(body, "reason").map(str::to_string);
1068 let details = str_member(body, "details").map(str::to_string);
1069 let child_policy = str_member(body, "childPolicy").map(str::to_string);
1070 self.with_account_mut(req, |data| {
1071 if !data.domains.contains_key(&domain) {
1072 return Err(unknown_resource(format!("Unknown domain: {domain}")));
1073 }
1074 let Some(exec) = data.find_execution_mut(&workflow_id, run_id.as_deref()) else {
1075 return Err(unknown_resource(format!(
1076 "Unknown execution: {workflow_id}"
1077 )));
1078 };
1079 let mut attrs = Map::new();
1080 if let Some(r) = &reason {
1081 attrs.insert("reason".into(), json!(r));
1082 }
1083 if let Some(d) = &details {
1084 attrs.insert("details".into(), json!(d));
1085 }
1086 attrs.insert(
1087 "childPolicy".into(),
1088 json!(child_policy
1089 .clone()
1090 .unwrap_or_else(|| exec.child_policy.clone())),
1091 );
1092 push_event(
1093 exec,
1094 "WorkflowExecutionTerminated",
1095 "workflowExecutionTerminatedEventAttributes",
1096 Value::Object(attrs),
1097 );
1098 close_execution(exec, "TERMINATED");
1099 empty_ok()
1100 })
1101 }
1102}
1103
1104fn default_undefined(field: &str) -> AwsServiceError {
1105 fault(
1106 "DefaultUndefinedFault",
1107 format!("No default configured for {field} and none supplied"),
1108 )
1109}
1110
1111fn execution_workflow_id(body: &Value) -> String {
1112 body.get("execution")
1113 .and_then(|e| e.get("workflowId"))
1114 .and_then(Value::as_str)
1115 .unwrap_or_default()
1116 .to_string()
1117}
1118
1119fn execution_run_id(body: &Value) -> Option<String> {
1120 body.get("execution")
1121 .and_then(|e| e.get("runId"))
1122 .and_then(Value::as_str)
1123 .map(str::to_string)
1124}
1125
1126fn wire_execution_info(exec: &Execution) -> Value {
1127 let mut obj = Map::new();
1128 obj.insert(
1129 "execution".into(),
1130 json!({ "workflowId": exec.workflow_id, "runId": exec.run_id }),
1131 );
1132 obj.insert(
1133 "workflowType".into(),
1134 json!({ "name": exec.workflow_type_name, "version": exec.workflow_type_version }),
1135 );
1136 obj.insert("startTimestamp".into(), json!(exec.start_timestamp));
1137 obj.insert("executionStatus".into(), json!(exec.status));
1138 obj.insert("cancelRequested".into(), json!(exec.cancel_requested));
1139 if let Some(ct) = exec.close_timestamp {
1140 obj.insert("closeTimestamp".into(), json!(ct));
1141 }
1142 if let Some(cs) = &exec.close_status {
1143 obj.insert("closeStatus".into(), json!(cs));
1144 }
1145 if !exec.tag_list.is_empty() {
1146 obj.insert("tagList".into(), json!(exec.tag_list));
1147 }
1148 Value::Object(obj)
1149}
1150
1151fn wire_execution_config(exec: &Execution) -> Value {
1152 let mut obj = Map::new();
1153 obj.insert(
1154 "taskStartToCloseTimeout".into(),
1155 json!(exec.task_start_to_close_timeout),
1156 );
1157 obj.insert(
1158 "executionStartToCloseTimeout".into(),
1159 json!(exec.execution_start_to_close_timeout),
1160 );
1161 obj.insert("taskList".into(), json!({ "name": exec.task_list }));
1162 obj.insert("childPolicy".into(), json!(exec.child_policy));
1163 if let Some(tp) = &exec.task_priority {
1164 obj.insert("taskPriority".into(), json!(tp));
1165 }
1166 if let Some(lr) = &exec.lambda_role {
1167 obj.insert("lambdaRole".into(), json!(lr));
1168 }
1169 Value::Object(obj)
1170}
1171
1172fn wire_open_counts(exec: &Execution) -> Value {
1173 let open_activities = exec
1174 .activities
1175 .iter()
1176 .filter(|a| a.state != ActivityState::Closed)
1177 .count();
1178 let open_decisions = i64::from(exec.decision_scheduled || exec.decision_task_token.is_some());
1179 json!({
1180 "openActivityTasks": open_activities,
1181 "openDecisionTasks": open_decisions,
1182 "openTimers": 0,
1183 "openChildWorkflowExecutions": 0,
1184 "openLambdaFunctions": 0,
1185 })
1186}
1187
1188impl SwfService {
1191 fn poll_for_decision_task(
1192 &self,
1193 req: &AwsRequest,
1194 body: &Value,
1195 ) -> Result<AwsResponse, AwsServiceError> {
1196 let domain = str_member(body, "domain").unwrap_or_default().to_string();
1197 let task_list = task_list_name(body.get("taskList")).unwrap_or_default();
1198 self.with_account_mut(req, |data| {
1199 if !data.domains.contains_key(&domain) {
1200 return Err(unknown_resource(format!("Unknown domain: {domain}")));
1201 }
1202 let run_id = data
1205 .executions
1206 .values()
1207 .find(|e| {
1208 e.domain == domain
1209 && e.status == "OPEN"
1210 && e.task_list == task_list
1211 && e.decision_scheduled
1212 && e.decision_task_token.is_none()
1213 })
1214 .map(|e| e.run_id.clone());
1215 let Some(run_id) = run_id else {
1216 return ok(json!({ "taskToken": "", "startedEventId": 0, "previousStartedEventId": 0 }));
1218 };
1219 let token = new_task_token();
1220 let exec = data.executions.get_mut(&run_id).expect("run id just found");
1221 let scheduled_id = exec.decision_scheduled_event_id.unwrap_or(0);
1222 let started_id = push_event(
1223 exec,
1224 "DecisionTaskStarted",
1225 "decisionTaskStartedEventAttributes",
1226 json!({ "scheduledEventId": scheduled_id }),
1227 );
1228 exec.decision_scheduled = false;
1229 exec.decision_started_event_id = Some(started_id);
1230 exec.decision_task_token = Some(token.clone());
1231 let previous_started = exec.previous_started_event_id;
1232 let events = exec.events.clone();
1233 let out = json!({
1234 "taskToken": token,
1235 "startedEventId": started_id,
1236 "previousStartedEventId": previous_started,
1237 "workflowExecution": { "workflowId": exec.workflow_id, "runId": exec.run_id },
1238 "workflowType": { "name": exec.workflow_type_name, "version": exec.workflow_type_version },
1239 "events": events,
1240 });
1241 data.decision_tokens.insert(token, run_id);
1242 ok(out)
1243 })
1244 }
1245
1246 fn poll_for_activity_task(
1247 &self,
1248 req: &AwsRequest,
1249 body: &Value,
1250 ) -> Result<AwsResponse, AwsServiceError> {
1251 let domain = str_member(body, "domain").unwrap_or_default().to_string();
1252 let task_list = task_list_name(body.get("taskList")).unwrap_or_default();
1253 self.with_account_mut(req, |data| {
1254 if !data.domains.contains_key(&domain) {
1255 return Err(unknown_resource(format!("Unknown domain: {domain}")));
1256 }
1257 let found = data
1258 .executions
1259 .values()
1260 .filter(|e| e.domain == domain && e.status == "OPEN")
1261 .flat_map(|e| {
1262 e.activities
1263 .iter()
1264 .filter(|a| a.state == ActivityState::Scheduled && a.task_list == task_list)
1265 .map(move |a| (e.run_id.clone(), a.activity_id.clone()))
1266 })
1267 .next();
1268 let Some((run_id, activity_id)) = found else {
1269 return ok(json!({ "taskToken": "" }));
1270 };
1271 let token = new_task_token();
1272 let exec = data.executions.get_mut(&run_id).expect("run id just found");
1273 let workflow_id = exec.workflow_id.clone();
1274 let (scheduled_id, act_name, act_version, input) = {
1275 let a = exec
1276 .activities
1277 .iter()
1278 .find(|a| a.activity_id == activity_id)
1279 .expect("activity just found");
1280 (
1281 a.scheduled_event_id,
1282 a.activity_type_name.clone(),
1283 a.activity_type_version.clone(),
1284 a.input.clone(),
1285 )
1286 };
1287 let started_id = push_event(
1288 exec,
1289 "ActivityTaskStarted",
1290 "activityTaskStartedEventAttributes",
1291 json!({ "scheduledEventId": scheduled_id }),
1292 );
1293 if let Some(a) = exec
1294 .activities
1295 .iter_mut()
1296 .find(|a| a.activity_id == activity_id)
1297 {
1298 a.state = ActivityState::Started;
1299 a.started_event_id = Some(started_id);
1300 a.task_token = token.clone();
1301 }
1302 let mut out = Map::new();
1303 out.insert("taskToken".into(), json!(token));
1304 out.insert("activityId".into(), json!(activity_id));
1305 out.insert("startedEventId".into(), json!(started_id));
1306 out.insert(
1307 "workflowExecution".into(),
1308 json!({ "workflowId": workflow_id, "runId": run_id }),
1309 );
1310 out.insert(
1311 "activityType".into(),
1312 json!({ "name": act_name, "version": act_version }),
1313 );
1314 if let Some(i) = input {
1315 out.insert("input".into(), json!(i));
1316 }
1317 data.activity_tokens.insert(
1318 token,
1319 ActivityRef {
1320 run_id,
1321 activity_id,
1322 },
1323 );
1324 ok(Value::Object(out))
1325 })
1326 }
1327
1328 fn respond_decision_task_completed(
1329 &self,
1330 req: &AwsRequest,
1331 body: &Value,
1332 ) -> Result<AwsResponse, AwsServiceError> {
1333 let token = str_member(body, "taskToken")
1334 .unwrap_or_default()
1335 .to_string();
1336 let execution_context = str_member(body, "executionContext").map(str::to_string);
1337 let decisions = body
1338 .get("decisions")
1339 .and_then(Value::as_array)
1340 .cloned()
1341 .unwrap_or_default();
1342 self.with_account_mut(req, |data| {
1343 let Some(run_id) = data.decision_tokens.remove(&token) else {
1344 return Err(unknown_resource("Unknown or expired decision task token"));
1345 };
1346 let exec = data
1347 .executions
1348 .get_mut(&run_id)
1349 .ok_or_else(|| unknown_resource("Unknown execution for decision token"))?;
1350 let scheduled_id = exec.decision_scheduled_event_id.unwrap_or(0);
1351 let started_id = exec.decision_started_event_id.unwrap_or(0);
1352 let mut dtc_attrs = Map::new();
1353 dtc_attrs.insert("scheduledEventId".into(), json!(scheduled_id));
1354 dtc_attrs.insert("startedEventId".into(), json!(started_id));
1355 if let Some(ctx) = &execution_context {
1356 dtc_attrs.insert("executionContext".into(), json!(ctx));
1357 }
1358 let dtc_id = push_event(
1359 exec,
1360 "DecisionTaskCompleted",
1361 "decisionTaskCompletedEventAttributes",
1362 Value::Object(dtc_attrs),
1363 );
1364 exec.previous_started_event_id = started_id;
1365 exec.decision_task_token = None;
1366 exec.decision_started_event_id = None;
1367 exec.decision_scheduled_event_id = None;
1368 if let Some(ctx) = execution_context {
1369 exec.latest_execution_context = Some(ctx);
1370 }
1371 for decision in &decisions {
1372 if exec.status != "OPEN" {
1373 break;
1374 }
1375 apply_decision(exec, decision, dtc_id);
1376 }
1377 empty_ok()
1378 })
1379 }
1380
1381 fn respond_activity_task(
1382 &self,
1383 req: &AwsRequest,
1384 body: &Value,
1385 outcome: ActivityOutcome,
1386 ) -> Result<AwsResponse, AwsServiceError> {
1387 let token = str_member(body, "taskToken")
1388 .unwrap_or_default()
1389 .to_string();
1390 let result = str_member(body, "result").map(str::to_string);
1391 let reason = str_member(body, "reason").map(str::to_string);
1392 let details = str_member(body, "details").map(str::to_string);
1393 self.with_account_mut(req, |data| {
1394 let Some(act_ref) = data.activity_tokens.remove(&token) else {
1395 return Err(unknown_resource("Unknown or expired activity task token"));
1396 };
1397 let exec = data
1398 .executions
1399 .get_mut(&act_ref.run_id)
1400 .ok_or_else(|| unknown_resource("Unknown execution for activity token"))?;
1401 let (scheduled_id, started_id) = {
1402 let a = exec
1403 .activities
1404 .iter_mut()
1405 .find(|a| a.activity_id == act_ref.activity_id)
1406 .ok_or_else(|| unknown_resource("Unknown activity for token"))?;
1407 a.state = ActivityState::Closed;
1408 (a.scheduled_event_id, a.started_event_id.unwrap_or(0))
1409 };
1410 let (event_type, attrs_key, mut attrs) = match outcome {
1411 ActivityOutcome::Completed => {
1412 let mut m = Map::new();
1413 if let Some(r) = &result {
1414 m.insert("result".into(), json!(r));
1415 }
1416 (
1417 "ActivityTaskCompleted",
1418 "activityTaskCompletedEventAttributes",
1419 m,
1420 )
1421 }
1422 ActivityOutcome::Failed => {
1423 let mut m = Map::new();
1424 if let Some(r) = &reason {
1425 m.insert("reason".into(), json!(r));
1426 }
1427 if let Some(d) = &details {
1428 m.insert("details".into(), json!(d));
1429 }
1430 ("ActivityTaskFailed", "activityTaskFailedEventAttributes", m)
1431 }
1432 ActivityOutcome::Canceled => {
1433 let mut m = Map::new();
1434 if let Some(d) = &details {
1435 m.insert("details".into(), json!(d));
1436 }
1437 (
1438 "ActivityTaskCanceled",
1439 "activityTaskCanceledEventAttributes",
1440 m,
1441 )
1442 }
1443 };
1444 attrs.insert("scheduledEventId".into(), json!(scheduled_id));
1445 attrs.insert("startedEventId".into(), json!(started_id));
1446 push_event(exec, event_type, attrs_key, Value::Object(attrs));
1447 schedule_decision_task(exec);
1450 empty_ok()
1451 })
1452 }
1453
1454 fn record_activity_task_heartbeat(
1455 &self,
1456 req: &AwsRequest,
1457 body: &Value,
1458 ) -> Result<AwsResponse, AwsServiceError> {
1459 let token = str_member(body, "taskToken")
1460 .unwrap_or_default()
1461 .to_string();
1462 self.with_account_mut(req, |data| {
1463 let Some(act_ref) = data.activity_tokens.get(&token).cloned() else {
1464 return Err(unknown_resource("Unknown or expired activity task token"));
1465 };
1466 let cancel_requested = data
1467 .executions
1468 .get(&act_ref.run_id)
1469 .map(|e| e.cancel_requested)
1470 .unwrap_or(false);
1471 ok(json!({ "cancelRequested": cancel_requested }))
1472 })
1473 }
1474}
1475
1476fn apply_decision(exec: &mut Execution, decision: &Value, dtc_id: i64) {
1480 let decision_type = decision
1481 .get("decisionType")
1482 .and_then(Value::as_str)
1483 .unwrap_or_default();
1484 match decision_type {
1485 "ScheduleActivityTask" => {
1486 let attrs = decision
1487 .get("scheduleActivityTaskDecisionAttributes")
1488 .cloned()
1489 .unwrap_or(Value::Null);
1490 let activity_id = attrs
1491 .get("activityId")
1492 .and_then(Value::as_str)
1493 .unwrap_or_default()
1494 .to_string();
1495 let (act_name, act_version) =
1496 type_name_version(attrs.get("activityType")).unwrap_or_default();
1497 let input = attrs
1498 .get("input")
1499 .and_then(Value::as_str)
1500 .map(str::to_string);
1501 let task_list =
1502 task_list_name(attrs.get("taskList")).unwrap_or_else(|| exec.task_list.clone());
1503 let mut ev_attrs = Map::new();
1504 ev_attrs.insert("activityId".into(), json!(activity_id));
1505 ev_attrs.insert(
1506 "activityType".into(),
1507 json!({ "name": act_name, "version": act_version }),
1508 );
1509 ev_attrs.insert("taskList".into(), json!({ "name": task_list }));
1510 ev_attrs.insert("decisionTaskCompletedEventId".into(), json!(dtc_id));
1511 if let Some(i) = &input {
1512 ev_attrs.insert("input".into(), json!(i));
1513 }
1514 let scheduled_id = push_event(
1515 exec,
1516 "ActivityTaskScheduled",
1517 "activityTaskScheduledEventAttributes",
1518 Value::Object(ev_attrs),
1519 );
1520 exec.activities.push(ActivityRecord {
1521 activity_id,
1522 activity_type_name: act_name,
1523 activity_type_version: act_version,
1524 input,
1525 task_list,
1526 scheduled_event_id: scheduled_id,
1527 started_event_id: None,
1528 state: ActivityState::Scheduled,
1529 task_token: String::new(),
1530 });
1531 }
1532 "CompleteWorkflowExecution" => {
1533 let attrs = decision
1534 .get("completeWorkflowExecutionDecisionAttributes")
1535 .cloned()
1536 .unwrap_or(Value::Null);
1537 let mut ev = Map::new();
1538 ev.insert("decisionTaskCompletedEventId".into(), json!(dtc_id));
1539 if let Some(r) = attrs.get("result").and_then(Value::as_str) {
1540 ev.insert("result".into(), json!(r));
1541 }
1542 push_event(
1543 exec,
1544 "WorkflowExecutionCompleted",
1545 "workflowExecutionCompletedEventAttributes",
1546 Value::Object(ev),
1547 );
1548 close_execution(exec, "COMPLETED");
1549 }
1550 "FailWorkflowExecution" => {
1551 let attrs = decision
1552 .get("failWorkflowExecutionDecisionAttributes")
1553 .cloned()
1554 .unwrap_or(Value::Null);
1555 let mut ev = Map::new();
1556 ev.insert("decisionTaskCompletedEventId".into(), json!(dtc_id));
1557 if let Some(r) = attrs.get("reason").and_then(Value::as_str) {
1558 ev.insert("reason".into(), json!(r));
1559 }
1560 if let Some(d) = attrs.get("details").and_then(Value::as_str) {
1561 ev.insert("details".into(), json!(d));
1562 }
1563 push_event(
1564 exec,
1565 "WorkflowExecutionFailed",
1566 "workflowExecutionFailedEventAttributes",
1567 Value::Object(ev),
1568 );
1569 close_execution(exec, "FAILED");
1570 }
1571 "CancelWorkflowExecution" => {
1572 let attrs = decision
1573 .get("cancelWorkflowExecutionDecisionAttributes")
1574 .cloned()
1575 .unwrap_or(Value::Null);
1576 let mut ev = Map::new();
1577 ev.insert("decisionTaskCompletedEventId".into(), json!(dtc_id));
1578 if let Some(d) = attrs.get("details").and_then(Value::as_str) {
1579 ev.insert("details".into(), json!(d));
1580 }
1581 push_event(
1582 exec,
1583 "WorkflowExecutionCanceled",
1584 "workflowExecutionCanceledEventAttributes",
1585 Value::Object(ev),
1586 );
1587 close_execution(exec, "CANCELED");
1588 }
1589 "ContinueAsNewWorkflowExecution" => {
1590 push_event(
1591 exec,
1592 "WorkflowExecutionContinuedAsNew",
1593 "workflowExecutionContinuedAsNewEventAttributes",
1594 json!({ "decisionTaskCompletedEventId": dtc_id, "newExecutionRunId": new_run_id() }),
1595 );
1596 close_execution(exec, "CONTINUED_AS_NEW");
1597 }
1598 "RecordMarker" => {
1599 let attrs = decision
1600 .get("recordMarkerDecisionAttributes")
1601 .cloned()
1602 .unwrap_or(Value::Null);
1603 let marker_name = attrs
1604 .get("markerName")
1605 .and_then(Value::as_str)
1606 .unwrap_or_default();
1607 let mut ev = Map::new();
1608 ev.insert("decisionTaskCompletedEventId".into(), json!(dtc_id));
1609 ev.insert("markerName".into(), json!(marker_name));
1610 if let Some(d) = attrs.get("details").and_then(Value::as_str) {
1611 ev.insert("details".into(), json!(d));
1612 }
1613 push_event(
1614 exec,
1615 "MarkerRecorded",
1616 "markerRecordedEventAttributes",
1617 Value::Object(ev),
1618 );
1619 }
1620 "StartTimer" => {
1621 let attrs = decision
1622 .get("startTimerDecisionAttributes")
1623 .cloned()
1624 .unwrap_or(Value::Null);
1625 let timer_id = attrs
1626 .get("timerId")
1627 .and_then(Value::as_str)
1628 .unwrap_or_default();
1629 let timeout = attrs
1630 .get("startToFireTimeout")
1631 .and_then(Value::as_str)
1632 .unwrap_or("0");
1633 push_event(
1634 exec,
1635 "TimerStarted",
1636 "timerStartedEventAttributes",
1637 json!({
1638 "timerId": timer_id,
1639 "startToFireTimeout": timeout,
1640 "decisionTaskCompletedEventId": dtc_id,
1641 }),
1642 );
1643 }
1644 "CancelTimer" => {
1645 let attrs = decision
1646 .get("cancelTimerDecisionAttributes")
1647 .cloned()
1648 .unwrap_or(Value::Null);
1649 let timer_id = attrs
1650 .get("timerId")
1651 .and_then(Value::as_str)
1652 .unwrap_or_default();
1653 push_event(
1654 exec,
1655 "TimerCanceled",
1656 "timerCanceledEventAttributes",
1657 json!({
1658 "timerId": timer_id,
1659 "startedEventId": 0,
1660 "decisionTaskCompletedEventId": dtc_id,
1661 }),
1662 );
1663 }
1664 "RequestCancelActivityTask" => {
1665 let attrs = decision
1666 .get("requestCancelActivityTaskDecisionAttributes")
1667 .cloned()
1668 .unwrap_or(Value::Null);
1669 let activity_id = attrs
1670 .get("activityId")
1671 .and_then(Value::as_str)
1672 .unwrap_or_default();
1673 push_event(
1674 exec,
1675 "ActivityTaskCancelRequested",
1676 "activityTaskCancelRequestedEventAttributes",
1677 json!({
1678 "activityId": activity_id,
1679 "decisionTaskCompletedEventId": dtc_id,
1680 }),
1681 );
1682 }
1683 "SignalExternalWorkflowExecution" => {
1684 push_event(
1685 exec,
1686 "SignalExternalWorkflowExecutionInitiated",
1687 "signalExternalWorkflowExecutionInitiatedEventAttributes",
1688 signal_external_attrs(decision, dtc_id),
1689 );
1690 }
1691 "RequestCancelExternalWorkflowExecution" => {
1692 let attrs = decision
1693 .get("requestCancelExternalWorkflowExecutionDecisionAttributes")
1694 .cloned()
1695 .unwrap_or(Value::Null);
1696 let workflow_id = attrs
1697 .get("workflowId")
1698 .and_then(Value::as_str)
1699 .unwrap_or_default();
1700 push_event(
1701 exec,
1702 "RequestCancelExternalWorkflowExecutionInitiated",
1703 "requestCancelExternalWorkflowExecutionInitiatedEventAttributes",
1704 json!({
1705 "workflowId": workflow_id,
1706 "decisionTaskCompletedEventId": dtc_id,
1707 }),
1708 );
1709 }
1710 "StartChildWorkflowExecution" => {
1711 let attrs = decision
1712 .get("startChildWorkflowExecutionDecisionAttributes")
1713 .cloned()
1714 .unwrap_or(Value::Null);
1715 let (name, version) = type_name_version(attrs.get("workflowType")).unwrap_or_default();
1716 let workflow_id = attrs
1717 .get("workflowId")
1718 .and_then(Value::as_str)
1719 .unwrap_or_default();
1720 push_event(
1721 exec,
1722 "StartChildWorkflowExecutionInitiated",
1723 "startChildWorkflowExecutionInitiatedEventAttributes",
1724 json!({
1725 "workflowId": workflow_id,
1726 "workflowType": { "name": name, "version": version },
1727 "taskList": { "name": exec.task_list },
1728 "childPolicy": exec.child_policy,
1729 "decisionTaskCompletedEventId": dtc_id,
1730 }),
1731 );
1732 }
1733 "ScheduleLambdaFunction" => {
1734 let attrs = decision
1735 .get("scheduleLambdaFunctionDecisionAttributes")
1736 .cloned()
1737 .unwrap_or(Value::Null);
1738 let id = attrs.get("id").and_then(Value::as_str).unwrap_or_default();
1739 let name = attrs
1740 .get("name")
1741 .and_then(Value::as_str)
1742 .unwrap_or_default();
1743 push_event(
1744 exec,
1745 "LambdaFunctionScheduled",
1746 "lambdaFunctionScheduledEventAttributes",
1747 json!({
1748 "id": id,
1749 "name": name,
1750 "decisionTaskCompletedEventId": dtc_id,
1751 }),
1752 );
1753 }
1754 _ => {}
1755 }
1756}
1757
1758fn signal_external_attrs(decision: &Value, dtc_id: i64) -> Value {
1759 let attrs = decision
1760 .get("signalExternalWorkflowExecutionDecisionAttributes")
1761 .cloned()
1762 .unwrap_or(Value::Null);
1763 let workflow_id = attrs
1764 .get("workflowId")
1765 .and_then(Value::as_str)
1766 .unwrap_or_default();
1767 let signal_name = attrs
1768 .get("signalName")
1769 .and_then(Value::as_str)
1770 .unwrap_or_default();
1771 json!({
1772 "workflowId": workflow_id,
1773 "signalName": signal_name,
1774 "decisionTaskCompletedEventId": dtc_id,
1775 })
1776}
1777
1778fn push_event(exec: &mut Execution, event_type: &str, attrs_key: &str, attrs: Value) -> i64 {
1780 let id = exec.alloc_event_id();
1781 let mut ev = Map::new();
1782 ev.insert("eventTimestamp".into(), json!(now_epoch()));
1783 ev.insert("eventType".into(), json!(event_type));
1784 ev.insert("eventId".into(), json!(id));
1785 if !attrs.is_null() {
1786 ev.insert(attrs_key.into(), attrs);
1787 }
1788 exec.events.push(Value::Object(ev));
1789 id
1790}
1791
1792fn schedule_decision_task(exec: &mut Execution) {
1795 if exec.status != "OPEN" {
1796 return;
1797 }
1798 if exec.decision_scheduled || exec.decision_task_token.is_some() {
1799 return;
1800 }
1801 let id = push_event(
1802 exec,
1803 "DecisionTaskScheduled",
1804 "decisionTaskScheduledEventAttributes",
1805 json!({
1806 "taskList": { "name": exec.task_list },
1807 "startToCloseTimeout": exec.task_start_to_close_timeout,
1808 }),
1809 );
1810 exec.decision_scheduled = true;
1811 exec.decision_scheduled_event_id = Some(id);
1812}
1813
1814fn close_execution(exec: &mut Execution, close_status: &str) {
1815 exec.status = "CLOSED".to_string();
1816 exec.close_status = Some(close_status.to_string());
1817 exec.close_timestamp = Some(now_epoch());
1818 exec.decision_scheduled = false;
1819 exec.decision_scheduled_event_id = None;
1820}
1821
1822impl SwfService {
1825 fn tag_resource(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
1826 let arn = str_member(body, "resourceArn")
1827 .unwrap_or_default()
1828 .to_string();
1829 let tags = body.get("tags").cloned().unwrap_or(Value::Null);
1830 self.with_account_mut(req, |data| {
1831 if !domain_exists_by_arn(data, &arn) {
1832 return Err(unknown_resource(format!("Unknown resource: {arn}")));
1833 }
1834 let map = data.tags.entry(arn).or_default();
1835 for (k, v) in resource_tags_to_map(&tags) {
1836 map.insert(k, v);
1837 }
1838 empty_ok()
1839 })
1840 }
1841
1842 fn untag_resource(
1843 &self,
1844 req: &AwsRequest,
1845 body: &Value,
1846 ) -> Result<AwsResponse, AwsServiceError> {
1847 let arn = str_member(body, "resourceArn")
1848 .unwrap_or_default()
1849 .to_string();
1850 let keys: Vec<String> = body
1851 .get("tagKeys")
1852 .and_then(Value::as_array)
1853 .map(|a| {
1854 a.iter()
1855 .filter_map(|v| v.as_str().map(str::to_string))
1856 .collect()
1857 })
1858 .unwrap_or_default();
1859 self.with_account_mut(req, |data| {
1860 if !domain_exists_by_arn(data, &arn) {
1861 return Err(unknown_resource(format!("Unknown resource: {arn}")));
1862 }
1863 if let Some(map) = data.tags.get_mut(&arn) {
1864 for k in &keys {
1865 map.remove(k);
1866 }
1867 }
1868 empty_ok()
1869 })
1870 }
1871
1872 fn list_tags_for_resource(
1873 &self,
1874 req: &AwsRequest,
1875 body: &Value,
1876 ) -> Result<AwsResponse, AwsServiceError> {
1877 let arn = str_member(body, "resourceArn")
1878 .unwrap_or_default()
1879 .to_string();
1880 self.with_account_mut(req, |data| {
1881 if !domain_exists_by_arn(data, &arn) {
1882 return Err(unknown_resource(format!("Unknown resource: {arn}")));
1883 }
1884 let tags: Vec<Value> = data
1885 .tags
1886 .get(&arn)
1887 .map(|m| {
1888 m.iter()
1889 .map(|(k, v)| json!({ "key": k, "value": v }))
1890 .collect()
1891 })
1892 .unwrap_or_default();
1893 ok(json!({ "tags": tags }))
1894 })
1895 }
1896}
1897
1898fn domain_exists_by_arn(data: &SwfData, arn: &str) -> bool {
1900 data.domains.values().any(|d| d.arn == arn)
1901}
1902
1903fn resource_tags_to_map(tags: &Value) -> BTreeMapStr {
1905 let mut out = std::collections::BTreeMap::new();
1906 if let Some(arr) = tags.as_array() {
1907 for t in arr {
1908 if let Some(k) = t.get("key").and_then(Value::as_str) {
1909 let v = t.get("value").and_then(Value::as_str).unwrap_or("");
1910 out.insert(k.to_string(), v.to_string());
1911 }
1912 }
1913 }
1914 out
1915}
1916
1917type BTreeMapStr = std::collections::BTreeMap<String, String>;
1918
1919#[cfg(test)]
1920mod tests {
1921 use super::*;
1922 use fakecloud_core::multi_account::MultiAccountState;
1923 use parking_lot::RwLock;
1924
1925 fn service() -> SwfService {
1926 SwfService::new(Arc::new(RwLock::new(MultiAccountState::new(
1927 "000000000000",
1928 "us-east-1",
1929 "",
1930 ))))
1931 }
1932
1933 fn req(action: &str, body: Value) -> AwsRequest {
1934 AwsRequest {
1935 service: "swf".to_string(),
1936 action: action.to_string(),
1937 region: "us-east-1".to_string(),
1938 account_id: "000000000000".to_string(),
1939 request_id: "test".to_string(),
1940 headers: http::HeaderMap::new(),
1941 query_params: std::collections::HashMap::new(),
1942 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1943 body_stream: parking_lot::Mutex::new(None),
1944 path_segments: Vec::new(),
1945 raw_path: String::new(),
1946 raw_query: String::new(),
1947 method: http::Method::POST,
1948 is_query_protocol: false,
1949 access_key_id: None,
1950 principal: None,
1951 }
1952 }
1953
1954 fn call(svc: &SwfService, action: &str, body: Value) -> Result<Value, String> {
1955 let r = req(action, body);
1956 crate::validate::validate_input(action, &r.json_body())?;
1957 match svc.dispatch(action, &r) {
1958 Ok(resp) => Ok(serde_json::from_slice(resp.body.expect_bytes()).unwrap_or(Value::Null)),
1959 Err(AwsServiceError::AwsError { code, .. }) => Err(code),
1960 Err(e) => Err(format!("{e:?}")),
1961 }
1962 }
1963
1964 #[test]
1965 fn register_and_describe_domain() {
1966 let svc = service();
1967 call(
1968 &svc,
1969 "RegisterDomain",
1970 json!({ "name": "d1", "workflowExecutionRetentionPeriodInDays": "30" }),
1971 )
1972 .unwrap();
1973 let out = call(&svc, "DescribeDomain", json!({ "name": "d1" })).unwrap();
1974 assert_eq!(out["domainInfo"]["status"], "REGISTERED");
1975 assert_eq!(
1976 out["configuration"]["workflowExecutionRetentionPeriodInDays"],
1977 "30"
1978 );
1979 }
1980
1981 #[test]
1982 fn duplicate_domain_rejected() {
1983 let svc = service();
1984 let body = json!({ "name": "d1", "workflowExecutionRetentionPeriodInDays": "1" });
1985 call(&svc, "RegisterDomain", body.clone()).unwrap();
1986 let err = call(&svc, "RegisterDomain", body).unwrap_err();
1987 assert_eq!(err, "DomainAlreadyExistsFault");
1988 }
1989
1990 #[test]
1991 fn describe_unknown_domain_faults() {
1992 let svc = service();
1993 let err = call(&svc, "DescribeDomain", json!({ "name": "nope" })).unwrap_err();
1994 assert_eq!(err, "UnknownResourceFault");
1995 }
1996
1997 #[test]
1998 fn register_type_requires_domain() {
1999 let svc = service();
2000 let err = call(
2001 &svc,
2002 "RegisterWorkflowType",
2003 json!({ "domain": "missing", "name": "wf", "version": "1" }),
2004 )
2005 .unwrap_err();
2006 assert_eq!(err, "UnknownResourceFault");
2007 }
2008
2009 #[test]
2010 fn start_workflow_needs_defaults() {
2011 let svc = service();
2012 call(
2013 &svc,
2014 "RegisterDomain",
2015 json!({ "name": "d", "workflowExecutionRetentionPeriodInDays": "1" }),
2016 )
2017 .unwrap();
2018 call(
2019 &svc,
2020 "RegisterWorkflowType",
2021 json!({ "domain": "d", "name": "wf", "version": "1" }),
2022 )
2023 .unwrap();
2024 let err = call(
2026 &svc,
2027 "StartWorkflowExecution",
2028 json!({ "domain": "d", "workflowId": "w1", "workflowType": { "name": "wf", "version": "1" } }),
2029 )
2030 .unwrap_err();
2031 assert_eq!(err, "DefaultUndefinedFault");
2032 }
2033
2034 #[test]
2035 fn full_decider_worker_loop() {
2036 let svc = service();
2037 call(
2038 &svc,
2039 "RegisterDomain",
2040 json!({ "name": "d", "workflowExecutionRetentionPeriodInDays": "1" }),
2041 )
2042 .unwrap();
2043 call(
2044 &svc,
2045 "RegisterWorkflowType",
2046 json!({
2047 "domain": "d", "name": "wf", "version": "1",
2048 "defaultTaskList": { "name": "dtl" },
2049 "defaultChildPolicy": "TERMINATE",
2050 "defaultExecutionStartToCloseTimeout": "3600",
2051 "defaultTaskStartToCloseTimeout": "60"
2052 }),
2053 )
2054 .unwrap();
2055 call(
2056 &svc,
2057 "RegisterActivityType",
2058 json!({ "domain": "d", "name": "act", "version": "1" }),
2059 )
2060 .unwrap();
2061 let start = call(
2062 &svc,
2063 "StartWorkflowExecution",
2064 json!({ "domain": "d", "workflowId": "w1", "workflowType": { "name": "wf", "version": "1" }, "input": "hello" }),
2065 )
2066 .unwrap();
2067 let run_id = start["runId"].as_str().unwrap().to_string();
2068
2069 let dt = call(
2071 &svc,
2072 "PollForDecisionTask",
2073 json!({ "domain": "d", "taskList": { "name": "dtl" } }),
2074 )
2075 .unwrap();
2076 let dtoken = dt["taskToken"].as_str().unwrap().to_string();
2077 assert!(!dtoken.is_empty());
2078 let types: Vec<&str> = dt["events"]
2079 .as_array()
2080 .unwrap()
2081 .iter()
2082 .map(|e| e["eventType"].as_str().unwrap())
2083 .collect();
2084 assert!(types.contains(&"WorkflowExecutionStarted"));
2085 assert!(types.contains(&"DecisionTaskStarted"));
2086
2087 call(
2089 &svc,
2090 "RespondDecisionTaskCompleted",
2091 json!({
2092 "taskToken": dtoken,
2093 "decisions": [{
2094 "decisionType": "ScheduleActivityTask",
2095 "scheduleActivityTaskDecisionAttributes": {
2096 "activityId": "a1",
2097 "activityType": { "name": "act", "version": "1" },
2098 "input": "hello",
2099 "taskList": { "name": "atl" }
2100 }
2101 }]
2102 }),
2103 )
2104 .unwrap();
2105
2106 let at = call(
2108 &svc,
2109 "PollForActivityTask",
2110 json!({ "domain": "d", "taskList": { "name": "atl" } }),
2111 )
2112 .unwrap();
2113 let atoken = at["taskToken"].as_str().unwrap().to_string();
2114 assert_eq!(at["activityId"], "a1");
2115 assert_eq!(at["input"], "hello");
2116 call(
2117 &svc,
2118 "RespondActivityTaskCompleted",
2119 json!({ "taskToken": atoken, "result": "42" }),
2120 )
2121 .unwrap();
2122
2123 let dt2 = call(
2125 &svc,
2126 "PollForDecisionTask",
2127 json!({ "domain": "d", "taskList": { "name": "dtl" } }),
2128 )
2129 .unwrap();
2130 let dtoken2 = dt2["taskToken"].as_str().unwrap().to_string();
2131 let types2: Vec<&str> = dt2["events"]
2132 .as_array()
2133 .unwrap()
2134 .iter()
2135 .map(|e| e["eventType"].as_str().unwrap())
2136 .collect();
2137 assert!(types2.contains(&"ActivityTaskCompleted"));
2138 call(
2139 &svc,
2140 "RespondDecisionTaskCompleted",
2141 json!({
2142 "taskToken": dtoken2,
2143 "decisions": [{
2144 "decisionType": "CompleteWorkflowExecution",
2145 "completeWorkflowExecutionDecisionAttributes": { "result": "42" }
2146 }]
2147 }),
2148 )
2149 .unwrap();
2150
2151 let desc = call(
2153 &svc,
2154 "DescribeWorkflowExecution",
2155 json!({ "domain": "d", "execution": { "workflowId": "w1", "runId": run_id } }),
2156 )
2157 .unwrap();
2158 assert_eq!(desc["executionInfo"]["executionStatus"], "CLOSED");
2159 assert_eq!(desc["executionInfo"]["closeStatus"], "COMPLETED");
2160
2161 let closed = call(
2162 &svc,
2163 "CountClosedWorkflowExecutions",
2164 json!({ "domain": "d" }),
2165 )
2166 .unwrap();
2167 assert_eq!(closed["count"], 1);
2168 }
2169}