Skip to main content

backbone_bucket/domain/state_machine/
user_quota_state_machine.rs

1//! State machine for UserQuota workflow
2//!
3//! Generated by metaphor-schema
4
5use serde::{Deserialize, Serialize};
6use std::str::FromStr;
7
8/// States for UserQuota workflow
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum UserQuotaState {
12    /// Initial state
13    /// Final state
14    Normal,
15    /// Final state
16    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    /// Check if this is the initial state
48    pub fn is_initial(&self) -> bool {
49        matches!(self, Self::Normal)
50    }
51
52    /// Check if this is a final state
53    pub fn is_final(&self) -> bool {
54        matches!(self, Self::Normal | Self::Exceeded)
55    }
56
57    /// Get all possible states
58    pub fn all() -> Vec<Self> {
59        vec![
60            Self::Normal,
61            Self::Exceeded,
62        ]
63    }
64}
65
66/// Transitions for UserQuota workflow
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum UserQuotaTransition {
70    /// normal -> exceeded
71    Exceed,
72    /// exceeded -> normal
73    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    /// Get the target state of this transition
99    pub fn target_state(&self) -> UserQuotaState {
100        match self {
101            Self::Exceed => UserQuotaState::Exceeded,
102            Self::Restore => UserQuotaState::Normal,
103        }
104    }
105
106    /// Get all transitions
107    pub fn all() -> Vec<Self> {
108        vec![
109            Self::Exceed,
110            Self::Restore,
111        ]
112    }
113
114    /// Get allowed roles for this transition
115    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/// State machine for UserQuota workflow
126#[derive(Debug, Clone)]
127pub struct UserQuotaStateMachine {
128    current_state: UserQuotaState,
129}
130
131impl UserQuotaStateMachine {
132    /// Create a new state machine with initial state
133    pub fn new() -> Self {
134        Self {
135            current_state: UserQuotaState::default(),
136        }
137    }
138
139    /// Create from an existing state
140    pub fn from_state(state: UserQuotaState) -> Self {
141        Self { current_state: state }
142    }
143
144    /// Get the current state
145    pub fn current_state(&self) -> UserQuotaState {
146        self.current_state
147    }
148
149    /// Check if a transition is allowed from the current state
150    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    /// Check if a transition is allowed for a given role
159    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; // No role restriction
167        }
168
169        allowed_roles.iter().any(|r| *r == role || *r == "*")
170    }
171
172    /// Apply a transition, returning the new state
173    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    /// Apply a transition with role check
186    pub fn transition_with_role(&mut self, transition: UserQuotaTransition, role: &str) -> Result<UserQuotaState, StateMachineError> {
187        // Check basic transition validity first
188        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        // Check role authorization
196        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    /// Get all available transitions from the current state
208    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    /// Get all available transitions for a given role
216    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    /// Attempt to transition directly to a target state.
224    ///
225    /// Finds any transition that leads to the target state and applies it.
226    /// Returns Err if no valid transition leads from current state to target.
227    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        // Restore is not valid from Normal state
271        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        // Should have at least some transitions available from initial state
286        assert!(!available.is_empty() || sm.current_state().is_final());
287    }
288}