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}