use super::state::State;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(bound = "")]
pub struct StateTransition<S: State> {
pub from: S,
pub to: S,
pub timestamp: DateTime<Utc>,
pub attempt: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(bound = "")]
pub struct StateHistory<S: State> {
transitions: Vec<StateTransition<S>>,
}
impl<S: State> Default for StateHistory<S> {
fn default() -> Self {
Self::new()
}
}
impl<S: State> StateHistory<S> {
pub fn new() -> Self {
Self {
transitions: Vec::new(),
}
}
pub fn record(&self, transition: StateTransition<S>) -> Self {
let mut transitions = self.transitions.clone();
transitions.push(transition);
Self { transitions }
}
pub fn get_path(&self) -> Vec<&S> {
let mut path = Vec::new();
if let Some(first) = self.transitions.first() {
path.push(&first.from);
}
for transition in &self.transitions {
path.push(&transition.to);
}
path
}
pub fn duration(&self) -> Option<Duration> {
if let (Some(first), Some(last)) = (self.transitions.first(), self.transitions.last()) {
let duration = last.timestamp.signed_duration_since(first.timestamp);
duration.to_std().ok()
} else {
None
}
}
pub fn transitions(&self) -> &[StateTransition<S>] {
&self.transitions
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
enum TestState {
Initial,
Processing,
Complete,
Failed,
}
impl State for TestState {
fn name(&self) -> &str {
match self {
Self::Initial => "Initial",
Self::Processing => "Processing",
Self::Complete => "Complete",
Self::Failed => "Failed",
}
}
fn is_final(&self) -> bool {
matches!(self, Self::Complete | Self::Failed)
}
fn is_error(&self) -> bool {
matches!(self, Self::Failed)
}
}
#[test]
fn new_history_is_empty() {
let history: StateHistory<TestState> = StateHistory::new();
assert_eq!(history.transitions().len(), 0);
assert!(history.get_path().is_empty());
assert!(history.duration().is_none());
}
#[test]
fn record_adds_transition() {
let history = StateHistory::new();
let transition = StateTransition {
from: TestState::Initial,
to: TestState::Processing,
timestamp: Utc::now(),
attempt: 1,
};
let history = history.record(transition);
assert_eq!(history.transitions().len(), 1);
}
#[test]
fn record_is_immutable() {
let history = StateHistory::new();
let transition = StateTransition {
from: TestState::Initial,
to: TestState::Processing,
timestamp: Utc::now(),
attempt: 1,
};
let new_history = history.record(transition);
assert_eq!(history.transitions().len(), 0);
assert_eq!(new_history.transitions().len(), 1);
}
#[test]
fn get_path_returns_state_sequence() {
let mut history = StateHistory::new();
let transition1 = StateTransition {
from: TestState::Initial,
to: TestState::Processing,
timestamp: Utc::now(),
attempt: 1,
};
history = history.record(transition1);
let transition2 = StateTransition {
from: TestState::Processing,
to: TestState::Complete,
timestamp: Utc::now(),
attempt: 1,
};
history = history.record(transition2);
let path = history.get_path();
assert_eq!(path.len(), 3);
assert_eq!(path[0], &TestState::Initial);
assert_eq!(path[1], &TestState::Processing);
assert_eq!(path[2], &TestState::Complete);
}
#[test]
fn duration_calculates_elapsed_time() {
let history = StateHistory::new();
let start = Utc::now();
let transition1 = StateTransition {
from: TestState::Initial,
to: TestState::Processing,
timestamp: start,
attempt: 1,
};
let history = history.record(transition1);
std::thread::sleep(std::time::Duration::from_millis(10));
let transition2 = StateTransition {
from: TestState::Processing,
to: TestState::Complete,
timestamp: Utc::now(),
attempt: 1,
};
let history = history.record(transition2);
let duration = history.duration();
assert!(duration.is_some());
assert!(duration.unwrap() >= std::time::Duration::from_millis(10));
}
#[test]
fn history_serializes_correctly() {
let mut history = StateHistory::new();
let transition = StateTransition {
from: TestState::Initial,
to: TestState::Processing,
timestamp: Utc::now(),
attempt: 1,
};
history = history.record(transition);
let json = serde_json::to_string(&history).unwrap();
let deserialized: StateHistory<TestState> = serde_json::from_str(&json).unwrap();
assert_eq!(
history.transitions().len(),
deserialized.transitions().len()
);
}
#[test]
fn single_transition_has_duration_zero() {
let timestamp = Utc::now();
let transition = StateTransition {
from: TestState::Initial,
to: TestState::Processing,
timestamp,
attempt: 1,
};
let history = StateHistory::new().record(transition);
let duration = history.duration();
assert!(duration.is_some());
assert_eq!(duration.unwrap(), std::time::Duration::from_secs(0));
}
#[test]
fn attempt_field_is_tracked() {
let transition = StateTransition {
from: TestState::Initial,
to: TestState::Processing,
timestamp: Utc::now(),
attempt: 3,
};
assert_eq!(transition.attempt, 3);
}
}