1use serde::{Deserialize, Serialize};
6use std::str::FromStr;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum UploadSessionState {
12 Initiated,
14 Uploading,
15 Completing,
16 Completed,
18 Failed,
20 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 pub fn is_initial(&self) -> bool {
62 matches!(self, Self::Initiated)
63 }
64
65 pub fn is_final(&self) -> bool {
67 matches!(self, Self::Completed | Self::Failed | Self::Expired)
68 }
69
70 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum UploadSessionTransition {
87 StartUpload,
89 AddPart,
91 Complete,
93 Finish,
95 Fail,
97 Abort,
99 Expire,
101 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 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 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 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#[derive(Debug, Clone)]
186pub struct UploadSessionStateMachine {
187 current_state: UploadSessionState,
188}
189
190impl UploadSessionStateMachine {
191 pub fn new() -> Self {
193 Self {
194 current_state: UploadSessionState::default(),
195 }
196 }
197
198 pub fn from_state(state: UploadSessionState) -> Self {
200 Self { current_state: state }
201 }
202
203 pub fn current_state(&self) -> UploadSessionState {
205 self.current_state
206 }
207
208 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 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; }
240
241 allowed_roles.iter().any(|r| *r == role || *r == "*")
242 }
243
244 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 pub fn transition_with_role(&mut self, transition: UploadSessionTransition, role: &str) -> Result<UploadSessionState, StateMachineError> {
259 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 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 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 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 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 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 assert!(!available.is_empty() || sm.current_state().is_final());
359 }
360}