Skip to main content

backbone_bucket/domain/state_machine/
bucket_state_machine.rs

1//! State machine for Bucket workflow
2//!
3//! Generated by metaphor-schema
4
5use serde::{Deserialize, Serialize};
6use std::str::FromStr;
7
8/// States for Bucket workflow
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum BucketState {
12    /// Initial state
13    Active,
14    Readonly,
15    Archived,
16    /// Final state
17    Deleted,
18}
19
20impl Default for BucketState {
21    fn default() -> Self {
22        Self::Active
23    }
24}
25
26impl std::fmt::Display for BucketState {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            Self::Active => write!(f, "active"),
30            Self::Readonly => write!(f, "readonly"),
31            Self::Archived => write!(f, "archived"),
32            Self::Deleted => write!(f, "deleted"),
33        }
34    }
35}
36
37impl FromStr for BucketState {
38    type Err = StateMachineError;
39
40    fn from_str(s: &str) -> Result<Self, Self::Err> {
41        match s.to_lowercase().as_str() {
42            "active" => Ok(Self::Active),
43            "readonly" => Ok(Self::Readonly),
44            "archived" => Ok(Self::Archived),
45            "deleted" => Ok(Self::Deleted),
46            _ => Err(StateMachineError::InvalidState(s.to_string())),
47        }
48    }
49}
50
51impl BucketState {
52    /// Check if this is the initial state
53    pub fn is_initial(&self) -> bool {
54        matches!(self, Self::Active)
55    }
56
57    /// Check if this is a final state
58    pub fn is_final(&self) -> bool {
59        matches!(self, Self::Deleted)
60    }
61
62    /// Get all possible states
63    pub fn all() -> Vec<Self> {
64        vec![
65            Self::Active,
66            Self::Readonly,
67            Self::Archived,
68            Self::Deleted,
69        ]
70    }
71}
72
73/// Transitions for Bucket workflow
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum BucketTransition {
77    /// active -> readonly
78    Lock,
79    /// readonly -> active
80    Unlock,
81    /// active, readonly -> archived
82    Archive,
83    /// archived -> active
84    Restore,
85    /// active, readonly, archived -> deleted
86    Delete,
87}
88
89impl std::fmt::Display for BucketTransition {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match self {
92            Self::Lock => write!(f, "lock"),
93            Self::Unlock => write!(f, "unlock"),
94            Self::Archive => write!(f, "archive"),
95            Self::Restore => write!(f, "restore"),
96            Self::Delete => write!(f, "delete"),
97        }
98    }
99}
100
101impl FromStr for BucketTransition {
102    type Err = StateMachineError;
103
104    fn from_str(s: &str) -> Result<Self, Self::Err> {
105        match s.to_lowercase().as_str() {
106            "lock" => Ok(Self::Lock),
107            "unlock" => Ok(Self::Unlock),
108            "archive" => Ok(Self::Archive),
109            "restore" => Ok(Self::Restore),
110            "delete" => Ok(Self::Delete),
111            _ => Err(StateMachineError::InvalidTransition(s.to_string())),
112        }
113    }
114}
115
116impl BucketTransition {
117    /// Get the target state of this transition
118    pub fn target_state(&self) -> BucketState {
119        match self {
120            Self::Lock => BucketState::Readonly,
121            Self::Unlock => BucketState::Active,
122            Self::Archive => BucketState::Archived,
123            Self::Restore => BucketState::Active,
124            Self::Delete => BucketState::Deleted,
125        }
126    }
127
128    /// Get all transitions
129    pub fn all() -> Vec<Self> {
130        vec![
131            Self::Lock,
132            Self::Unlock,
133            Self::Archive,
134            Self::Restore,
135            Self::Delete,
136        ]
137    }
138
139    /// Get allowed roles for this transition
140    pub fn allowed_roles(&self) -> &'static [&'static str] {
141        match self {
142            Self::Lock => &["admin", "owner"],
143            Self::Unlock => &["admin", "owner"],
144            Self::Archive => &["admin", "owner"],
145            Self::Restore => &["admin", "owner"],
146            Self::Delete => &["admin", "owner"],
147        }
148    }
149}
150
151use super::StateMachineError;
152
153/// State machine for Bucket workflow
154#[derive(Debug, Clone)]
155pub struct BucketStateMachine {
156    current_state: BucketState,
157}
158
159impl BucketStateMachine {
160    /// Create a new state machine with initial state
161    pub fn new() -> Self {
162        Self {
163            current_state: BucketState::default(),
164        }
165    }
166
167    /// Create from an existing state
168    pub fn from_state(state: BucketState) -> Self {
169        Self { current_state: state }
170    }
171
172    /// Get the current state
173    pub fn current_state(&self) -> BucketState {
174        self.current_state
175    }
176
177    /// Check if a transition is allowed from the current state
178    pub fn can_transition(&self, transition: BucketTransition) -> bool {
179        if matches!(self.current_state, BucketState::Deleted) {
180            return false;
181        }
182
183        match (self.current_state, transition) {
184            (BucketState::Active, BucketTransition::Lock) => true,
185            (BucketState::Readonly, BucketTransition::Unlock) => true,
186            (BucketState::Active, BucketTransition::Archive) => true,
187            (BucketState::Readonly, BucketTransition::Archive) => true,
188            (BucketState::Archived, BucketTransition::Restore) => true,
189            (BucketState::Active, BucketTransition::Delete) => true,
190            (BucketState::Readonly, BucketTransition::Delete) => true,
191            (BucketState::Archived, BucketTransition::Delete) => true,
192            _ => false,
193        }
194    }
195
196    /// Check if a transition is allowed for a given role
197    pub fn can_transition_with_role(&self, transition: BucketTransition, role: &str) -> bool {
198        if !self.can_transition(transition) {
199            return false;
200        }
201
202        let allowed_roles = transition.allowed_roles();
203        if allowed_roles.is_empty() {
204            return true; // No role restriction
205        }
206
207        allowed_roles.iter().any(|r| *r == role || *r == "*")
208    }
209
210    /// Apply a transition, returning the new state
211    pub fn transition(&mut self, transition: BucketTransition) -> Result<BucketState, StateMachineError> {
212        if !self.can_transition(transition) {
213            return Err(StateMachineError::TransitionNotAllowed {
214                transition: transition.to_string(),
215                from: self.current_state.to_string(),
216            });
217        }
218
219        self.current_state = transition.target_state();
220        Ok(self.current_state)
221    }
222
223    /// Apply a transition with role check
224    pub fn transition_with_role(&mut self, transition: BucketTransition, role: &str) -> Result<BucketState, StateMachineError> {
225        // Check basic transition validity first
226        if !self.can_transition(transition) {
227            return Err(StateMachineError::TransitionNotAllowed {
228                transition: transition.to_string(),
229                from: self.current_state.to_string(),
230            });
231        }
232
233        // Check role authorization
234        if !self.can_transition_with_role(transition, role) {
235            return Err(StateMachineError::RoleNotAuthorized {
236                role: role.to_string(),
237                transition: transition.to_string(),
238            });
239        }
240
241        self.current_state = transition.target_state();
242        Ok(self.current_state)
243    }
244
245    /// Get all available transitions from the current state
246    pub fn available_transitions(&self) -> Vec<BucketTransition> {
247        BucketTransition::all()
248            .into_iter()
249            .filter(|t| self.can_transition(*t))
250            .collect()
251    }
252
253    /// Get all available transitions for a given role
254    pub fn available_transitions_for_role(&self, role: &str) -> Vec<BucketTransition> {
255        BucketTransition::all()
256            .into_iter()
257            .filter(|t| self.can_transition_with_role(*t, role))
258            .collect()
259    }
260
261    /// Attempt to transition directly to a target state.
262    ///
263    /// Finds any transition that leads to the target state and applies it.
264    /// Returns Err if no valid transition leads from current state to target.
265    pub fn transition_to_state(&mut self, target: BucketState) -> Result<BucketState, StateMachineError> {
266        let valid = BucketTransition::all().into_iter()
267            .filter(|t| self.can_transition(*t))
268            .find(|t| t.target_state() == target);
269        match valid {
270            Some(t) => self.transition(t),
271            None => Err(StateMachineError::TransitionNotAllowed {
272                transition: target.to_string(),
273                from: self.current_state.to_string(),
274            }),
275        }
276    }
277}
278
279impl Default for BucketStateMachine {
280    fn default() -> Self {
281        Self::new()
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_initial_state() {
291        let sm = BucketStateMachine::new();
292        assert_eq!(sm.current_state(), BucketState::Active);
293        assert!(sm.current_state().is_initial());
294    }
295
296    #[test]
297    fn test_valid_transition() {
298        let mut sm = BucketStateMachine::from_state(BucketState::Active);
299        assert!(sm.can_transition(BucketTransition::Lock));
300        let result = sm.transition(BucketTransition::Lock);
301        assert!(result.is_ok());
302        assert_eq!(sm.current_state(), BucketState::Readonly);
303    }
304
305    #[test]
306    fn test_invalid_transition() {
307        let mut sm = BucketStateMachine::from_state(BucketState::Active);
308        // Unlock is not valid from Active state
309        let result = sm.transition(BucketTransition::Unlock);
310        assert!(result.is_err());
311    }
312
313    #[test]
314    fn test_state_parsing() {
315        let state: BucketState = "active".parse().unwrap();
316        assert_eq!(state, BucketState::Active);
317    }
318
319    #[test]
320    fn test_available_transitions() {
321        let sm = BucketStateMachine::new();
322        let available = sm.available_transitions();
323        // Should have at least some transitions available from initial state
324        assert!(!available.is_empty() || sm.current_state().is_final());
325    }
326}