Skip to main content

fakecloud_stepfunctions/
error_handling.rs

1use serde_json::Value;
2
3/// Check if an error matches a Catch block and return the target state name.
4pub fn find_catcher(catchers: &[Value], error: &str) -> Option<(String, Option<String>)> {
5    for catcher in catchers {
6        let error_equals = match catcher["ErrorEquals"].as_array() {
7            Some(arr) => arr,
8            None => continue,
9        };
10
11        let matches = error_equals.iter().any(|e| {
12            let pattern = e.as_str().unwrap_or("");
13            pattern == "States.ALL" || pattern == error
14        });
15
16        if matches {
17            let next = catcher["Next"].as_str()?.to_string();
18            // Distinguish between absent ResultPath (None) and JSON null ResultPath
19            let result_path = if catcher.get("ResultPath").is_some_and(|v| v.is_null()) {
20                Some("null".to_string())
21            } else {
22                catcher["ResultPath"].as_str().map(|s| s.to_string())
23            };
24            return Some((next, result_path));
25        }
26    }
27    None
28}
29
30/// Check if we should retry an error based on Retry configuration.
31/// Returns the delay in milliseconds if we should retry, or None if retries are exhausted.
32pub fn should_retry(retriers: &[Value], error: &str, attempt: u32) -> Option<u64> {
33    for retrier in retriers {
34        let error_equals = match retrier["ErrorEquals"].as_array() {
35            Some(arr) => arr,
36            None => continue,
37        };
38
39        let matches = error_equals.iter().any(|e| {
40            let pattern = e.as_str().unwrap_or("");
41            pattern == "States.ALL" || pattern == error
42        });
43
44        if matches {
45            let max_attempts = retrier["MaxAttempts"].as_u64().unwrap_or(3) as u32;
46            if attempt >= max_attempts {
47                return None;
48            }
49
50            let interval_seconds = retrier["IntervalSeconds"].as_f64().unwrap_or(1.0);
51            let backoff_rate = retrier["BackoffRate"].as_f64().unwrap_or(2.0);
52            let max_delay = retrier["MaxDelaySeconds"].as_f64().unwrap_or(60.0);
53
54            let delay = interval_seconds * backoff_rate.powi(attempt as i32);
55            let delay = delay.min(max_delay);
56
57            return Some((delay * 1000.0) as u64);
58        }
59    }
60    None
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use serde_json::json;
67
68    #[test]
69    fn test_find_catcher_exact_match() {
70        let catchers = vec![json!({
71            "ErrorEquals": ["CustomError"],
72            "Next": "HandleError"
73        })];
74        let result = find_catcher(&catchers, "CustomError");
75        assert_eq!(result, Some(("HandleError".to_string(), None)));
76    }
77
78    #[test]
79    fn test_find_catcher_states_all() {
80        let catchers = vec![json!({
81            "ErrorEquals": ["States.ALL"],
82            "Next": "CatchAll"
83        })];
84        let result = find_catcher(&catchers, "AnyError");
85        assert_eq!(result, Some(("CatchAll".to_string(), None)));
86    }
87
88    #[test]
89    fn test_find_catcher_no_match() {
90        let catchers = vec![json!({
91            "ErrorEquals": ["SpecificError"],
92            "Next": "Handle"
93        })];
94        let result = find_catcher(&catchers, "DifferentError");
95        assert_eq!(result, None);
96    }
97
98    #[test]
99    fn test_find_catcher_with_result_path() {
100        let catchers = vec![json!({
101            "ErrorEquals": ["States.ALL"],
102            "Next": "Handle",
103            "ResultPath": "$.error"
104        })];
105        let result = find_catcher(&catchers, "AnyError");
106        assert_eq!(
107            result,
108            Some(("Handle".to_string(), Some("$.error".to_string())))
109        );
110    }
111
112    #[test]
113    fn test_should_retry_first_attempt() {
114        let retriers = vec![json!({
115            "ErrorEquals": ["States.ALL"],
116            "IntervalSeconds": 1,
117            "MaxAttempts": 3,
118            "BackoffRate": 2.0
119        })];
120        let result = should_retry(&retriers, "AnyError", 0);
121        assert_eq!(result, Some(1000)); // 1s * 2^0 = 1s
122    }
123
124    #[test]
125    fn test_should_retry_second_attempt() {
126        let retriers = vec![json!({
127            "ErrorEquals": ["States.ALL"],
128            "IntervalSeconds": 1,
129            "MaxAttempts": 3,
130            "BackoffRate": 2.0
131        })];
132        let result = should_retry(&retriers, "AnyError", 1);
133        assert_eq!(result, Some(2000)); // 1s * 2^1 = 2s
134    }
135
136    #[test]
137    fn test_should_retry_exhausted() {
138        let retriers = vec![json!({
139            "ErrorEquals": ["States.ALL"],
140            "MaxAttempts": 2
141        })];
142        let result = should_retry(&retriers, "AnyError", 2);
143        assert_eq!(result, None);
144    }
145
146    #[test]
147    fn test_should_retry_no_match() {
148        let retriers = vec![json!({
149            "ErrorEquals": ["SpecificError"],
150            "MaxAttempts": 3
151        })];
152        let result = should_retry(&retriers, "DifferentError", 0);
153        assert_eq!(result, None);
154    }
155}