1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::FoldContext;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct FoldOutcome<S> {
13 pub state: S,
15
16 pub entries_processed: usize,
18
19 pub started_at: DateTime<Utc>,
21
22 pub completed_at: DateTime<Utc>,
24
25 pub context: FoldContext,
27
28 #[serde(default)]
30 pub metadata: serde_json::Value,
31}
32
33impl<S> FoldOutcome<S> {
34 pub fn new(state: S, entries_processed: usize, context: FoldContext) -> Self {
36 let now = Utc::now();
37 Self {
38 state,
39 entries_processed,
40 started_at: now,
41 completed_at: now,
42 context,
43 metadata: serde_json::Value::Null,
44 }
45 }
46
47 pub fn with_timing(
49 state: S,
50 entries_processed: usize,
51 context: FoldContext,
52 started_at: DateTime<Utc>,
53 ) -> Self {
54 Self {
55 state,
56 entries_processed,
57 started_at,
58 completed_at: Utc::now(),
59 context,
60 metadata: serde_json::Value::Null,
61 }
62 }
63
64 pub fn with_elapsed(
69 state: S,
70 entries_processed: usize,
71 context: FoldContext,
72 started_at: DateTime<Utc>,
73 elapsed: std::time::Duration,
74 ) -> Self {
75 let completed_at = started_at
76 + chrono::Duration::from_std(elapsed).unwrap_or_else(|_| chrono::Duration::zero());
77
78 Self {
79 state,
80 entries_processed,
81 started_at,
82 completed_at,
83 context,
84 metadata: serde_json::Value::Null,
85 }
86 }
87
88 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
90 self.metadata = metadata;
91 self
92 }
93
94 pub fn duration(&self) -> chrono::Duration {
96 self.completed_at - self.started_at
97 }
98
99 pub fn map<T, F: FnOnce(S) -> T>(self, f: F) -> FoldOutcome<T> {
101 FoldOutcome {
102 state: f(self.state),
103 entries_processed: self.entries_processed,
104 started_at: self.started_at,
105 completed_at: self.completed_at,
106 context: self.context,
107 metadata: self.metadata,
108 }
109 }
110}
111
112impl<S: Default> Default for FoldOutcome<S> {
113 fn default() -> Self {
114 Self::new(S::default(), 0, FoldContext::default())
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_fold_outcome_creation() {
124 let result = FoldOutcome::new(42, 10, FoldContext::new());
125 assert_eq!(result.state, 42);
126 assert_eq!(result.entries_processed, 10);
127 }
128
129 #[test]
130 fn test_fold_outcome_map() {
131 let result = FoldOutcome::new(42, 10, FoldContext::new());
132 let mapped = result.map(|x| x.to_string());
133 assert_eq!(mapped.state, "42");
134 assert_eq!(mapped.entries_processed, 10);
135 }
136
137 #[test]
138 fn test_fold_outcome_with_elapsed() {
139 let started_at = Utc::now();
140 let outcome = FoldOutcome::with_elapsed(
141 7usize,
142 2,
143 FoldContext::new(),
144 started_at,
145 std::time::Duration::from_millis(5),
146 );
147 assert!(outcome.completed_at >= outcome.started_at);
148 }
149
150 #[test]
151 fn test_fold_outcome_with_elapsed_exact_arithmetic() {
152 let started_at = Utc::now();
153 let elapsed = std::time::Duration::from_millis(123);
154 let outcome =
155 FoldOutcome::with_elapsed("state", 5, FoldContext::new(), started_at, elapsed);
156 let expected_completed = started_at + chrono::Duration::from_std(elapsed).unwrap();
157 assert_eq!(outcome.completed_at, expected_completed);
158 assert_eq!(outcome.started_at, started_at);
159 }
160
161 #[test]
162 fn test_fold_outcome_with_elapsed_zero_duration() {
163 let started_at = Utc::now();
164 let outcome = FoldOutcome::with_elapsed(
165 0u32,
166 0,
167 FoldContext::new(),
168 started_at,
169 std::time::Duration::ZERO,
170 );
171 assert_eq!(outcome.completed_at, outcome.started_at);
172 }
173
174 #[test]
175 fn test_fold_outcome_with_elapsed_large_duration() {
176 let started_at = Utc::now();
177 let elapsed = std::time::Duration::from_secs(3600);
178 let outcome =
179 FoldOutcome::with_elapsed(42u64, 100, FoldContext::new(), started_at, elapsed);
180 let expected = started_at + chrono::Duration::from_std(elapsed).unwrap();
181 assert_eq!(outcome.completed_at, expected);
182 assert_eq!(outcome.state, 42u64);
183 }
184}