backbone_bucket/domain/state_machine/
file_share_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 FileShareState {
12 Active,
14 Inactive,
16 Expired,
18 Exhausted,
20 Revoked,
22}
23
24impl Default for FileShareState {
25 fn default() -> Self {
26 Self::Active
27 }
28}
29
30impl std::fmt::Display for FileShareState {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 Self::Active => write!(f, "active"),
34 Self::Inactive => write!(f, "inactive"),
35 Self::Expired => write!(f, "expired"),
36 Self::Exhausted => write!(f, "exhausted"),
37 Self::Revoked => write!(f, "revoked"),
38 }
39 }
40}
41
42impl FromStr for FileShareState {
43 type Err = StateMachineError;
44
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 match s.to_lowercase().as_str() {
47 "active" => Ok(Self::Active),
48 "inactive" => Ok(Self::Inactive),
49 "expired" => Ok(Self::Expired),
50 "exhausted" => Ok(Self::Exhausted),
51 "revoked" => Ok(Self::Revoked),
52 _ => Err(StateMachineError::InvalidState(s.to_string())),
53 }
54 }
55}
56
57impl FileShareState {
58 pub fn is_initial(&self) -> bool {
60 matches!(self, Self::Active)
61 }
62
63 pub fn is_final(&self) -> bool {
65 matches!(self, Self::Inactive | Self::Expired | Self::Exhausted | Self::Revoked)
66 }
67
68 pub fn all() -> Vec<Self> {
70 vec![
71 Self::Active,
72 Self::Inactive,
73 Self::Expired,
74 Self::Exhausted,
75 Self::Revoked,
76 ]
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum FileShareTransition {
84 Expire,
86 Exhaust,
88 Revoke,
90}
91
92impl std::fmt::Display for FileShareTransition {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 match self {
95 Self::Expire => write!(f, "expire"),
96 Self::Exhaust => write!(f, "exhaust"),
97 Self::Revoke => write!(f, "revoke"),
98 }
99 }
100}
101
102impl FromStr for FileShareTransition {
103 type Err = StateMachineError;
104
105 fn from_str(s: &str) -> Result<Self, Self::Err> {
106 match s.to_lowercase().as_str() {
107 "expire" => Ok(Self::Expire),
108 "exhaust" => Ok(Self::Exhaust),
109 "revoke" => Ok(Self::Revoke),
110 _ => Err(StateMachineError::InvalidTransition(s.to_string())),
111 }
112 }
113}
114
115impl FileShareTransition {
116 pub fn target_state(&self) -> FileShareState {
118 match self {
119 Self::Expire => FileShareState::Expired,
120 Self::Exhaust => FileShareState::Exhausted,
121 Self::Revoke => FileShareState::Revoked,
122 }
123 }
124
125 pub fn all() -> Vec<Self> {
127 vec![
128 Self::Expire,
129 Self::Exhaust,
130 Self::Revoke,
131 ]
132 }
133
134 pub fn allowed_roles(&self) -> &'static [&'static str] {
136 match self {
137 Self::Expire => &["system"],
138 Self::Exhaust => &["system"],
139 Self::Revoke => &["admin", "owner"],
140 }
141 }
142}
143
144use super::StateMachineError;
145
146#[derive(Debug, Clone)]
148pub struct FileShareStateMachine {
149 current_state: FileShareState,
150}
151
152impl FileShareStateMachine {
153 pub fn new() -> Self {
155 Self {
156 current_state: FileShareState::default(),
157 }
158 }
159
160 pub fn from_state(state: FileShareState) -> Self {
162 Self { current_state: state }
163 }
164
165 pub fn current_state(&self) -> FileShareState {
167 self.current_state
168 }
169
170 pub fn can_transition(&self, transition: FileShareTransition) -> bool {
172 if matches!(self.current_state, FileShareState::Inactive | FileShareState::Expired | FileShareState::Exhausted | FileShareState::Revoked) {
173 return false;
174 }
175
176 match (self.current_state, transition) {
177 (FileShareState::Active, FileShareTransition::Expire) => true,
178 (FileShareState::Active, FileShareTransition::Exhaust) => true,
179 (FileShareState::Active, FileShareTransition::Revoke) => true,
180 _ => false,
181 }
182 }
183
184 pub fn can_transition_with_role(&self, transition: FileShareTransition, role: &str) -> bool {
186 if !self.can_transition(transition) {
187 return false;
188 }
189
190 let allowed_roles = transition.allowed_roles();
191 if allowed_roles.is_empty() {
192 return true; }
194
195 allowed_roles.iter().any(|r| *r == role || *r == "*")
196 }
197
198 pub fn transition(&mut self, transition: FileShareTransition) -> Result<FileShareState, StateMachineError> {
200 if !self.can_transition(transition) {
201 return Err(StateMachineError::TransitionNotAllowed {
202 transition: transition.to_string(),
203 from: self.current_state.to_string(),
204 });
205 }
206
207 self.current_state = transition.target_state();
208 Ok(self.current_state)
209 }
210
211 pub fn transition_with_role(&mut self, transition: FileShareTransition, role: &str) -> Result<FileShareState, StateMachineError> {
213 if !self.can_transition(transition) {
215 return Err(StateMachineError::TransitionNotAllowed {
216 transition: transition.to_string(),
217 from: self.current_state.to_string(),
218 });
219 }
220
221 if !self.can_transition_with_role(transition, role) {
223 return Err(StateMachineError::RoleNotAuthorized {
224 role: role.to_string(),
225 transition: transition.to_string(),
226 });
227 }
228
229 self.current_state = transition.target_state();
230 Ok(self.current_state)
231 }
232
233 pub fn available_transitions(&self) -> Vec<FileShareTransition> {
235 FileShareTransition::all()
236 .into_iter()
237 .filter(|t| self.can_transition(*t))
238 .collect()
239 }
240
241 pub fn available_transitions_for_role(&self, role: &str) -> Vec<FileShareTransition> {
243 FileShareTransition::all()
244 .into_iter()
245 .filter(|t| self.can_transition_with_role(*t, role))
246 .collect()
247 }
248
249 pub fn transition_to_state(&mut self, target: FileShareState) -> Result<FileShareState, StateMachineError> {
254 let valid = FileShareTransition::all().into_iter()
255 .filter(|t| self.can_transition(*t))
256 .find(|t| t.target_state() == target);
257 match valid {
258 Some(t) => self.transition(t),
259 None => Err(StateMachineError::TransitionNotAllowed {
260 transition: target.to_string(),
261 from: self.current_state.to_string(),
262 }),
263 }
264 }
265}
266
267impl Default for FileShareStateMachine {
268 fn default() -> Self {
269 Self::new()
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_initial_state() {
279 let sm = FileShareStateMachine::new();
280 assert_eq!(sm.current_state(), FileShareState::Active);
281 assert!(sm.current_state().is_initial());
282 }
283
284 #[test]
285 fn test_valid_transition() {
286 let mut sm = FileShareStateMachine::from_state(FileShareState::Active);
287 assert!(sm.can_transition(FileShareTransition::Expire));
288 let result = sm.transition(FileShareTransition::Expire);
289 assert!(result.is_ok());
290 assert_eq!(sm.current_state(), FileShareState::Expired);
291 }
292
293 #[test]
294 fn test_invalid_transition() {
295 let mut sm = FileShareStateMachine::from_state(FileShareState::Inactive);
296 let result = sm.transition(FileShareTransition::Expire);
298 assert!(result.is_err());
299 }
300
301 #[test]
302 fn test_state_parsing() {
303 let state: FileShareState = "active".parse().unwrap();
304 assert_eq!(state, FileShareState::Active);
305 }
306
307 #[test]
308 fn test_available_transitions() {
309 let sm = FileShareStateMachine::new();
310 let available = sm.available_transitions();
311 assert!(!available.is_empty() || sm.current_state().is_final());
313 }
314}