Skip to main content

backbone_bucket/domain/state_machine/
file_share_state_machine.rs

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