Skip to main content

appcore_core/
state.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: state.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/05/29 20:47:35 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/06/04 11:51:29 by dnettoRaw
8//      ###########      S: 0.6.0
9// =============================================================================
10
11//! State contracts: registry plus a minimal deterministic state machine.
12
13use crate::error::{RuntimeError, RuntimeResult};
14use crate::ids::{EventName, StateName};
15use crate::registry::NameRegistry;
16
17/// Minimal runtime state contract.
18pub trait RuntimeState {
19    /// Returns the stable state name.
20    fn name(&self) -> &StateName;
21}
22
23/// Explicit state transition definition.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct StateTransition {
26    /// Source state.
27    pub from: StateName,
28    /// Event that triggers the transition.
29    pub event: EventName,
30    /// Destination state.
31    pub to: StateName,
32}
33
34/// Minimal state machine based on explicit transitions.
35#[derive(Debug, Clone)]
36pub struct StateMachine {
37    current: StateName,
38    transitions: Vec<StateTransition>,
39}
40
41impl StateMachine {
42    /// Creates a state machine without registered transitions.
43    pub fn new(initial: StateName) -> Self {
44        Self {
45            current: initial,
46            transitions: Vec::new(),
47        }
48    }
49
50    /// Returns the current state.
51    pub fn current(&self) -> &StateName {
52        &self.current
53    }
54
55    /// Registers one deterministic transition, rejecting duplicate source/event pairs.
56    pub fn add_transition(&mut self, transition: StateTransition) -> RuntimeResult<()> {
57        let duplicated = self
58            .transitions
59            .iter()
60            .any(|existing| existing.from == transition.from && existing.event == transition.event);
61        if duplicated {
62            return Err(RuntimeError::DuplicateStateTransition);
63        }
64
65        self.transitions.push(transition);
66        Ok(())
67    }
68
69    /// Reports whether `event` can be applied from the current state.
70    pub fn can_apply(&self, event: &EventName) -> bool {
71        self.transitions
72            .iter()
73            .any(|transition| transition.from == self.current && &transition.event == event)
74    }
75
76    /// Applies `event` and returns the new current state.
77    pub fn apply(&mut self, event: &EventName) -> RuntimeResult<&StateName> {
78        let transition = self
79            .transitions
80            .iter()
81            .find(|transition| transition.from == self.current && &transition.event == event);
82
83        let Some(transition) = transition else {
84            return Err(RuntimeError::InvalidStateTransition);
85        };
86
87        self.current = transition.to.clone();
88        Ok(&self.current)
89    }
90
91    /// Returns all transitions in registration order.
92    pub fn transitions(&self) -> &[StateTransition] {
93        &self.transitions
94    }
95}
96
97/// Ordered registry of declared state names.
98#[derive(Debug, Default)]
99pub struct StateRegistry {
100    names: NameRegistry<StateName>,
101}
102
103impl StateRegistry {
104    /// Creates an empty state registry.
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    /// Registers a state name, rejecting duplicates.
110    pub fn register(&mut self, name: StateName) -> RuntimeResult<()> {
111        self.names.register(name, "state")
112    }
113
114    /// Reports whether a state name is registered.
115    pub fn contains(&self, name: &StateName) -> bool {
116        self.names.contains(name)
117    }
118
119    /// Returns the number of registered state names.
120    pub fn len(&self) -> usize {
121        self.names.len()
122    }
123
124    /// Reports whether no states are registered.
125    pub fn is_empty(&self) -> bool {
126        self.names.is_empty()
127    }
128
129    /// Returns state names in registration order.
130    pub fn list(&self) -> &[StateName] {
131        self.names.list()
132    }
133}
134
135#[cfg(test)]
136#[path = "state_tests.rs"]
137mod tests;