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}