backbone_bucket/domain/state_machine/
stored_file_state_machine.rs1use 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 StoredFileState {
12 Uploading,
14 Processing,
15 Active,
16 Quarantined,
17 Deleted,
18 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 pub fn is_initial(&self) -> bool {
60 matches!(self, Self::Uploading)
61 }
62
63 pub fn is_final(&self) -> bool {
65 matches!(self, Self::Purged)
66 }
67
68 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum StoredFileTransition {
85 CompleteUpload,
87 MarkSafe,
89 Quarantine,
91 MarkSafeByAdmin,
93 SoftDelete,
95 Restore,
97 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 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 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 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#[derive(Debug, Clone)]
177pub struct StoredFileStateMachine {
178 current_state: StoredFileState,
179}
180
181impl StoredFileStateMachine {
182 pub fn new() -> Self {
184 Self {
185 current_state: StoredFileState::default(),
186 }
187 }
188
189 pub fn from_state(state: StoredFileState) -> Self {
191 Self { current_state: state }
192 }
193
194 pub fn current_state(&self) -> StoredFileState {
196 self.current_state
197 }
198
199 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 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; }
228
229 allowed_roles.iter().any(|r| *r == role || *r == "*")
230 }
231
232 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 pub fn transition_with_role(&mut self, transition: StoredFileTransition, role: &str) -> Result<StoredFileState, StateMachineError> {
247 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 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 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 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 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 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 assert!(!available.is_empty() || sm.current_state().is_final());
347 }
348}