1use parking_lot::Mutex;
2use rskit_errors::{AppError, AppResult, ErrorCode};
3use std::collections::HashMap;
4use std::hash::Hash;
5use std::sync::Arc;
6use std::time::SystemTime;
7
8pub trait State: Clone + Send + Sync + 'static {}
10
11impl<T> State for T where T: Clone + Send + Sync + 'static {}
12
13type Guard<S, C> = Arc<dyn Fn(&S, &C) -> AppResult<()> + Send + Sync>;
14type Action<S, C> = Arc<dyn Fn(&S, &S, &C) -> AppResult<()> + Send + Sync>;
15
16pub struct Transition<S, C> {
18 name: String,
19 from: Option<S>,
20 to: S,
21 guard: Option<Guard<S, C>>,
22 action: Option<Action<S, C>>,
23}
24
25impl<S, C> Transition<S, C>
26where
27 S: State,
28{
29 #[must_use]
31 pub fn new(name: impl Into<String>, to: S) -> Self {
32 Self {
33 name: name.into(),
34 from: None,
35 to,
36 guard: None,
37 action: None,
38 }
39 }
40
41 #[must_use]
43 pub fn from(mut self, state: S) -> Self {
44 self.from = Some(state);
45 self
46 }
47
48 #[must_use]
50 pub fn with_guard(
51 mut self,
52 guard: impl Fn(&S, &C) -> AppResult<()> + Send + Sync + 'static,
53 ) -> Self {
54 self.guard = Some(Arc::new(guard));
55 self
56 }
57
58 #[must_use]
62 pub fn with_action(
63 mut self,
64 action: impl Fn(&S, &S, &C) -> AppResult<()> + Send + Sync + 'static,
65 ) -> Self {
66 self.action = Some(Arc::new(action));
67 self
68 }
69
70 #[must_use]
72 pub fn name(&self) -> &str {
73 &self.name
74 }
75}
76
77#[derive(Debug, Clone)]
79pub struct StateSnapshot<S> {
80 pub state: S,
82 pub version: u64,
84}
85
86#[derive(Debug, Clone)]
88pub struct AuditEntry<S, C> {
89 pub transition: String,
91 pub from: S,
93 pub to: S,
95 pub context: C,
97 pub version: u64,
99 pub recorded_at: SystemTime,
101}
102
103pub trait StatePersistence<S, C>: Send + Sync
105where
106 S: State,
107{
108 fn persist(&self, snapshot: &StateSnapshot<S>, audit: &AuditEntry<S, C>) -> AppResult<()>;
110}
111
112struct MachineState<S, C> {
113 current: S,
114 version: u64,
115 audit_log: Vec<AuditEntry<S, C>>,
116}
117
118pub struct StateMachine<S, C> {
120 state: Mutex<MachineState<S, C>>,
121 transitions: HashMap<String, Transition<S, C>>,
122 persistence: Vec<Arc<dyn StatePersistence<S, C>>>,
123}
124
125impl<S, C> StateMachine<S, C>
126where
127 S: State + Eq + Hash,
128 C: Clone + Send + Sync + 'static,
129{
130 #[must_use]
132 pub fn new(initial: S) -> Self {
133 Self {
134 state: Mutex::new(MachineState {
135 current: initial,
136 version: 0,
137 audit_log: Vec::new(),
138 }),
139 transitions: HashMap::new(),
140 persistence: Vec::new(),
141 }
142 }
143
144 #[must_use]
146 pub fn with_transition(mut self, transition: Transition<S, C>) -> Self {
147 self.transitions.insert(transition.name.clone(), transition);
148 self
149 }
150
151 #[must_use]
153 pub fn with_persistence(mut self, persistence: Arc<dyn StatePersistence<S, C>>) -> Self {
154 self.persistence.push(persistence);
155 self
156 }
157
158 #[must_use]
160 pub fn state(&self) -> S {
161 self.state.lock().current.clone()
162 }
163
164 #[must_use]
166 pub fn snapshot(&self) -> StateSnapshot<S> {
167 let state = self.state.lock();
168 StateSnapshot {
169 state: state.current.clone(),
170 version: state.version,
171 }
172 }
173
174 #[must_use]
176 pub fn audit_log(&self) -> Vec<AuditEntry<S, C>> {
177 self.state.lock().audit_log.clone()
178 }
179
180 pub fn apply(&self, name: &str, context: C) -> AppResult<StateSnapshot<S>> {
182 let transition = self.transitions.get(name).ok_or_else(|| {
183 AppError::new(
184 ErrorCode::InvalidInput,
185 format!("state transition '{name}' is not registered"),
186 )
187 })?;
188
189 let mut state = self.state.lock();
190 let from = state.current.clone();
191 if let Some(required) = &transition.from
192 && required != &from
193 {
194 return Err(AppError::new(
195 ErrorCode::InvalidInput,
196 format!("transition '{name}' cannot be applied from current state"),
197 ));
198 }
199 if let Some(guard) = &transition.guard {
200 guard(&from, &context)?;
201 }
202
203 let to = transition.to.clone();
204 let next_version = state.version + 1;
205 let snapshot = StateSnapshot {
206 state: to.clone(),
207 version: next_version,
208 };
209
210 if let Some(action) = &transition.action {
211 action(&from, &to, &context)?;
212 }
213
214 let audit = AuditEntry {
215 transition: transition.name.clone(),
216 from,
217 to: to.clone(),
218 context,
219 version: snapshot.version,
220 recorded_at: SystemTime::now(),
221 };
222
223 for persistence in &self.persistence {
224 persistence.persist(&snapshot, &audit)?;
225 }
226 state.current = to;
227 state.version = next_version;
228 state.audit_log.push(audit);
229
230 Ok(snapshot)
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use std::sync::atomic::{AtomicUsize, Ordering};
238
239 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
240 enum OrderState {
241 Draft,
242 Submitted,
243 }
244
245 struct CountingPersistence(AtomicUsize);
246 struct FailingPersistence;
247
248 impl StatePersistence<OrderState, bool> for CountingPersistence {
249 fn persist(
250 &self,
251 _snapshot: &StateSnapshot<OrderState>,
252 _audit: &AuditEntry<OrderState, bool>,
253 ) -> AppResult<()> {
254 self.0.fetch_add(1, Ordering::SeqCst);
255 Ok(())
256 }
257 }
258
259 impl StatePersistence<OrderState, bool> for FailingPersistence {
260 fn persist(
261 &self,
262 _snapshot: &StateSnapshot<OrderState>,
263 _audit: &AuditEntry<OrderState, bool>,
264 ) -> AppResult<()> {
265 Err(AppError::new(ErrorCode::Internal, "persist failed"))
266 }
267 }
268
269 #[test]
270 fn guarded_transition_updates_state_and_audit() {
271 let persistence = Arc::new(CountingPersistence(AtomicUsize::new(0)));
272 let machine = StateMachine::new(OrderState::Draft)
273 .with_transition(
274 Transition::new("submit", OrderState::Submitted)
275 .from(OrderState::Draft)
276 .with_guard(|_, allowed| {
277 if *allowed {
278 Ok(())
279 } else {
280 Err(AppError::new(ErrorCode::InvalidInput, "not allowed"))
281 }
282 }),
283 )
284 .with_persistence(persistence.clone());
285
286 let snapshot = machine.apply("submit", true).unwrap();
287 assert_eq!(snapshot.state, OrderState::Submitted);
288 assert_eq!(snapshot.version, 1);
289 assert_eq!(machine.audit_log().len(), 1);
290 assert_eq!(persistence.0.load(Ordering::SeqCst), 1);
291 }
292
293 #[test]
294 fn action_failure_does_not_change_state_or_audit() {
295 let machine = StateMachine::new(OrderState::Draft).with_transition(
296 Transition::new("submit", OrderState::Submitted)
297 .from(OrderState::Draft)
298 .with_action(|_, _, _| Err(AppError::new(ErrorCode::Internal, "action failed"))),
299 );
300
301 let error = machine.apply("submit", true).unwrap_err();
302
303 assert_eq!(error.code(), ErrorCode::Internal);
304 assert_eq!(machine.state(), OrderState::Draft);
305 assert_eq!(machine.snapshot().version, 0);
306 assert!(machine.audit_log().is_empty());
307 }
308
309 #[test]
310 fn persistence_failure_does_not_change_state_or_audit() {
311 let machine = StateMachine::new(OrderState::Draft)
312 .with_transition(
313 Transition::new("submit", OrderState::Submitted).from(OrderState::Draft),
314 )
315 .with_persistence(Arc::new(FailingPersistence));
316
317 let error = machine.apply("submit", true).unwrap_err();
318
319 assert_eq!(error.code(), ErrorCode::Internal);
320 assert_eq!(machine.state(), OrderState::Draft);
321 assert_eq!(machine.snapshot().version, 0);
322 assert!(machine.audit_log().is_empty());
323 }
324}