Skip to main content

backbone_bucket/domain/state_machine/
file_lock_state_machine.rs

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