Skip to main content

khive_fold/
result.rs

1//! Fold outcome type
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::FoldContext;
7
8/// Outcome of a fold operation.
9///
10/// Contains the derived state along with metadata about the fold execution.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct FoldOutcome<S> {
13    /// The derived state
14    pub state: S,
15
16    /// Number of entries processed
17    pub entries_processed: usize,
18
19    /// When the fold started
20    pub started_at: DateTime<Utc>,
21
22    /// When the fold completed
23    pub completed_at: DateTime<Utc>,
24
25    /// Context used for the fold
26    pub context: FoldContext,
27
28    /// Optional metadata
29    #[serde(default)]
30    pub metadata: serde_json::Value,
31}
32
33impl<S> FoldOutcome<S> {
34    /// Create a new fold result with identical start and completion timestamps.
35    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    /// Create with timing information.
48    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    /// Create with timing information derived from a monotonic elapsed duration.
65    ///
66    /// Avoids a second `Utc::now()` call by computing `completed_at` from
67    /// `started_at + elapsed`.
68    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    /// Set metadata.
89    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
90        self.metadata = metadata;
91        self
92    }
93
94    /// Get duration of the fold.
95    pub fn duration(&self) -> chrono::Duration {
96        self.completed_at - self.started_at
97    }
98
99    /// Map the state to a different type.
100    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}