backbone_bucket/domain/state_machine/
bucket_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 BucketState {
12 Active,
14 Readonly,
15 Archived,
16 Deleted,
18}
19
20impl Default for BucketState {
21 fn default() -> Self {
22 Self::Active
23 }
24}
25
26impl std::fmt::Display for BucketState {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 Self::Active => write!(f, "active"),
30 Self::Readonly => write!(f, "readonly"),
31 Self::Archived => write!(f, "archived"),
32 Self::Deleted => write!(f, "deleted"),
33 }
34 }
35}
36
37impl FromStr for BucketState {
38 type Err = StateMachineError;
39
40 fn from_str(s: &str) -> Result<Self, Self::Err> {
41 match s.to_lowercase().as_str() {
42 "active" => Ok(Self::Active),
43 "readonly" => Ok(Self::Readonly),
44 "archived" => Ok(Self::Archived),
45 "deleted" => Ok(Self::Deleted),
46 _ => Err(StateMachineError::InvalidState(s.to_string())),
47 }
48 }
49}
50
51impl BucketState {
52 pub fn is_initial(&self) -> bool {
54 matches!(self, Self::Active)
55 }
56
57 pub fn is_final(&self) -> bool {
59 matches!(self, Self::Deleted)
60 }
61
62 pub fn all() -> Vec<Self> {
64 vec![
65 Self::Active,
66 Self::Readonly,
67 Self::Archived,
68 Self::Deleted,
69 ]
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum BucketTransition {
77 Lock,
79 Unlock,
81 Archive,
83 Restore,
85 Delete,
87}
88
89impl std::fmt::Display for BucketTransition {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 Self::Lock => write!(f, "lock"),
93 Self::Unlock => write!(f, "unlock"),
94 Self::Archive => write!(f, "archive"),
95 Self::Restore => write!(f, "restore"),
96 Self::Delete => write!(f, "delete"),
97 }
98 }
99}
100
101impl FromStr for BucketTransition {
102 type Err = StateMachineError;
103
104 fn from_str(s: &str) -> Result<Self, Self::Err> {
105 match s.to_lowercase().as_str() {
106 "lock" => Ok(Self::Lock),
107 "unlock" => Ok(Self::Unlock),
108 "archive" => Ok(Self::Archive),
109 "restore" => Ok(Self::Restore),
110 "delete" => Ok(Self::Delete),
111 _ => Err(StateMachineError::InvalidTransition(s.to_string())),
112 }
113 }
114}
115
116impl BucketTransition {
117 pub fn target_state(&self) -> BucketState {
119 match self {
120 Self::Lock => BucketState::Readonly,
121 Self::Unlock => BucketState::Active,
122 Self::Archive => BucketState::Archived,
123 Self::Restore => BucketState::Active,
124 Self::Delete => BucketState::Deleted,
125 }
126 }
127
128 pub fn all() -> Vec<Self> {
130 vec![
131 Self::Lock,
132 Self::Unlock,
133 Self::Archive,
134 Self::Restore,
135 Self::Delete,
136 ]
137 }
138
139 pub fn allowed_roles(&self) -> &'static [&'static str] {
141 match self {
142 Self::Lock => &["admin", "owner"],
143 Self::Unlock => &["admin", "owner"],
144 Self::Archive => &["admin", "owner"],
145 Self::Restore => &["admin", "owner"],
146 Self::Delete => &["admin", "owner"],
147 }
148 }
149}
150
151use super::StateMachineError;
152
153#[derive(Debug, Clone)]
155pub struct BucketStateMachine {
156 current_state: BucketState,
157}
158
159impl BucketStateMachine {
160 pub fn new() -> Self {
162 Self {
163 current_state: BucketState::default(),
164 }
165 }
166
167 pub fn from_state(state: BucketState) -> Self {
169 Self { current_state: state }
170 }
171
172 pub fn current_state(&self) -> BucketState {
174 self.current_state
175 }
176
177 pub fn can_transition(&self, transition: BucketTransition) -> bool {
179 if matches!(self.current_state, BucketState::Deleted) {
180 return false;
181 }
182
183 match (self.current_state, transition) {
184 (BucketState::Active, BucketTransition::Lock) => true,
185 (BucketState::Readonly, BucketTransition::Unlock) => true,
186 (BucketState::Active, BucketTransition::Archive) => true,
187 (BucketState::Readonly, BucketTransition::Archive) => true,
188 (BucketState::Archived, BucketTransition::Restore) => true,
189 (BucketState::Active, BucketTransition::Delete) => true,
190 (BucketState::Readonly, BucketTransition::Delete) => true,
191 (BucketState::Archived, BucketTransition::Delete) => true,
192 _ => false,
193 }
194 }
195
196 pub fn can_transition_with_role(&self, transition: BucketTransition, role: &str) -> bool {
198 if !self.can_transition(transition) {
199 return false;
200 }
201
202 let allowed_roles = transition.allowed_roles();
203 if allowed_roles.is_empty() {
204 return true; }
206
207 allowed_roles.iter().any(|r| *r == role || *r == "*")
208 }
209
210 pub fn transition(&mut self, transition: BucketTransition) -> Result<BucketState, StateMachineError> {
212 if !self.can_transition(transition) {
213 return Err(StateMachineError::TransitionNotAllowed {
214 transition: transition.to_string(),
215 from: self.current_state.to_string(),
216 });
217 }
218
219 self.current_state = transition.target_state();
220 Ok(self.current_state)
221 }
222
223 pub fn transition_with_role(&mut self, transition: BucketTransition, role: &str) -> Result<BucketState, StateMachineError> {
225 if !self.can_transition(transition) {
227 return Err(StateMachineError::TransitionNotAllowed {
228 transition: transition.to_string(),
229 from: self.current_state.to_string(),
230 });
231 }
232
233 if !self.can_transition_with_role(transition, role) {
235 return Err(StateMachineError::RoleNotAuthorized {
236 role: role.to_string(),
237 transition: transition.to_string(),
238 });
239 }
240
241 self.current_state = transition.target_state();
242 Ok(self.current_state)
243 }
244
245 pub fn available_transitions(&self) -> Vec<BucketTransition> {
247 BucketTransition::all()
248 .into_iter()
249 .filter(|t| self.can_transition(*t))
250 .collect()
251 }
252
253 pub fn available_transitions_for_role(&self, role: &str) -> Vec<BucketTransition> {
255 BucketTransition::all()
256 .into_iter()
257 .filter(|t| self.can_transition_with_role(*t, role))
258 .collect()
259 }
260
261 pub fn transition_to_state(&mut self, target: BucketState) -> Result<BucketState, StateMachineError> {
266 let valid = BucketTransition::all().into_iter()
267 .filter(|t| self.can_transition(*t))
268 .find(|t| t.target_state() == target);
269 match valid {
270 Some(t) => self.transition(t),
271 None => Err(StateMachineError::TransitionNotAllowed {
272 transition: target.to_string(),
273 from: self.current_state.to_string(),
274 }),
275 }
276 }
277}
278
279impl Default for BucketStateMachine {
280 fn default() -> Self {
281 Self::new()
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_initial_state() {
291 let sm = BucketStateMachine::new();
292 assert_eq!(sm.current_state(), BucketState::Active);
293 assert!(sm.current_state().is_initial());
294 }
295
296 #[test]
297 fn test_valid_transition() {
298 let mut sm = BucketStateMachine::from_state(BucketState::Active);
299 assert!(sm.can_transition(BucketTransition::Lock));
300 let result = sm.transition(BucketTransition::Lock);
301 assert!(result.is_ok());
302 assert_eq!(sm.current_state(), BucketState::Readonly);
303 }
304
305 #[test]
306 fn test_invalid_transition() {
307 let mut sm = BucketStateMachine::from_state(BucketState::Active);
308 let result = sm.transition(BucketTransition::Unlock);
310 assert!(result.is_err());
311 }
312
313 #[test]
314 fn test_state_parsing() {
315 let state: BucketState = "active".parse().unwrap();
316 assert_eq!(state, BucketState::Active);
317 }
318
319 #[test]
320 fn test_available_transitions() {
321 let sm = BucketStateMachine::new();
322 let available = sm.available_transitions();
323 assert!(!available.is_empty() || sm.current_state().is_final());
325 }
326}