Skip to main content

fakecloud_stepfunctions/
state.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use chrono::{DateTime, Utc};
5use parking_lot::RwLock;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9pub type SharedStepFunctionsState =
10    Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<StepFunctionsState>>>;
11
12impl fakecloud_core::multi_account::AccountState for StepFunctionsState {
13    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
14        Self::new(account_id, region)
15    }
16}
17
18pub const STEPFUNCTIONS_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
19
20#[derive(Debug, Serialize, Deserialize)]
21pub struct StepFunctionsSnapshot {
22    pub schema_version: u32,
23    #[serde(default)]
24    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<StepFunctionsState>>,
25    #[serde(default)]
26    pub state: Option<StepFunctionsState>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct StepFunctionsState {
31    pub account_id: String,
32    pub region: String,
33    /// State machines keyed by ARN.
34    #[serde(default)]
35    pub state_machines: BTreeMap<String, StateMachine>,
36    /// Executions keyed by execution ARN.
37    #[serde(default)]
38    pub executions: BTreeMap<String, Execution>,
39    #[serde(default)]
40    pub activities: BTreeMap<String, Activity>,
41    #[serde(default)]
42    pub state_machine_versions: BTreeMap<String, StateMachineVersion>,
43    #[serde(default)]
44    pub state_machine_aliases: BTreeMap<String, StateMachineAlias>,
45    #[serde(default)]
46    pub map_runs: BTreeMap<String, MapRun>,
47    /// Pending task tokens issued for sync activities + their outcome.
48    #[serde(default)]
49    pub task_tokens: BTreeMap<String, TaskTokenState>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Activity {
54    pub name: String,
55    pub arn: String,
56    pub creation_date: DateTime<Utc>,
57    pub tags: BTreeMap<String, String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct StateMachineVersion {
62    pub state_machine_arn: String,
63    pub version: i64,
64    pub revision_id: String,
65    pub description: String,
66    pub creation_date: DateTime<Utc>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct StateMachineAlias {
71    pub name: String,
72    pub arn: String,
73    pub description: String,
74    pub routing_configuration: Vec<AliasRoute>,
75    pub creation_date: DateTime<Utc>,
76    pub update_date: DateTime<Utc>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct AliasRoute {
81    pub state_machine_version_arn: String,
82    pub weight: i32,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct MapRun {
87    pub map_run_arn: String,
88    pub execution_arn: String,
89    pub max_concurrency: i32,
90    pub tolerated_failure_percentage: f64,
91    pub tolerated_failure_count: i64,
92    pub status: String,
93    pub start_date: DateTime<Utc>,
94    pub stop_date: Option<DateTime<Utc>>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct TaskTokenState {
99    pub activity_arn: String,
100    /// PENDING (waiting for `GetActivityTask` to dequeue) /
101    /// IN_PROGRESS (worker has picked it up) /
102    /// SUCCEEDED / FAILED / TIMED_OUT.
103    pub status: String,
104    pub output: Option<String>,
105    pub error: Option<String>,
106    pub cause: Option<String>,
107    /// Input the state machine wanted the worker to process. `None`
108    /// for tokens minted by external `GetActivityTask` callers without
109    /// any associated activity execution (legacy synthetic path).
110    #[serde(default)]
111    pub input: Option<String>,
112    #[serde(default = "default_now")]
113    pub created_at: DateTime<Utc>,
114    #[serde(default)]
115    pub last_heartbeat_at: Option<DateTime<Utc>>,
116    /// Per AWS docs: state machine fails the task if no heartbeat in
117    /// this many seconds while the worker is running.
118    #[serde(default)]
119    pub heartbeat_seconds: Option<i64>,
120    /// Overall timeout for the task; counted from `created_at`.
121    #[serde(default)]
122    pub timeout_seconds: Option<i64>,
123}
124
125fn default_now() -> DateTime<Utc> {
126    Utc::now()
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct StateMachine {
131    pub name: String,
132    pub arn: String,
133    pub definition: String,
134    pub role_arn: String,
135    pub machine_type: StateMachineType,
136    pub status: StateMachineStatus,
137    pub creation_date: DateTime<Utc>,
138    pub update_date: DateTime<Utc>,
139    pub tags: BTreeMap<String, String>,
140    pub revision_id: String,
141    pub logging_configuration: Option<Value>,
142    pub tracing_configuration: Option<Value>,
143    pub description: String,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147pub enum StateMachineType {
148    Standard,
149    Express,
150}
151
152impl StateMachineType {
153    pub fn as_str(&self) -> &'static str {
154        match self {
155            Self::Standard => "STANDARD",
156            Self::Express => "EXPRESS",
157        }
158    }
159
160    pub fn parse(s: &str) -> Option<Self> {
161        match s {
162            "STANDARD" => Some(Self::Standard),
163            "EXPRESS" => Some(Self::Express),
164            _ => None,
165        }
166    }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
170pub enum StateMachineStatus {
171    Active,
172    Deleting,
173}
174
175impl StateMachineStatus {
176    pub fn as_str(&self) -> &'static str {
177        match self {
178            Self::Active => "ACTIVE",
179            Self::Deleting => "DELETING",
180        }
181    }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct Execution {
186    pub execution_arn: String,
187    pub state_machine_arn: String,
188    pub state_machine_name: String,
189    pub name: String,
190    pub status: ExecutionStatus,
191    pub input: Option<String>,
192    pub output: Option<String>,
193    pub start_date: DateTime<Utc>,
194    pub stop_date: Option<DateTime<Utc>>,
195    pub error: Option<String>,
196    pub cause: Option<String>,
197    pub history_events: Vec<HistoryEvent>,
198    /// Parent execution ARN when this execution was started by another
199    /// state machine via `arn:aws:states:::states:startExecution[.sync]`.
200    /// `None` for top-level executions started by external callers.
201    #[serde(default)]
202    pub parent_execution_arn: Option<String>,
203    /// True when this execution was created by `StartSyncExecution`
204    /// (EXPRESS state machines only). Distinguishes it from regular
205    /// async executions in introspection endpoints.
206    #[serde(default)]
207    pub is_sync: bool,
208    /// Billed duration in milliseconds, populated on terminal state for
209    /// sync executions. Mirrors `billingDetails.billedDurationInMilliseconds`
210    /// from the StartSyncExecution response.
211    #[serde(default)]
212    pub billed_duration_ms: Option<i64>,
213    /// Billed memory in MB for sync executions. Mirrors
214    /// `billingDetails.billedMemoryUsedInMB`.
215    #[serde(default)]
216    pub billed_memory_mb: Option<i64>,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220pub enum ExecutionStatus {
221    Running,
222    Succeeded,
223    Failed,
224    TimedOut,
225    Aborted,
226    PendingRedrive,
227}
228
229impl ExecutionStatus {
230    pub fn as_str(&self) -> &'static str {
231        match self {
232            Self::Running => "RUNNING",
233            Self::Succeeded => "SUCCEEDED",
234            Self::Failed => "FAILED",
235            Self::TimedOut => "TIMED_OUT",
236            Self::Aborted => "ABORTED",
237            Self::PendingRedrive => "PENDING_REDRIVE",
238        }
239    }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct HistoryEvent {
244    pub id: i64,
245    pub event_type: String,
246    pub timestamp: DateTime<Utc>,
247    pub previous_event_id: i64,
248    pub details: Value,
249}
250
251impl StepFunctionsState {
252    pub fn new(account_id: &str, region: &str) -> Self {
253        Self {
254            account_id: account_id.to_string(),
255            region: region.to_string(),
256            state_machines: BTreeMap::new(),
257            executions: BTreeMap::new(),
258            activities: BTreeMap::new(),
259            state_machine_versions: BTreeMap::new(),
260            state_machine_aliases: BTreeMap::new(),
261            map_runs: BTreeMap::new(),
262            task_tokens: BTreeMap::new(),
263        }
264    }
265
266    pub fn reset(&mut self) {
267        self.state_machines.clear();
268        self.executions.clear();
269        self.activities.clear();
270        self.state_machine_versions.clear();
271        self.state_machine_aliases.clear();
272        self.map_runs.clear();
273        self.task_tokens.clear();
274    }
275
276    pub fn state_machine_arn(&self, name: &str) -> String {
277        format!(
278            "arn:aws:states:{}:{}:stateMachine:{}",
279            self.region, self.account_id, name
280        )
281    }
282
283    pub fn execution_arn(&self, state_machine_name: &str, execution_name: &str) -> String {
284        format!(
285            "arn:aws:states:{}:{}:execution:{}:{}",
286            self.region, self.account_id, state_machine_name, execution_name
287        )
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn state_machine_type_as_str() {
297        assert_eq!(StateMachineType::Standard.as_str(), "STANDARD");
298        assert_eq!(StateMachineType::Express.as_str(), "EXPRESS");
299    }
300
301    #[test]
302    fn state_machine_type_parse() {
303        assert_eq!(
304            StateMachineType::parse("STANDARD"),
305            Some(StateMachineType::Standard)
306        );
307        assert_eq!(
308            StateMachineType::parse("EXPRESS"),
309            Some(StateMachineType::Express)
310        );
311        assert_eq!(StateMachineType::parse("bogus"), None);
312    }
313
314    #[test]
315    fn state_machine_status_as_str() {
316        assert_eq!(StateMachineStatus::Active.as_str(), "ACTIVE");
317        assert_eq!(StateMachineStatus::Deleting.as_str(), "DELETING");
318    }
319
320    #[test]
321    fn execution_status_as_str() {
322        assert_eq!(ExecutionStatus::Running.as_str(), "RUNNING");
323        assert_eq!(ExecutionStatus::Succeeded.as_str(), "SUCCEEDED");
324        assert_eq!(ExecutionStatus::Failed.as_str(), "FAILED");
325        assert_eq!(ExecutionStatus::TimedOut.as_str(), "TIMED_OUT");
326        assert_eq!(ExecutionStatus::Aborted.as_str(), "ABORTED");
327        assert_eq!(ExecutionStatus::PendingRedrive.as_str(), "PENDING_REDRIVE");
328    }
329
330    #[test]
331    fn state_machine_arn_format() {
332        let state = StepFunctionsState::new("123456789012", "us-east-1");
333        assert_eq!(
334            state.state_machine_arn("my-sm"),
335            "arn:aws:states:us-east-1:123456789012:stateMachine:my-sm"
336        );
337    }
338
339    #[test]
340    fn execution_arn_format() {
341        let state = StepFunctionsState::new("123456789012", "us-east-1");
342        assert_eq!(
343            state.execution_arn("sm", "exec-1"),
344            "arn:aws:states:us-east-1:123456789012:execution:sm:exec-1"
345        );
346    }
347
348    #[test]
349    fn state_reset_clears_all() {
350        let mut state = StepFunctionsState::new("123456789012", "us-east-1");
351        state.state_machines.insert(
352            "x".to_string(),
353            StateMachine {
354                name: "sm".to_string(),
355                arn: "arn:aws:states:us-east-1:123:stateMachine:sm".to_string(),
356                definition: "{}".to_string(),
357                role_arn: "r".to_string(),
358                machine_type: StateMachineType::Standard,
359                status: StateMachineStatus::Active,
360                creation_date: Utc::now(),
361                update_date: Utc::now(),
362                tags: BTreeMap::new(),
363                revision_id: "v1".to_string(),
364                logging_configuration: None,
365                tracing_configuration: None,
366                description: String::new(),
367            },
368        );
369        state.reset();
370        assert!(state.state_machines.is_empty());
371        assert!(state.executions.is_empty());
372    }
373}