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