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    pub fn retry_payload(&self) -> Option<&str> {
141        self.operation
142            .as_ref()
143            .and_then(|op| op.get_retry_payload())
144    }
145
146    /// Returns the current attempt number for STEP operations.
147    ///
148    /// # Returns
149    ///
150    /// The attempt number (0-indexed) if available, None otherwise.
151    pub fn attempt(&self) -> Option<u32> {
152        self.operation.as_ref().and_then(|op| op.get_attempt())
153    }
154
155    /// Returns the callback ID for CALLBACK operations.
156    ///
157    /// The callback ID is generated by the Lambda service when a CALLBACK operation
158    /// is checkpointed and is stored in CallbackDetails.CallbackId.
159    ///
160    /// # Returns
161    ///
162    /// The callback ID if this is a CALLBACK operation with a callback ID, None otherwise.
163    pub fn callback_id(&self) -> Option<String> {
164        self.operation
165            .as_ref()
166            .and_then(|op| op.callback_details.as_ref())
167            .and_then(|details| details.callback_id.clone())
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::error::ErrorObject;
175
176    fn create_operation(status: OperationStatus) -> Operation {
177        let mut op = Operation::new("test-op", OperationType::Step);
178        op.status = status;
179        op
180    }
181
182    fn create_succeeded_operation() -> Operation {
183        let mut op = Operation::new("test-op", OperationType::Step);
184        op.status = OperationStatus::Succeeded;
185        op.result = Some(r#"{"value": 42}"#.to_string());
186        op
187    }
188
189    fn create_failed_operation() -> Operation {
190        let mut op = Operation::new("test-op", OperationType::Step);
191        op.status = OperationStatus::Failed;
192        op.error = Some(ErrorObject::new("TestError", "Something went wrong"));
193        op
194    }
195
196    #[test]
197    fn test_empty_checkpoint_result() {
198        let result = CheckpointedResult::empty();
199        assert!(!result.is_existent());
200        assert!(!result.is_succeeded());
201        assert!(!result.is_failed());
202        assert!(!result.is_cancelled());
203        assert!(!result.is_timed_out());
204        assert!(!result.is_stopped());
205        assert!(!result.is_terminal());
206        assert!(result.status().is_none());
207        assert!(result.operation_type().is_none());
208        assert!(result.result().is_none());
209        assert!(result.error().is_none());
210        assert!(result.operation().is_none());
211    }
212
213    #[test]
214    fn test_succeeded_checkpoint_result() {
215        let op = create_succeeded_operation();
216        let result = CheckpointedResult::new(Some(op));
217
218        assert!(result.is_existent());
219        assert!(result.is_succeeded());
220        assert!(!result.is_failed());
221        assert!(result.is_terminal());
222        assert_eq!(result.status(), Some(OperationStatus::Succeeded));
223        assert_eq!(result.operation_type(), Some(OperationType::Step));
224        assert_eq!(result.result(), Some(r#"{"value": 42}"#));
225        assert!(result.error().is_none());
226    }
227
228    #[test]
229    fn test_failed_checkpoint_result() {
230        let op = create_failed_operation();
231        let result = CheckpointedResult::new(Some(op));
232
233        assert!(result.is_existent());
234        assert!(!result.is_succeeded());
235        assert!(result.is_failed());
236        assert!(result.is_terminal());
237        assert_eq!(result.status(), Some(OperationStatus::Failed));
238        assert!(result.result().is_none());
239        assert!(result.error().is_some());
240        assert_eq!(result.error().unwrap().error_type, "TestError");
241    }
242
243    #[test]
244    fn test_cancelled_checkpoint_result() {
245        let op = create_operation(OperationStatus::Cancelled);
246        let result = CheckpointedResult::new(Some(op));
247
248        assert!(result.is_existent());
249        assert!(result.is_cancelled());
250        assert!(result.is_terminal());
251    }
252
253    #[test]
254    fn test_timed_out_checkpoint_result() {
255        let op = create_operation(OperationStatus::TimedOut);
256        let result = CheckpointedResult::new(Some(op));
257
258        assert!(result.is_existent());
259        assert!(result.is_timed_out());
260        assert!(result.is_terminal());
261    }
262
263    #[test]
264    fn test_stopped_checkpoint_result() {
265        let op = create_operation(OperationStatus::Stopped);
266        let result = CheckpointedResult::new(Some(op));
267
268        assert!(result.is_existent());
269        assert!(result.is_stopped());
270        assert!(result.is_terminal());
271    }
272
273    #[test]
274    fn test_pending_checkpoint_result() {
275        let op = create_operation(OperationStatus::Pending);
276        let result = CheckpointedResult::new(Some(op));
277
278        assert!(result.is_existent());
279        assert!(result.is_pending());
280        assert!(!result.is_ready());
281        assert!(!result.is_terminal());
282    }
283
284    #[test]
285    fn test_ready_checkpoint_result() {
286        let op = create_operation(OperationStatus::Ready);
287        let result = CheckpointedResult::new(Some(op));
288
289        assert!(result.is_existent());
290        assert!(result.is_ready());
291        assert!(!result.is_pending());
292        assert!(!result.is_terminal());
293    }
294
295    #[test]
296    fn test_started_checkpoint_result() {
297        let op = create_operation(OperationStatus::Started);
298        let result = CheckpointedResult::new(Some(op));
299
300        assert!(result.is_existent());
301        assert!(!result.is_succeeded());
302        assert!(!result.is_failed());
303        assert!(!result.is_terminal());
304    }
305
306    #[test]
307    fn test_into_operation() {
308        let op = create_succeeded_operation();
309        let result = CheckpointedResult::new(Some(op));
310
311        let operation = result.into_operation();
312        assert!(operation.is_some());
313        assert_eq!(operation.unwrap().operation_id, "test-op");
314    }
315}