Skip to main content

fakecloud_swf/
service.rs

1//! Amazon SWF awsJson1_0 dispatch + operation handlers.
2//!
3//! Requests carry `X-Amz-Target: SimpleWorkflowService.<Operation>`; dispatch
4//! keys off `req.action`. Every operation runs model-driven input validation
5//! first (required / length / range / enum), then real, account-partitioned,
6//! persisted behavior: domains, versioned activity/workflow types, and workflow
7//! executions with a working decider/worker state machine (poll a decision task,
8//! respond with decisions that append the right history events, poll the
9//! scheduled activity task, respond, observe the result in the next decision
10//! task, close the execution).
11//!
12//! Dereferencing a domain / type / execution / task token that does not exist
13//! returns SWF's canonical `UnknownResourceFault`; a duplicate domain or type
14//! returns `DomainAlreadyExistsFault` / `TypeAlreadyExistsFault`; missing
15//! start-time defaults return `DefaultUndefinedFault`; a second open execution
16//! of the same workflow id returns `WorkflowExecutionAlreadyStartedFault`.
17
18use 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
34/// Every operation name in the Amazon SWF Smithy model (39 operations).
35pub 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
77/// Read-only verbs; any other action mutates persisted state and triggers a
78/// snapshot after success. Poll/Respond/Record mutate history, so only the
79/// pure getters are read-only.
80fn 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    /// Persist hook for the CloudFormation provisioner; `None` in memory mode.
115    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            // Domains
166            "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            // Activity types
172            "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            // Workflow types
179            "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            // Executions
186            "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            // Decider / worker loop
199            "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            // Tagging
213            "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
237// ---- error / response helpers --------------------------------------------
238
239fn 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
259// ---- small member helpers -------------------------------------------------
260
261fn str_member<'a>(body: &'a Value, name: &str) -> Option<&'a str> {
262    body.get(name).and_then(Value::as_str)
263}
264
265/// Copy each named optional member from `body` into `obj` when present and
266/// non-null.
267fn 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
275/// Read a `TaskList` member (`{ "name": ... }`) as its name string.
276fn 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
282/// Extract `(name, version)` from an `ActivityType` / `WorkflowType` member.
283fn 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
290// ---- domains --------------------------------------------------------------
291
292impl 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(&region, &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
431// ---- activity / workflow types -------------------------------------------
432
433impl 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
695// ---- workflow executions --------------------------------------------------
696
697impl 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            // Resolve each start-time default from the request, then the type's
741            // registered default; a value defined by neither is a
742            // DefaultUndefinedFault (exactly as AWS reports it).
743            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            // Seed the history: WorkflowExecutionStarted, then the first
814            // DecisionTaskScheduled so a decider's first poll has work.
815            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
1188// ---- decider / worker loop ------------------------------------------------
1189
1190impl 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            // Find the next execution in this task list with a scheduled,
1203            // not-yet-started decision task.
1204            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                // Long-poll timeout: an empty task carries no taskToken.
1217                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            // The activity result becomes decider-visible: schedule the next
1448            // decision task so the next PollForDecisionTask sees it.
1449            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
1476/// Apply a single decision from `RespondDecisionTaskCompleted`, appending the
1477/// history events it produces. `dtc_id` is the `DecisionTaskCompleted` event id
1478/// the produced events reference.
1479fn 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
1778/// Append a `HistoryEvent` and return its assigned `eventId`.
1779fn 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
1792/// Schedule a fresh decision task on an open execution when none is pending or
1793/// in flight, appending the `DecisionTaskScheduled` event.
1794fn 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
1822// ---- tagging --------------------------------------------------------------
1823
1824impl 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
1898/// True when `arn` names a registered domain in this account.
1899fn domain_exists_by_arn(data: &SwfData, arn: &str) -> bool {
1900    data.domains.values().any(|d| d.arn == arn)
1901}
1902
1903/// Convert a `ResourceTagList` (`[{key,value}]`) into a sorted key/value map.
1904fn 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        // No taskList / timeouts / childPolicy defaults -> DefaultUndefinedFault.
2025        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        // Decider polls, gets a decision task with the seeded history.
2070        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        // Schedule an activity.
2088        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        // Worker polls, runs, completes.
2107        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        // Next decision task shows ActivityTaskCompleted; decider completes wf.
2124        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        // Execution is closed COMPLETED and shows in the closed list.
2152        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}