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