Skip to main content

backbone_core/
state_machine.rs

1//! Generic state machine infrastructure for domain state machines.
2//!
3//! Phase 0 generic base for the `state_machine.rs` generator (Category C).
4//!
5//! Every generated `{Name}StateMachine` used to re-define the same structural
6//! boilerplate. This module provides:
7//!
8//! - `StateMachineError` — shared error type (identical across all modules)
9//! - `TransitionMeta<S>` — required trait for Transition enums
10//! - `StateMachineBehavior` — trait with default impls for all mechanical methods
11//!
12//! Generated files collapse to:
13//!
14//! ```rust,ignore
15//! use backbone_core::state_machine::{
16//!     StateMachineError, StateMachineBehavior, TransitionMeta,
17//! };
18//!
19//! pub enum AgentState { PendingVerification, Active, Terminated }
20//! pub enum AgentTransition { Approve, Suspend, Terminate }
21//!
22//! impl TransitionMeta<AgentState> for AgentTransition {
23//!     fn target_state(&self) -> AgentState { ... }
24//!     fn all() -> Vec<Self> { vec![...] }
25//!     fn allowed_roles(&self) -> &'static [&'static str] { ... }
26//! }
27//!
28//! pub struct AgentStateMachine { current_state: AgentState }
29//!
30//! impl StateMachineBehavior for AgentStateMachine {
31//!     type State = AgentState;
32//!     type Transition = AgentTransition;
33//!     fn current_state(&self) -> AgentState { self.current_state }
34//!     fn set_current_state(&mut self, s: AgentState) { self.current_state = s; }
35//!     fn can_transition(&self, t: AgentTransition) -> bool { /* unique table */ }
36//! }
37//! ```
38
39// ─── StateMachineError ────────────────────────────────────────────────────────
40
41/// Shared error type for all state machines.
42///
43/// Generated modules re-export this via:
44/// `pub use backbone_core::state_machine::StateMachineError;`
45#[derive(Debug, Clone, thiserror::Error)]
46pub enum StateMachineError {
47    #[error("Invalid state: {0}")]
48    InvalidState(String),
49
50    #[error("Invalid transition: {0}")]
51    InvalidTransition(String),
52
53    #[error("Transition '{transition}' not allowed from state '{from}'")]
54    TransitionNotAllowed {
55        transition: String,
56        from: String,
57    },
58
59    #[error("Role '{role}' not authorized for transition '{transition}'")]
60    RoleNotAuthorized {
61        role: String,
62        transition: String,
63    },
64
65    #[error("Guard condition failed for transition '{0}'")]
66    GuardFailed(String),
67
68    #[error("Cannot transition from final state '{0}'")]
69    FinalStateReached(String),
70}
71
72// ─── TransitionMeta ──────────────────────────────────────────────────────────
73
74/// Required trait for Transition enums — provides the metadata that
75/// `StateMachineBehavior` default methods need.
76///
77/// Implement this on each `{Name}Transition` enum:
78///
79/// ```rust,ignore
80/// impl TransitionMeta<AgentState> for AgentTransition {
81///     fn target_state(&self) -> AgentState { match self { ... } }
82///     fn all() -> Vec<Self> { vec![...] }
83///     fn allowed_roles(&self) -> &'static [&'static str] { match self { ... } }
84/// }
85/// ```
86pub trait TransitionMeta<S>: Copy + std::fmt::Display {
87    /// The state this transition leads to.
88    fn target_state(&self) -> S;
89
90    /// All possible transitions (used by `available_transitions`).
91    fn all() -> Vec<Self>;
92
93    /// Roles allowed to fire this transition. Empty slice means unrestricted.
94    fn allowed_roles(&self) -> &'static [&'static str];
95}
96
97// ─── StateMachineBehavior ────────────────────────────────────────────────────
98
99/// Trait that provides all mechanical state machine methods as default impls.
100///
101/// Implementors only need to define three methods:
102/// - `current_state()` — read the current state field
103/// - `set_current_state()` — write the current state field
104/// - `can_transition()` — the unique per-entity transition table
105///
106/// All other methods (`transition`, `transition_with_role`,
107/// `available_transitions`, `available_transitions_for_role`,
108/// `transition_to_state`, `can_transition_with_role`) are provided for free.
109pub trait StateMachineBehavior: Sized {
110    type State: Copy + PartialEq + Default + std::fmt::Display;
111    type Transition: TransitionMeta<Self::State>;
112
113    // ── Required ──────────────────────────────────────────────────────────────
114
115    fn current_state(&self) -> Self::State;
116    fn set_current_state(&mut self, state: Self::State);
117
118    /// The unique per-entity transition table.
119    fn can_transition(&self, transition: Self::Transition) -> bool;
120
121    // ── Provided (default impls) ──────────────────────────────────────────────
122
123    fn new() -> Self where Self: Default { Self::default() }
124
125    fn from_state(state: Self::State) -> Self where Self: Default {
126        let mut sm = Self::default();
127        sm.set_current_state(state);
128        sm
129    }
130
131    /// Check if a transition is allowed for the given role.
132    fn can_transition_with_role(&self, transition: Self::Transition, role: &str) -> bool {
133        if !self.can_transition(transition) {
134            return false;
135        }
136        let allowed = transition.allowed_roles();
137        allowed.is_empty() || allowed.iter().any(|r| *r == role || *r == "*")
138    }
139
140    /// Apply a transition, returning the new state on success.
141    fn transition(&mut self, transition: Self::Transition) -> Result<Self::State, StateMachineError> {
142        if !self.can_transition(transition) {
143            return Err(StateMachineError::TransitionNotAllowed {
144                transition: transition.to_string(),
145                from: self.current_state().to_string(),
146            });
147        }
148        let next = transition.target_state();
149        self.set_current_state(next);
150        Ok(next)
151    }
152
153    /// Apply a transition with role authorization check.
154    fn transition_with_role(
155        &mut self,
156        transition: Self::Transition,
157        role: &str,
158    ) -> Result<Self::State, StateMachineError> {
159        if !self.can_transition(transition) {
160            return Err(StateMachineError::TransitionNotAllowed {
161                transition: transition.to_string(),
162                from: self.current_state().to_string(),
163            });
164        }
165        if !self.can_transition_with_role(transition, role) {
166            return Err(StateMachineError::RoleNotAuthorized {
167                role: role.to_string(),
168                transition: transition.to_string(),
169            });
170        }
171        let next = transition.target_state();
172        self.set_current_state(next);
173        Ok(next)
174    }
175
176    /// All transitions valid from the current state.
177    fn available_transitions(&self) -> Vec<Self::Transition> {
178        Self::Transition::all()
179            .into_iter()
180            .filter(|t| self.can_transition(*t))
181            .collect()
182    }
183
184    /// All transitions valid from the current state for the given role.
185    fn available_transitions_for_role(&self, role: &str) -> Vec<Self::Transition> {
186        Self::Transition::all()
187            .into_iter()
188            .filter(|t| self.can_transition_with_role(*t, role))
189            .collect()
190    }
191
192    /// Transition directly to a target state by finding any valid transition
193    /// that leads there from the current state.
194    fn transition_to_state(
195        &mut self,
196        target: Self::State,
197    ) -> Result<Self::State, StateMachineError> {
198        let valid = Self::Transition::all()
199            .into_iter()
200            .filter(|t| self.can_transition(*t))
201            .find(|t| t.target_state() == target);
202        match valid {
203            Some(t) => self.transition(t),
204            None => Err(StateMachineError::TransitionNotAllowed {
205                transition: target.to_string(),
206                from: self.current_state().to_string(),
207            }),
208        }
209    }
210}