backbone_bucket/domain/state_machine/
user_quota_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 UserQuotaState {
12 Normal,
15 Exceeded,
17}
18
19impl Default for UserQuotaState {
20 fn default() -> Self {
21 Self::Normal
22 }
23}
24
25impl std::fmt::Display for UserQuotaState {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 Self::Normal => write!(f, "normal"),
29 Self::Exceeded => write!(f, "exceeded"),
30 }
31 }
32}
33
34impl FromStr for UserQuotaState {
35 type Err = StateMachineError;
36
37 fn from_str(s: &str) -> Result<Self, Self::Err> {
38 match s.to_lowercase().as_str() {
39 "normal" => Ok(Self::Normal),
40 "exceeded" => Ok(Self::Exceeded),
41 _ => Err(StateMachineError::InvalidState(s.to_string())),
42 }
43 }
44}
45
46impl UserQuotaState {
47 pub fn is_initial(&self) -> bool {
49 matches!(self, Self::Normal)
50 }
51
52 pub fn is_final(&self) -> bool {
54 matches!(self, Self::Normal | Self::Exceeded)
55 }
56
57 pub fn all() -> Vec<Self> {
59 vec![
60 Self::Normal,
61 Self::Exceeded,
62 ]
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum UserQuotaTransition {
70 Exceed,
72 Restore,
74}
75
76impl std::fmt::Display for UserQuotaTransition {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 Self::Exceed => write!(f, "exceed"),
80 Self::Restore => write!(f, "restore"),
81 }
82 }
83}
84
85impl FromStr for UserQuotaTransition {
86 type Err = StateMachineError;
87
88 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 match s.to_lowercase().as_str() {
90 "exceed" => Ok(Self::Exceed),
91 "restore" => Ok(Self::Restore),
92 _ => Err(StateMachineError::InvalidTransition(s.to_string())),
93 }
94 }
95}
96
97impl UserQuotaTransition {
98 pub fn target_state(&self) -> UserQuotaState {
100 match self {
101 Self::Exceed => UserQuotaState::Exceeded,
102 Self::Restore => UserQuotaState::Normal,
103 }
104 }
105
106 pub fn all() -> Vec<Self> {
108 vec![
109 Self::Exceed,
110 Self::Restore,
111 ]
112 }
113
114 pub fn allowed_roles(&self) -> &'static [&'static str] {
116 match self {
117 Self::Exceed => &["system"],
118 Self::Restore => &["system"],
119 }
120 }
121}
122
123use super::StateMachineError;
124
125#[derive(Debug, Clone)]
127pub struct UserQuotaStateMachine {
128 current_state: UserQuotaState,
129}
130
131impl UserQuotaStateMachine {
132 pub fn new() -> Self {
134 Self {
135 current_state: UserQuotaState::default(),
136 }
137 }
138
139 pub fn from_state(state: UserQuotaState) -> Self {
141 Self { current_state: state }
142 }
143
144 pub fn current_state(&self) -> UserQuotaState {
146 self.current_state
147 }
148
149 pub fn can_transition(&self, transition: UserQuotaTransition) -> bool {
151 match (self.current_state, transition) {
152 (UserQuotaState::Normal, UserQuotaTransition::Exceed) => true,
153 (UserQuotaState::Exceeded, UserQuotaTransition::Restore) => true,
154 _ => false,
155 }
156 }
157
158 pub fn can_transition_with_role(&self, transition: UserQuotaTransition, role: &str) -> bool {
160 if !self.can_transition(transition) {
161 return false;
162 }
163
164 let allowed_roles = transition.allowed_roles();
165 if allowed_roles.is_empty() {
166 return true; }
168
169 allowed_roles.iter().any(|r| *r == role || *r == "*")
170 }
171
172 pub fn transition(&mut self, transition: UserQuotaTransition) -> Result<UserQuotaState, StateMachineError> {
174 if !self.can_transition(transition) {
175 return Err(StateMachineError::TransitionNotAllowed {
176 transition: transition.to_string(),
177 from: self.current_state.to_string(),
178 });
179 }
180
181 self.current_state = transition.target_state();
182 Ok(self.current_state)
183 }
184
185 pub fn transition_with_role(&mut self, transition: UserQuotaTransition, role: &str) -> Result<UserQuotaState, StateMachineError> {
187 if !self.can_transition(transition) {
189 return Err(StateMachineError::TransitionNotAllowed {
190 transition: transition.to_string(),
191 from: self.current_state.to_string(),
192 });
193 }
194
195 if !self.can_transition_with_role(transition, role) {
197 return Err(StateMachineError::RoleNotAuthorized {
198 role: role.to_string(),
199 transition: transition.to_string(),
200 });
201 }
202
203 self.current_state = transition.target_state();
204 Ok(self.current_state)
205 }
206
207 pub fn available_transitions(&self) -> Vec<UserQuotaTransition> {
209 UserQuotaTransition::all()
210 .into_iter()
211 .filter(|t| self.can_transition(*t))
212 .collect()
213 }
214
215 pub fn available_transitions_for_role(&self, role: &str) -> Vec<UserQuotaTransition> {
217 UserQuotaTransition::all()
218 .into_iter()
219 .filter(|t| self.can_transition_with_role(*t, role))
220 .collect()
221 }
222
223 pub fn transition_to_state(&mut self, target: UserQuotaState) -> Result<UserQuotaState, StateMachineError> {
228 let valid = UserQuotaTransition::all().into_iter()
229 .filter(|t| self.can_transition(*t))
230 .find(|t| t.target_state() == target);
231 match valid {
232 Some(t) => self.transition(t),
233 None => Err(StateMachineError::TransitionNotAllowed {
234 transition: target.to_string(),
235 from: self.current_state.to_string(),
236 }),
237 }
238 }
239}
240
241impl Default for UserQuotaStateMachine {
242 fn default() -> Self {
243 Self::new()
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_initial_state() {
253 let sm = UserQuotaStateMachine::new();
254 assert_eq!(sm.current_state(), UserQuotaState::Normal);
255 assert!(sm.current_state().is_initial());
256 }
257
258 #[test]
259 fn test_valid_transition() {
260 let mut sm = UserQuotaStateMachine::from_state(UserQuotaState::Normal);
261 assert!(sm.can_transition(UserQuotaTransition::Exceed));
262 let result = sm.transition(UserQuotaTransition::Exceed);
263 assert!(result.is_ok());
264 assert_eq!(sm.current_state(), UserQuotaState::Exceeded);
265 }
266
267 #[test]
268 fn test_invalid_transition() {
269 let mut sm = UserQuotaStateMachine::from_state(UserQuotaState::Normal);
270 let result = sm.transition(UserQuotaTransition::Restore);
272 assert!(result.is_err());
273 }
274
275 #[test]
276 fn test_state_parsing() {
277 let state: UserQuotaState = "normal".parse().unwrap();
278 assert_eq!(state, UserQuotaState::Normal);
279 }
280
281 #[test]
282 fn test_available_transitions() {
283 let sm = UserQuotaStateMachine::new();
284 let available = sm.available_transitions();
285 assert!(!available.is_empty() || sm.current_state().is_final());
287 }
288}