Skip to main content

backbone_bucket/domain/state_machine/
upload_session_state_machine.rs

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