use std::collections::VecDeque;
use std::fmt::Debug;
use crate::error::FinitomataError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Lifecycle {
Created,
Loaded,
Running,
Terminating,
Terminated,
}
#[derive(Debug, Clone)]
pub struct HistoryEntry<S> {
pub state: S,
pub count: usize,
}
#[derive(Debug, Clone)]
pub struct BoundedHistory<S> {
entries: VecDeque<HistoryEntry<S>>,
max_size: usize,
}
impl<S: PartialEq + Clone + Debug> BoundedHistory<S> {
pub fn new(max_size: usize) -> Self {
Self {
entries: VecDeque::with_capacity(max_size),
max_size,
}
}
pub fn push(&mut self, state: S) {
if let Some(last) = self.entries.back_mut()
&& last.state == state
{
last.count += 1;
return;
}
if self.entries.len() >= self.max_size {
self.entries.pop_front();
}
self.entries.push_back(HistoryEntry { state, count: 1 });
}
pub fn entries(&self) -> &VecDeque<HistoryEntry<S>> {
&self.entries
}
pub fn last(&self) -> Option<&HistoryEntry<S>> {
self.entries.back()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct FsmState<S: Clone + Debug, P: Clone + Debug> {
pub name: String,
pub current: S,
pub payload: P,
pub history: BoundedHistory<S>,
pub lifecycle: Lifecycle,
pub last_error: Option<FinitomataError>,
}
impl<S: Clone + Debug + PartialEq, P: Clone + Debug> FsmState<S, P> {
pub fn new(name: impl Into<String>, initial_state: S, payload: P) -> Self {
let name = name.into();
let mut history = BoundedHistory::new(32);
history.push(initial_state.clone());
Self {
name,
current: initial_state,
payload,
history,
lifecycle: Lifecycle::Created,
last_error: None,
}
}
pub fn transition_to(&mut self, new_state: S) {
self.current = new_state.clone();
self.history.push(new_state);
}
pub fn set_payload(&mut self, payload: P) {
self.payload = payload;
}
pub fn set_error(&mut self, error: FinitomataError) {
self.last_error = Some(error);
}
pub fn clear_error(&mut self) {
self.last_error = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bounded_history_collapses_repeats() {
let mut history = BoundedHistory::new(10);
history.push("idle");
history.push("idle");
history.push("idle");
assert_eq!(history.len(), 1);
assert_eq!(history.last().unwrap().count, 3);
}
#[test]
fn test_bounded_history_distinct_entries() {
let mut history = BoundedHistory::new(10);
history.push("idle");
history.push("running");
history.push("idle");
assert_eq!(history.len(), 3);
}
#[test]
fn test_bounded_history_eviction() {
let mut history = BoundedHistory::new(3);
history.push("a");
history.push("b");
history.push("c");
history.push("d");
assert_eq!(history.len(), 3);
assert_eq!(history.entries()[0].state, "b");
}
#[test]
fn test_fsm_state_new() {
let state: FsmState<&str, u32> = FsmState::new("test", "idle", 0);
assert_eq!(state.current, "idle");
assert_eq!(state.payload, 0);
assert_eq!(state.lifecycle, Lifecycle::Created);
assert!(state.last_error.is_none());
}
}