Skip to main content

durable_execution_sdk/
summary_generators.rs

1//! Pre-built summary generators for batch operations.
2//!
3//! Provides closures suitable for use with `ChildConfig::summary_generator`
4//! to produce compact JSON summaries when parallel or map results exceed
5//! the 256KB checkpoint size limit.
6//!
7//! # Example
8//!
9//! ```
10//! use durable_execution_sdk::summary_generators;
11//!
12//! let gen = summary_generators::parallel_summary();
13//! let input = r#"[{"status":"ok"},{"error":"timeout"}]"#;
14//! let summary = gen(input);
15//! assert!(summary.contains("\"type\":\"parallel\""));
16//!
17//! let gen = summary_generators::map_summary();
18//! let summary = gen(input);
19//! assert!(summary.contains("\"type\":\"map\""));
20//! ```
21
22/// Creates a summary generator for parallel operation results.
23///
24/// The returned closure accepts a serialized JSON array of results and
25/// produces a JSON string containing:
26/// - `type`: `"parallel"`
27/// - `totalCount`: total number of elements
28/// - `successCount`: number of successful elements
29/// - `failureCount`: number of failed elements
30/// - `status`: `"completed"` if all succeeded, `"partial"` if some failed,
31///   `"failed"` if all failed
32///
33/// Each array element is considered a failure if it is an object containing
34/// an `"error"` field. All other elements are counted as successes.
35pub fn parallel_summary() -> impl Fn(&str) -> String {
36    build_summary("parallel")
37}
38
39/// Creates a summary generator for map operation results.
40///
41/// The returned closure accepts a serialized JSON array of results and
42/// produces a JSON string containing:
43/// - `type`: `"map"`
44/// - `totalCount`: total number of elements
45/// - `successCount`: number of successful elements
46/// - `failureCount`: number of failed elements
47/// - `status`: `"completed"` if all succeeded, `"partial"` if some failed,
48///   `"failed"` if all failed
49///
50/// Each array element is considered a failure if it is an object containing
51/// an `"error"` field. All other elements are counted as successes.
52pub fn map_summary() -> impl Fn(&str) -> String {
53    build_summary("map")
54}
55
56fn build_summary(summary_type: &str) -> impl Fn(&str) -> String {
57    let summary_type = summary_type.to_string();
58    move |serialized: &str| {
59        let items: Vec<serde_json::Value> = serde_json::from_str(serialized).unwrap_or_default();
60
61        let total = items.len();
62        let failure_count = items
63            .iter()
64            .filter(|item| item.is_object() && item.get("error").is_some())
65            .count();
66        let success_count = total - failure_count;
67
68        let status = if total == 0 || failure_count == total {
69            "failed"
70        } else if failure_count == 0 {
71            "completed"
72        } else {
73            "partial"
74        };
75
76        serde_json::json!({
77            "type": summary_type,
78            "totalCount": total,
79            "successCount": success_count,
80            "failureCount": failure_count,
81            "status": status,
82        })
83        .to_string()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    // --- parallel_summary tests ---
92
93    #[test]
94    fn parallel_summary_all_success() {
95        let gen = parallel_summary();
96        let input = r#"[{"status":"ok"},{"status":"done"},{"status":"ok"}]"#;
97        let output = gen(input);
98        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
99
100        assert_eq!(parsed["type"], "parallel");
101        assert_eq!(parsed["totalCount"], 3);
102        assert_eq!(parsed["successCount"], 3);
103        assert_eq!(parsed["failureCount"], 0);
104        assert_eq!(parsed["status"], "completed");
105    }
106
107    #[test]
108    fn parallel_summary_all_failure() {
109        let gen = parallel_summary();
110        let input = r#"[{"error":"timeout"},{"error":"connection refused"}]"#;
111        let output = gen(input);
112        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
113
114        assert_eq!(parsed["type"], "parallel");
115        assert_eq!(parsed["totalCount"], 2);
116        assert_eq!(parsed["successCount"], 0);
117        assert_eq!(parsed["failureCount"], 2);
118        assert_eq!(parsed["status"], "failed");
119    }
120
121    #[test]
122    fn parallel_summary_partial() {
123        let gen = parallel_summary();
124        let input = r#"[{"status":"ok"},{"error":"timeout"},{"status":"done"}]"#;
125        let output = gen(input);
126        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
127
128        assert_eq!(parsed["type"], "parallel");
129        assert_eq!(parsed["totalCount"], 3);
130        assert_eq!(parsed["successCount"], 2);
131        assert_eq!(parsed["failureCount"], 1);
132        assert_eq!(parsed["status"], "partial");
133    }
134
135    #[test]
136    fn parallel_summary_empty_array() {
137        let gen = parallel_summary();
138        let output = gen("[]");
139        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
140
141        assert_eq!(parsed["type"], "parallel");
142        assert_eq!(parsed["totalCount"], 0);
143        assert_eq!(parsed["successCount"], 0);
144        assert_eq!(parsed["failureCount"], 0);
145        assert_eq!(parsed["status"], "failed");
146    }
147
148    #[test]
149    fn parallel_summary_invalid_json_returns_empty() {
150        let gen = parallel_summary();
151        let output = gen("not json");
152        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
153
154        assert_eq!(parsed["type"], "parallel");
155        assert_eq!(parsed["totalCount"], 0);
156        assert_eq!(parsed["status"], "failed");
157    }
158
159    #[test]
160    fn parallel_summary_primitive_values_are_successes() {
161        let gen = parallel_summary();
162        let input = r#"[42, "hello", true, null]"#;
163        let output = gen(input);
164        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
165
166        assert_eq!(parsed["totalCount"], 4);
167        assert_eq!(parsed["successCount"], 4);
168        assert_eq!(parsed["failureCount"], 0);
169        assert_eq!(parsed["status"], "completed");
170    }
171
172    #[test]
173    fn parallel_summary_object_without_error_is_success() {
174        let gen = parallel_summary();
175        let input = r#"[{"result":"ok"},{"value":123}]"#;
176        let output = gen(input);
177        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
178
179        assert_eq!(parsed["successCount"], 2);
180        assert_eq!(parsed["failureCount"], 0);
181    }
182
183    // --- map_summary tests ---
184
185    #[test]
186    fn map_summary_all_success() {
187        let gen = map_summary();
188        let input = r#"[{"status":"ok"},{"status":"done"}]"#;
189        let output = gen(input);
190        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
191
192        assert_eq!(parsed["type"], "map");
193        assert_eq!(parsed["totalCount"], 2);
194        assert_eq!(parsed["successCount"], 2);
195        assert_eq!(parsed["failureCount"], 0);
196        assert_eq!(parsed["status"], "completed");
197    }
198
199    #[test]
200    fn map_summary_all_failure() {
201        let gen = map_summary();
202        let input = r#"[{"error":"e1"},{"error":"e2"}]"#;
203        let output = gen(input);
204        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
205
206        assert_eq!(parsed["type"], "map");
207        assert_eq!(parsed["totalCount"], 2);
208        assert_eq!(parsed["successCount"], 0);
209        assert_eq!(parsed["failureCount"], 2);
210        assert_eq!(parsed["status"], "failed");
211    }
212
213    #[test]
214    fn map_summary_partial() {
215        let gen = map_summary();
216        let input = r#"[{"status":"ok"},{"error":"timeout"}]"#;
217        let output = gen(input);
218        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
219
220        assert_eq!(parsed["type"], "map");
221        assert_eq!(parsed["totalCount"], 2);
222        assert_eq!(parsed["successCount"], 1);
223        assert_eq!(parsed["failureCount"], 1);
224        assert_eq!(parsed["status"], "partial");
225    }
226
227    #[test]
228    fn map_summary_empty_array() {
229        let gen = map_summary();
230        let output = gen("[]");
231        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
232
233        assert_eq!(parsed["type"], "map");
234        assert_eq!(parsed["totalCount"], 0);
235        assert_eq!(parsed["status"], "failed");
236    }
237
238    // --- JSON structure tests ---
239
240    #[test]
241    fn parallel_summary_output_is_valid_json() {
242        let gen = parallel_summary();
243        let output = gen(r#"[1,2,3]"#);
244        let parsed: Result<serde_json::Value, _> = serde_json::from_str(&output);
245        assert!(parsed.is_ok());
246    }
247
248    #[test]
249    fn map_summary_output_is_valid_json() {
250        let gen = map_summary();
251        let output = gen(r#"[1,2,3]"#);
252        let parsed: Result<serde_json::Value, _> = serde_json::from_str(&output);
253        assert!(parsed.is_ok());
254    }
255
256    #[test]
257    fn parallel_summary_contains_all_required_fields() {
258        let gen = parallel_summary();
259        let output = gen(r#"[{"status":"ok"}]"#);
260        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
261
262        assert!(parsed.get("type").is_some(), "missing 'type' field");
263        assert!(parsed.get("totalCount").is_some(), "missing 'totalCount'");
264        assert!(
265            parsed.get("successCount").is_some(),
266            "missing 'successCount'"
267        );
268        assert!(
269            parsed.get("failureCount").is_some(),
270            "missing 'failureCount'"
271        );
272        assert!(parsed.get("status").is_some(), "missing 'status' field");
273    }
274
275    #[test]
276    fn map_summary_contains_all_required_fields() {
277        let gen = map_summary();
278        let output = gen(r#"[{"status":"ok"}]"#);
279        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
280
281        assert!(parsed.get("type").is_some(), "missing 'type' field");
282        assert!(parsed.get("totalCount").is_some(), "missing 'totalCount'");
283        assert!(
284            parsed.get("successCount").is_some(),
285            "missing 'successCount'"
286        );
287        assert!(
288            parsed.get("failureCount").is_some(),
289            "missing 'failureCount'"
290        );
291        assert!(parsed.get("status").is_some(), "missing 'status' field");
292    }
293}