Skip to main content

backbone_bucket/domain/state_machine/
stored_file_state_machine.rs

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