Skip to main content

durable_execution_sdk/state/
checkpoint_result.rs

1//! Result type for checkpoint queries.
2//!
3//! This module provides the [`CheckpointedResult`] type for querying the status
4//! of previously checkpointed operations during replay.
5
6use crate::error::ErrorObject;
7use crate::operation::{Operation, OperationStatus, OperationType};
8
9/// Result of checking for a checkpointed operation.
10///
11/// This struct provides methods to query the status of a previously
12/// checkpointed operation during replay.
13#[derive(Debug, Clone)]
14pub struct CheckpointedResult {
15    /// The operation if it exists in the checkpoint
16    operation: Option<Operation>,
17}
18
19impl CheckpointedResult {
20    /// Creates a new CheckpointedResult with the given operation.
21    pub fn new(operation: Option<Operation>) -> Self {
22        Self { operation }
23    }
24
25    /// Creates an empty CheckpointedResult (no checkpoint exists).
26    pub fn empty() -> Self {
27        Self { operation: None }
28    }
29
30    /// Returns true if a checkpoint exists for this operation.
31    pub fn is_existent(&self) -> bool {
32        self.operation.is_some()
33    }
34
35    /// Returns true if the operation succeeded.
36    pub fn is_succeeded(&self) -> bool {
37        self.operation
38            .as_ref()
39            .map(|op| op.status == OperationStatus::Succeeded)
40            .unwrap_or(false)
41    }
42
43    /// Returns true if the operation failed.
44    pub fn is_failed(&self) -> bool {
45        self.operation
46            .as_ref()
47            .map(|op| op.status == OperationStatus::Failed)
48            .unwrap_or(false)
49    }
50
51    /// Returns true if the operation was cancelled.
52    pub fn is_cancelled(&self) -> bool {
53        self.operation
54            .as_ref()
55            .map(|op| op.status == OperationStatus::Cancelled)
56            .unwrap_or(false)
57    }
58
59    /// Returns true if the operation timed out.
60    pub fn is_timed_out(&self) -> bool {
61        self.operation
62            .as_ref()
63            .map(|op| op.status == OperationStatus::TimedOut)
64            .unwrap_or(false)
65    }
66
67    /// Returns true if the operation was stopped.
68    pub fn is_stopped(&self) -> bool {
69        self.operation
70            .as_ref()
71            .map(|op| op.status == OperationStatus::Stopped)
72            .unwrap_or(false)
73    }
74
75    /// Returns true if the operation is pending (waiting for retry).
76    /// Requirements: 3.7, 4.7
77    pub fn is_pending(&self) -> bool {
78        self.operation
79            .as_ref()
80            .map(|op| op.status == OperationStatus::Pending)
81            .unwrap_or(false)
82    }
83
84    /// Returns true if the operation is ready to resume execution.
85    /// Requirements: 3.7
86    pub fn is_ready(&self) -> bool {
87        self.operation
88            .as_ref()
89            .map(|op| op.status == OperationStatus::Ready)
90            .unwrap_or(false)
91    }
92
93    /// Returns true if the operation is in a terminal state (completed).
94    pub fn is_terminal(&self) -> bool {
95        self.operation
96            .as_ref()
97            .map(|op| op.status.is_terminal())
98            .unwrap_or(false)
99    }
100
101    /// Returns the operation status if the checkpoint exists.
102    pub fn status(&self) -> Option<OperationStatus> {
103        self.operation.as_ref().map(|op| op.status)
104    }
105
106    /// Returns the operation type if the checkpoint exists.
107    pub fn operation_type(&self) -> Option<OperationType> {
108        self.operation.as_ref().map(|op| op.operation_type)
109    }
110
111    /// Returns the serialized result if the operation succeeded.
112    /// This checks both type-specific details (e.g., StepDetails.Result) and the legacy Result field.
113    pub fn result(&self) -> Option<&str> {
114        self.operation.as_ref().and_then(|op| op.get_result())
115    }
116
117    /// Returns the error if the operation failed.
118    pub fn error(&self) -> Option<&ErrorObject> {
119        self.operation.as_ref().and_then(|op| op.error.as_ref())
120    }
121
122    /// Returns a reference to the underlying operation.
123    pub fn operation(&self) -> Option<&Operation> {
124        self.operation.as_ref()
125    }
126
127    /// Consumes self and returns the underlying operation.
128    pub fn into_operation(self) -> Option<Operation> {
129        self.operation
130    }
131
132    /// Returns the retry payload if this is a STEP operation with a payload.
133    ///
134    /// This is used for the wait-for-condition pattern where state is passed
135    /// between retry attempts via the Payload field.
136    ///
137    /// # Returns
138    ///
139    /// The payload string if available, None otherwise.
140    ///
141    /// # Requirements
142    ///
143    /// - 4.9: THE Step_Operation SHALL support RETRY action with Payload for wait-for-condition pattern
144    pub fn retry_payload(&self) -> Option<&str> {
145        self.operation
146            .as_ref()
147            .and_then(|op| op.get_retry_payload())
148    }
149
150    /// Returns the current attempt number for STEP operations.
151    ///
152    /// # Returns
153    ///
154    /// The attempt number (0-indexed) if available, None otherwise.
155    ///
156    /// # Requirements
157    ///
158    /// - 4.8: THE Step_Operation SHALL track attempt numbers in StepDetails.Attempt
159    pub fn attempt(&self) -> Option<u32> {
160        self.operation.as_ref().and_then(|op| op.get_attempt())
161    }
162
163    /// Returns the callback ID for CALLBACK operations.
164    ///
165    /// The callback ID is generated by the Lambda service when a CALLBACK operation
166    /// is checkpointed and is stored in CallbackDetails.CallbackId.
167    ///
168    /// # Returns
169    ///
170    /// The callback ID if this is a CALLBACK operation with a callback ID, None otherwise.
171    pub fn callback_id(&self) -> Option<String> {
172        self.operation
173            .as_ref()
174            .and_then(|op| op.callback_details.as_ref())
175            .and_then(|details| details.callback_id.clone())
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::error::ErrorObject;
183
184    fn create_operation(status: OperationStatus) -> Operation {
185        let mut op = Operation::new("test-op", OperationType::Step);
186        op.status = status;
187        op
188    }
189
190    fn create_succeeded_operation() -> Operation {
191        let mut op = Operation::new("test-op", OperationType::Step);
192        op.status = OperationStatus::Succeeded;
193        op.result = Some(r#"{"value": 42}"#.to_string());
194        op
195    }
196
197    fn create_failed_operation() -> Operation {
198        let mut op = Operation::new("test-op", OperationType::Step);
199        op.status = OperationStatus::Failed;
200        op.error = Some(ErrorObject::new("TestError", "Something went wrong"));
201        op
202    }
203
204    #[test]
205    fn test_empty_checkpoint_result() {
206        let result = CheckpointedResult::empty();
207        assert!(!result.is_existent());
208        assert!(!result.is_succeeded());
209        assert!(!result.is_failed());
210        assert!(!result.is_cancelled());
211        assert!(!result.is_timed_out());
212        assert!(!result.is_stopped());
213        assert!(!result.is_terminal());
214        assert!(result.status().is_none());
215        assert!(result.operation_type().is_none());
216        assert!(result.result().is_none());
217        assert!(result.error().is_none());
218        assert!(result.operation().is_none());
219    }
220
221    #[test]
222    fn test_succeeded_checkpoint_result() {
223        let op = create_succeeded_operation();
224        let result = CheckpointedResult::new(Some(op));
225
226        assert!(result.is_existent());
227        assert!(result.is_succeeded());
228        assert!(!result.is_failed());
229        assert!(result.is_terminal());
230        assert_eq!(result.status(), Some(OperationStatus::Succeeded));
231        assert_eq!(result.operation_type(), Some(OperationType::Step));
232        assert_eq!(result.result(), Some(r#"{"value": 42}"#));
233        assert!(result.error().is_none());
234    }
235
236    #[test]
237    fn test_failed_checkpoint_result() {
238        let op = create_failed_operation();
239        let result = CheckpointedResult::new(Some(op));
240
241        assert!(result.is_existent());
242        assert!(!result.is_succeeded());
243        assert!(result.is_failed());
244        assert!(result.is_terminal());
245        assert_eq!(result.status(), Some(OperationStatus::Failed));
246        assert!(result.result().is_none());
247        assert!(result.error().is_some());
248        assert_eq!(result.error().unwrap().error_type, "TestError");
249    }
250
251    #[test]
252    fn test_cancelled_checkpoint_result() {
253        let op = create_operation(OperationStatus::Cancelled);
254        let result = CheckpointedResult::new(Some(op));
255
256        assert!(result.is_existent());
257        assert!(result.is_cancelled());
258        assert!(result.is_terminal());
259    }
260
261    #[test]
262    fn test_timed_out_checkpoint_result() {
263        let op = create_operation(OperationStatus::TimedOut);
264        let result = CheckpointedResult::new(Some(op));
265
266        assert!(result.is_existent());
267        assert!(result.is_timed_out());
268        assert!(result.is_terminal());
269    }
270
271    #[test]
272    fn test_stopped_checkpoint_result() {
273        let op = create_operation(OperationStatus::Stopped);
274        let result = CheckpointedResult::new(Some(op));
275
276        assert!(result.is_existent());
277        assert!(result.is_stopped());
278        assert!(result.is_terminal());
279    }
280
281    #[test]
282    fn test_pending_checkpoint_result() {
283        let op = create_operation(OperationStatus::Pending);
284        let result = CheckpointedResult::new(Some(op));
285
286        assert!(result.is_existent());
287        assert!(result.is_pending());
288        assert!(!result.is_ready());
289        assert!(!result.is_terminal());
290    }
291
292    #[test]
293    fn test_ready_checkpoint_result() {
294        let op = create_operation(OperationStatus::Ready);
295        let result = CheckpointedResult::new(Some(op));
296
297        assert!(result.is_existent());
298        assert!(result.is_ready());
299        assert!(!result.is_pending());
300        assert!(!result.is_terminal());
301    }
302
303    #[test]
304    fn test_started_checkpoint_result() {
305        let op = create_operation(OperationStatus::Started);
306        let result = CheckpointedResult::new(Some(op));
307
308        assert!(result.is_existent());
309        assert!(!result.is_succeeded());
310        assert!(!result.is_failed());
311        assert!(!result.is_terminal());
312    }
313
314    #[test]
315    fn test_into_operation() {
316        let op = create_succeeded_operation();
317        let result = CheckpointedResult::new(Some(op));
318
319        let operation = result.into_operation();
320        assert!(operation.is_some());
321        assert_eq!(operation.unwrap().operation_id, "test-op");
322    }
323}