mindset/core/history.rs
1//! State transition history tracking.
2//!
3//! Provides immutable tracking of state machine transitions over time,
4//! following functional programming principles.
5
6use super::state::State;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::time::Duration;
10
11/// Record of a single state transition.
12///
13/// Transitions are immutable values representing a move from one state
14/// to another at a specific point in time.
15///
16/// # Example
17///
18/// ```rust
19/// use mindset::core::{State, StateTransition};
20/// use serde::{Deserialize, Serialize};
21/// use chrono::Utc;
22///
23/// #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
24/// enum TaskState {
25/// Pending,
26/// Running,
27/// }
28///
29/// impl State for TaskState {
30/// fn name(&self) -> &str {
31/// match self {
32/// Self::Pending => "Pending",
33/// Self::Running => "Running",
34/// }
35/// }
36/// }
37///
38/// let transition = StateTransition {
39/// from: TaskState::Pending,
40/// to: TaskState::Running,
41/// timestamp: Utc::now(),
42/// attempt: 1,
43/// };
44/// ```
45#[derive(Clone, Debug, Serialize, Deserialize)]
46#[serde(bound = "")]
47pub struct StateTransition<S: State> {
48 /// The state being transitioned from
49 pub from: S,
50 /// The state being transitioned to
51 pub to: S,
52 /// When the transition occurred
53 pub timestamp: DateTime<Utc>,
54 /// The attempt number for this transition (for retry logic)
55 pub attempt: usize,
56}
57
58/// Ordered history of state transitions.
59///
60/// History is immutable - the `record` method returns a new history
61/// with the transition added, following functional programming principles.
62///
63/// # Example
64///
65/// ```rust
66/// use mindset::core::{State, StateHistory, StateTransition};
67/// use serde::{Deserialize, Serialize};
68/// use chrono::Utc;
69///
70/// #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
71/// enum WorkState {
72/// Start,
73/// Middle,
74/// End,
75/// }
76///
77/// impl State for WorkState {
78/// fn name(&self) -> &str {
79/// match self {
80/// Self::Start => "Start",
81/// Self::Middle => "Middle",
82/// Self::End => "End",
83/// }
84/// }
85/// }
86///
87/// let history = StateHistory::new();
88///
89/// let transition1 = StateTransition {
90/// from: WorkState::Start,
91/// to: WorkState::Middle,
92/// timestamp: Utc::now(),
93/// attempt: 1,
94/// };
95///
96/// let history = history.record(transition1);
97///
98/// let transition2 = StateTransition {
99/// from: WorkState::Middle,
100/// to: WorkState::End,
101/// timestamp: Utc::now(),
102/// attempt: 1,
103/// };
104///
105/// let history = history.record(transition2);
106///
107/// let path = history.get_path();
108/// assert_eq!(path.len(), 3); // Start -> Middle -> End
109/// ```
110#[derive(Clone, Debug, Serialize, Deserialize)]
111#[serde(bound = "")]
112pub struct StateHistory<S: State> {
113 transitions: Vec<StateTransition<S>>,
114}
115
116impl<S: State> Default for StateHistory<S> {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122impl<S: State> StateHistory<S> {
123 /// Create a new empty history.
124 ///
125 /// # Example
126 ///
127 /// ```rust
128 /// use mindset::core::{State, StateHistory};
129 /// use serde::{Deserialize, Serialize};
130 ///
131 /// #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
132 /// enum Status { Active }
133 ///
134 /// impl State for Status {
135 /// fn name(&self) -> &str { "Active" }
136 /// }
137 ///
138 /// let history: StateHistory<Status> = StateHistory::new();
139 /// assert_eq!(history.transitions().len(), 0);
140 /// ```
141 pub fn new() -> Self {
142 Self {
143 transitions: Vec::new(),
144 }
145 }
146
147 /// Record a transition, returning a new history.
148 ///
149 /// This is a pure function - it does not mutate the existing history
150 /// but returns a new one with the transition added.
151 ///
152 /// # Example
153 ///
154 /// ```rust
155 /// use mindset::core::{State, StateHistory, StateTransition};
156 /// use serde::{Deserialize, Serialize};
157 /// use chrono::Utc;
158 ///
159 /// #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
160 /// enum Step { A, B }
161 ///
162 /// impl State for Step {
163 /// fn name(&self) -> &str {
164 /// match self {
165 /// Self::A => "A",
166 /// Self::B => "B",
167 /// }
168 /// }
169 /// }
170 ///
171 /// let history = StateHistory::new();
172 /// let transition = StateTransition {
173 /// from: Step::A,
174 /// to: Step::B,
175 /// timestamp: Utc::now(),
176 /// attempt: 1,
177 /// };
178 ///
179 /// let new_history = history.record(transition);
180 /// assert_eq!(new_history.transitions().len(), 1);
181 /// assert_eq!(history.transitions().len(), 0); // Original unchanged
182 /// ```
183 pub fn record(&self, transition: StateTransition<S>) -> Self {
184 let mut transitions = self.transitions.clone();
185 transitions.push(transition);
186 Self { transitions }
187 }
188
189 /// Get the path of states traversed.
190 ///
191 /// Returns references to states in order: initial state, then
192 /// the `to` state of each transition.
193 ///
194 /// # Example
195 ///
196 /// ```rust
197 /// use mindset::core::{State, StateHistory, StateTransition};
198 /// use serde::{Deserialize, Serialize};
199 /// use chrono::Utc;
200 ///
201 /// #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
202 /// enum Phase { One, Two, Three }
203 ///
204 /// impl State for Phase {
205 /// fn name(&self) -> &str {
206 /// match self {
207 /// Self::One => "One",
208 /// Self::Two => "Two",
209 /// Self::Three => "Three",
210 /// }
211 /// }
212 /// }
213 ///
214 /// let mut history = StateHistory::new();
215 ///
216 /// history = history.record(StateTransition {
217 /// from: Phase::One,
218 /// to: Phase::Two,
219 /// timestamp: Utc::now(),
220 /// attempt: 1,
221 /// });
222 ///
223 /// history = history.record(StateTransition {
224 /// from: Phase::Two,
225 /// to: Phase::Three,
226 /// timestamp: Utc::now(),
227 /// attempt: 1,
228 /// });
229 ///
230 /// let path = history.get_path();
231 /// assert_eq!(path.len(), 3);
232 /// assert_eq!(path[0], &Phase::One);
233 /// assert_eq!(path[1], &Phase::Two);
234 /// assert_eq!(path[2], &Phase::Three);
235 /// ```
236 pub fn get_path(&self) -> Vec<&S> {
237 let mut path = Vec::new();
238 if let Some(first) = self.transitions.first() {
239 path.push(&first.from);
240 }
241 for transition in &self.transitions {
242 path.push(&transition.to);
243 }
244 path
245 }
246
247 /// Calculate total duration from first to last transition.
248 ///
249 /// Returns `None` if there are no transitions. Otherwise returns
250 /// the duration between the first and last transition timestamps.
251 ///
252 /// # Example
253 ///
254 /// ```rust
255 /// use mindset::core::{State, StateHistory, StateTransition};
256 /// use serde::{Deserialize, Serialize};
257 /// use chrono::Utc;
258 ///
259 /// #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
260 /// enum State1 { A, B }
261 ///
262 /// impl State for State1 {
263 /// fn name(&self) -> &str {
264 /// match self {
265 /// Self::A => "A",
266 /// Self::B => "B",
267 /// }
268 /// }
269 /// }
270 ///
271 /// let history = StateHistory::new();
272 /// assert!(history.duration().is_none());
273 ///
274 /// let start = Utc::now();
275 /// let history = history.record(StateTransition {
276 /// from: State1::A,
277 /// to: State1::B,
278 /// timestamp: start,
279 /// attempt: 1,
280 /// });
281 ///
282 /// assert!(history.duration().is_some());
283 /// ```
284 pub fn duration(&self) -> Option<Duration> {
285 if let (Some(first), Some(last)) = (self.transitions.first(), self.transitions.last()) {
286 let duration = last.timestamp.signed_duration_since(first.timestamp);
287 duration.to_std().ok()
288 } else {
289 None
290 }
291 }
292
293 /// Get all transitions.
294 ///
295 /// Returns a slice of all recorded transitions in order.
296 ///
297 /// # Example
298 ///
299 /// ```rust
300 /// use mindset::core::{State, StateHistory, StateTransition};
301 /// use serde::{Deserialize, Serialize};
302 /// use chrono::Utc;
303 ///
304 /// #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
305 /// enum MyState { X, Y }
306 ///
307 /// impl State for MyState {
308 /// fn name(&self) -> &str {
309 /// match self {
310 /// Self::X => "X",
311 /// Self::Y => "Y",
312 /// }
313 /// }
314 /// }
315 ///
316 /// let history = StateHistory::new();
317 /// let history = history.record(StateTransition {
318 /// from: MyState::X,
319 /// to: MyState::Y,
320 /// timestamp: Utc::now(),
321 /// attempt: 1,
322 /// });
323 ///
324 /// assert_eq!(history.transitions().len(), 1);
325 /// ```
326 pub fn transitions(&self) -> &[StateTransition<S>] {
327 &self.transitions
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use serde::{Deserialize, Serialize};
335
336 #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
337 enum TestState {
338 Initial,
339 Processing,
340 Complete,
341 Failed,
342 }
343
344 impl State for TestState {
345 fn name(&self) -> &str {
346 match self {
347 Self::Initial => "Initial",
348 Self::Processing => "Processing",
349 Self::Complete => "Complete",
350 Self::Failed => "Failed",
351 }
352 }
353
354 fn is_final(&self) -> bool {
355 matches!(self, Self::Complete | Self::Failed)
356 }
357
358 fn is_error(&self) -> bool {
359 matches!(self, Self::Failed)
360 }
361 }
362
363 #[test]
364 fn new_history_is_empty() {
365 let history: StateHistory<TestState> = StateHistory::new();
366 assert_eq!(history.transitions().len(), 0);
367 assert!(history.get_path().is_empty());
368 assert!(history.duration().is_none());
369 }
370
371 #[test]
372 fn record_adds_transition() {
373 let history = StateHistory::new();
374
375 let transition = StateTransition {
376 from: TestState::Initial,
377 to: TestState::Processing,
378 timestamp: Utc::now(),
379 attempt: 1,
380 };
381
382 let history = history.record(transition);
383
384 assert_eq!(history.transitions().len(), 1);
385 }
386
387 #[test]
388 fn record_is_immutable() {
389 let history = StateHistory::new();
390
391 let transition = StateTransition {
392 from: TestState::Initial,
393 to: TestState::Processing,
394 timestamp: Utc::now(),
395 attempt: 1,
396 };
397
398 let new_history = history.record(transition);
399
400 assert_eq!(history.transitions().len(), 0);
401 assert_eq!(new_history.transitions().len(), 1);
402 }
403
404 #[test]
405 fn get_path_returns_state_sequence() {
406 let mut history = StateHistory::new();
407
408 let transition1 = StateTransition {
409 from: TestState::Initial,
410 to: TestState::Processing,
411 timestamp: Utc::now(),
412 attempt: 1,
413 };
414
415 history = history.record(transition1);
416
417 let transition2 = StateTransition {
418 from: TestState::Processing,
419 to: TestState::Complete,
420 timestamp: Utc::now(),
421 attempt: 1,
422 };
423
424 history = history.record(transition2);
425
426 let path = history.get_path();
427 assert_eq!(path.len(), 3);
428 assert_eq!(path[0], &TestState::Initial);
429 assert_eq!(path[1], &TestState::Processing);
430 assert_eq!(path[2], &TestState::Complete);
431 }
432
433 #[test]
434 fn duration_calculates_elapsed_time() {
435 let history = StateHistory::new();
436 let start = Utc::now();
437
438 let transition1 = StateTransition {
439 from: TestState::Initial,
440 to: TestState::Processing,
441 timestamp: start,
442 attempt: 1,
443 };
444
445 let history = history.record(transition1);
446
447 std::thread::sleep(std::time::Duration::from_millis(10));
448
449 let transition2 = StateTransition {
450 from: TestState::Processing,
451 to: TestState::Complete,
452 timestamp: Utc::now(),
453 attempt: 1,
454 };
455
456 let history = history.record(transition2);
457
458 let duration = history.duration();
459 assert!(duration.is_some());
460 assert!(duration.unwrap() >= std::time::Duration::from_millis(10));
461 }
462
463 #[test]
464 fn history_serializes_correctly() {
465 let mut history = StateHistory::new();
466
467 let transition = StateTransition {
468 from: TestState::Initial,
469 to: TestState::Processing,
470 timestamp: Utc::now(),
471 attempt: 1,
472 };
473
474 history = history.record(transition);
475
476 let json = serde_json::to_string(&history).unwrap();
477 let deserialized: StateHistory<TestState> = serde_json::from_str(&json).unwrap();
478
479 assert_eq!(
480 history.transitions().len(),
481 deserialized.transitions().len()
482 );
483 }
484
485 #[test]
486 fn single_transition_has_duration_zero() {
487 let timestamp = Utc::now();
488
489 let transition = StateTransition {
490 from: TestState::Initial,
491 to: TestState::Processing,
492 timestamp,
493 attempt: 1,
494 };
495
496 let history = StateHistory::new().record(transition);
497
498 let duration = history.duration();
499 assert!(duration.is_some());
500 assert_eq!(duration.unwrap(), std::time::Duration::from_secs(0));
501 }
502
503 #[test]
504 fn attempt_field_is_tracked() {
505 let transition = StateTransition {
506 from: TestState::Initial,
507 to: TestState::Processing,
508 timestamp: Utc::now(),
509 attempt: 3,
510 };
511
512 assert_eq!(transition.attempt, 3);
513 }
514}