1use serde_json::Value;
2
3const NON_WILDCARD_ERRORS: [&str; 2] = ["States.Runtime", "States.DataLimitExceeded"];
8
9fn pattern_matches(pattern: &str, error: &str, is_task_error: bool) -> bool {
18 match pattern {
19 "States.ALL" => !NON_WILDCARD_ERRORS.contains(&error),
20 "States.TaskFailed" => {
21 is_task_error && error != "States.Timeout" && !NON_WILDCARD_ERRORS.contains(&error)
22 }
23 exact => exact == error,
24 }
25}
26
27pub fn find_catcher(
32 catchers: &[Value],
33 error: &str,
34 is_task_error: bool,
35) -> Option<(String, Option<String>)> {
36 for catcher in catchers {
37 let error_equals = match catcher["ErrorEquals"].as_array() {
38 Some(arr) => arr,
39 None => continue,
40 };
41
42 let matches = error_equals
43 .iter()
44 .any(|e| pattern_matches(e.as_str().unwrap_or(""), error, is_task_error));
45
46 if matches {
47 let next = match catcher["Next"].as_str() {
48 Some(s) => s.to_string(),
49 None => continue, };
51 let result_path = if catcher.get("ResultPath").is_some_and(|v| v.is_null()) {
53 Some("null".to_string())
54 } else {
55 catcher["ResultPath"].as_str().map(|s| s.to_string())
56 };
57 return Some((next, result_path));
58 }
59 }
60 None
61}
62
63pub fn should_retry(
69 retriers: &[Value],
70 error: &str,
71 attempt: u32,
72 is_task_error: bool,
73) -> Option<u64> {
74 for retrier in retriers {
75 let error_equals = match retrier["ErrorEquals"].as_array() {
76 Some(arr) => arr,
77 None => continue,
78 };
79
80 let matches = error_equals
81 .iter()
82 .any(|e| pattern_matches(e.as_str().unwrap_or(""), error, is_task_error));
83
84 if matches {
85 let max_attempts = retrier["MaxAttempts"].as_u64().unwrap_or(3) as u32;
86 if attempt >= max_attempts {
87 return None;
88 }
89
90 let interval_seconds = retrier["IntervalSeconds"].as_f64().unwrap_or(1.0);
91 let backoff_rate = retrier["BackoffRate"].as_f64().unwrap_or(2.0);
92 let max_delay = retrier["MaxDelaySeconds"].as_f64().unwrap_or(60.0);
93
94 let delay = interval_seconds * backoff_rate.powi(attempt as i32);
95 let delay = delay.min(max_delay);
96
97 return Some((delay * 1000.0) as u64);
98 }
99 }
100 None
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use serde_json::json;
107
108 #[test]
109 fn test_find_catcher_exact_match() {
110 let catchers = vec![json!({
111 "ErrorEquals": ["CustomError"],
112 "Next": "HandleError"
113 })];
114 let result = find_catcher(&catchers, "CustomError", true);
115 assert_eq!(result, Some(("HandleError".to_string(), None)));
116 }
117
118 #[test]
119 fn test_find_catcher_states_all() {
120 let catchers = vec![json!({
121 "ErrorEquals": ["States.ALL"],
122 "Next": "CatchAll"
123 })];
124 let result = find_catcher(&catchers, "AnyError", true);
125 assert_eq!(result, Some(("CatchAll".to_string(), None)));
126 }
127
128 #[test]
129 fn test_find_catcher_no_match() {
130 let catchers = vec![json!({
131 "ErrorEquals": ["SpecificError"],
132 "Next": "Handle"
133 })];
134 let result = find_catcher(&catchers, "DifferentError", true);
135 assert_eq!(result, None);
136 }
137
138 #[test]
139 fn test_find_catcher_with_result_path() {
140 let catchers = vec![json!({
141 "ErrorEquals": ["States.ALL"],
142 "Next": "Handle",
143 "ResultPath": "$.error"
144 })];
145 let result = find_catcher(&catchers, "AnyError", true);
146 assert_eq!(
147 result,
148 Some(("Handle".to_string(), Some("$.error".to_string())))
149 );
150 }
151
152 #[test]
153 fn test_find_catcher_skips_malformed_and_finds_next() {
154 let catchers = vec![
156 json!({
157 "ErrorEquals": ["States.ALL"]
158 }),
160 json!({
161 "ErrorEquals": ["States.ALL"],
162 "Next": "FallbackHandler"
163 }),
164 ];
165 let result = find_catcher(&catchers, "AnyError", true);
166 assert_eq!(result, Some(("FallbackHandler".to_string(), None)));
167 }
168
169 #[test]
172 fn test_find_catcher_task_failed_wildcard_catches_custom_error() {
173 let catchers = vec![json!({
174 "ErrorEquals": ["States.TaskFailed"],
175 "Next": "Handler"
176 })];
177 let result = find_catcher(&catchers, "CustomLambdaError", true);
178 assert_eq!(result, Some(("Handler".to_string(), None)));
179 }
180
181 #[test]
183 fn test_find_catcher_task_failed_excludes_timeout() {
184 let catchers = vec![json!({
185 "ErrorEquals": ["States.TaskFailed"],
186 "Next": "Handler"
187 })];
188 assert_eq!(find_catcher(&catchers, "States.Timeout", true), None);
189 }
190
191 #[test]
193 fn test_find_catcher_task_failed_only_for_task_errors() {
194 let catchers = vec![json!({
195 "ErrorEquals": ["States.TaskFailed"],
196 "Next": "Handler"
197 })];
198 assert_eq!(find_catcher(&catchers, "SomeBranchError", false), None);
199 }
200
201 #[test]
203 fn test_find_catcher_states_all_excludes_runtime() {
204 let catchers = vec![json!({
205 "ErrorEquals": ["States.ALL"],
206 "Next": "Handler"
207 })];
208 assert_eq!(find_catcher(&catchers, "States.Runtime", true), None);
209 assert_eq!(
210 find_catcher(&catchers, "States.DataLimitExceeded", true),
211 None
212 );
213 assert!(find_catcher(&catchers, "States.Timeout", true).is_some());
215 assert!(find_catcher(&catchers, "Whatever", true).is_some());
216 }
217
218 #[test]
219 fn test_should_retry_first_attempt() {
220 let retriers = vec![json!({
221 "ErrorEquals": ["States.ALL"],
222 "IntervalSeconds": 1,
223 "MaxAttempts": 3,
224 "BackoffRate": 2.0
225 })];
226 let result = should_retry(&retriers, "AnyError", 0, true);
227 assert_eq!(result, Some(1000)); }
229
230 #[test]
231 fn test_should_retry_second_attempt() {
232 let retriers = vec![json!({
233 "ErrorEquals": ["States.ALL"],
234 "IntervalSeconds": 1,
235 "MaxAttempts": 3,
236 "BackoffRate": 2.0
237 })];
238 let result = should_retry(&retriers, "AnyError", 1, true);
239 assert_eq!(result, Some(2000)); }
241
242 #[test]
243 fn test_should_retry_exhausted() {
244 let retriers = vec![json!({
245 "ErrorEquals": ["States.ALL"],
246 "MaxAttempts": 2
247 })];
248 let result = should_retry(&retriers, "AnyError", 2, true);
249 assert_eq!(result, None);
250 }
251
252 #[test]
253 fn test_should_retry_no_match() {
254 let retriers = vec![json!({
255 "ErrorEquals": ["SpecificError"],
256 "MaxAttempts": 3
257 })];
258 let result = should_retry(&retriers, "DifferentError", 0, true);
259 assert_eq!(result, None);
260 }
261
262 #[test]
264 fn test_should_retry_task_failed_wildcard() {
265 let retriers = vec![json!({
266 "ErrorEquals": ["States.TaskFailed"],
267 "IntervalSeconds": 1,
268 "MaxAttempts": 3
269 })];
270 assert_eq!(should_retry(&retriers, "CustomError", 0, true), Some(1000));
271 assert_eq!(should_retry(&retriers, "CustomError", 0, false), None);
273 }
274
275 #[test]
277 fn test_should_retry_honours_max_delay_seconds() {
278 let retriers = vec![json!({
279 "ErrorEquals": ["States.ALL"],
280 "IntervalSeconds": 30,
281 "MaxAttempts": 5,
282 "BackoffRate": 2.0,
283 "MaxDelaySeconds": 40
284 })];
285 assert_eq!(should_retry(&retriers, "AnyError", 1, true), Some(40_000));
287 assert_eq!(should_retry(&retriers, "AnyError", 0, true), Some(30_000));
289 }
290}