Skip to main content

backbone_bucket/domain/state_machine/
file_comment_state_machine.rs

1//! State machine for FileComment workflow
2//!
3//! Generated by metaphor-schema
4
5use serde::{Deserialize, Serialize};
6use std::str::FromStr;
7
8/// States for FileComment workflow
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum FileCommentState {
12    /// Initial state
13    Active,
14    Resolved,
15    /// Final state
16    Deleted,
17}
18
19impl Default for FileCommentState {
20    fn default() -> Self {
21        Self::Active
22    }
23}
24
25impl std::fmt::Display for FileCommentState {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            Self::Active => write!(f, "active"),
29            Self::Resolved => write!(f, "resolved"),
30            Self::Deleted => write!(f, "deleted"),
31        }
32    }
33}
34
35impl FromStr for FileCommentState {
36    type Err = StateMachineError;
37
38    fn from_str(s: &str) -> Result<Self, Self::Err> {
39        match s.to_lowercase().as_str() {
40            "active" => Ok(Self::Active),
41            "resolved" => Ok(Self::Resolved),
42            "deleted" => Ok(Self::Deleted),
43            _ => Err(StateMachineError::InvalidState(s.to_string())),
44        }
45    }
46}
47
48impl FileCommentState {
49    /// Check if this is the initial state
50    pub fn is_initial(&self) -> bool {
51        matches!(self, Self::Active)
52    }
53
54    /// Check if this is a final state
55    pub fn is_final(&self) -> bool {
56        matches!(self, Self::Deleted)
57    }
58
59    /// Get all possible states
60    pub fn all() -> Vec<Self> {
61        vec![
62            Self::Active,
63            Self::Resolved,
64            Self::Deleted,
65        ]
66    }
67}
68
69/// Transitions for FileComment workflow
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum FileCommentTransition {
73    /// active -> resolved
74    Resolve,
75    /// active -> active
76    Edit,
77    /// active -> deleted
78    DeleteActive,
79    /// resolved -> active
80    Reopen,
81    /// resolved -> deleted
82    DeleteResolved,
83}
84
85impl std::fmt::Display for FileCommentTransition {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        match self {
88            Self::Resolve => write!(f, "resolve"),
89            Self::Edit => write!(f, "edit"),
90            Self::DeleteActive => write!(f, "delete_active"),
91            Self::Reopen => write!(f, "reopen"),
92            Self::DeleteResolved => write!(f, "delete_resolved"),
93        }
94    }
95}
96
97impl FromStr for FileCommentTransition {
98    type Err = StateMachineError;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        match s.to_lowercase().as_str() {
102            "resolve" => Ok(Self::Resolve),
103            "edit" => Ok(Self::Edit),
104            "delete_active" => Ok(Self::DeleteActive),
105            "reopen" => Ok(Self::Reopen),
106            "delete_resolved" => Ok(Self::DeleteResolved),
107            _ => Err(StateMachineError::InvalidTransition(s.to_string())),
108        }
109    }
110}
111
112impl FileCommentTransition {
113    /// Get the target state of this transition
114    pub fn target_state(&self) -> FileCommentState {
115        match self {
116            Self::Resolve => FileCommentState::Resolved,
117            Self::Edit => FileCommentState::Active,
118            Self::DeleteActive => FileCommentState::Deleted,
119            Self::Reopen => FileCommentState::Active,
120            Self::DeleteResolved => FileCommentState::Deleted,
121        }
122    }
123
124    /// Get all transitions
125    pub fn all() -> Vec<Self> {
126        vec![
127            Self::Resolve,
128            Self::Edit,
129            Self::DeleteActive,
130            Self::Reopen,
131            Self::DeleteResolved,
132        ]
133    }
134
135    /// Get allowed roles for this transition
136    pub fn allowed_roles(&self) -> &'static [&'static str] {
137        match self {
138            Self::Resolve => &["owner", "file_owner"],
139            Self::Edit => &["owner"],
140            Self::DeleteActive => &["owner", "file_owner"],
141            Self::Reopen => &["user"],
142            Self::DeleteResolved => &["owner", "file_owner"],
143        }
144    }
145}
146
147use super::StateMachineError;
148
149/// State machine for FileComment workflow
150#[derive(Debug, Clone)]
151pub struct FileCommentStateMachine {
152    current_state: FileCommentState,
153}
154
155impl FileCommentStateMachine {
156    /// Create a new state machine with initial state
157    pub fn new() -> Self {
158        Self {
159            current_state: FileCommentState::default(),
160        }
161    }
162
163    /// Create from an existing state
164    pub fn from_state(state: FileCommentState) -> Self {
165        Self { current_state: state }
166    }
167
168    /// Get the current state
169    pub fn current_state(&self) -> FileCommentState {
170        self.current_state
171    }
172
173    /// Check if a transition is allowed from the current state
174    pub fn can_transition(&self, transition: FileCommentTransition) -> bool {
175        if matches!(self.current_state, FileCommentState::Deleted) {
176            return false;
177        }
178
179        match (self.current_state, transition) {
180            (FileCommentState::Active, FileCommentTransition::Resolve) => true,
181            (FileCommentState::Active, FileCommentTransition::Edit) => true,
182            (FileCommentState::Active, FileCommentTransition::DeleteActive) => true,
183            (FileCommentState::Resolved, FileCommentTransition::Reopen) => true,
184            (FileCommentState::Resolved, FileCommentTransition::DeleteResolved) => true,
185            _ => false,
186        }
187    }
188
189    /// Check if a transition is allowed for a given role
190    pub fn can_transition_with_role(&self, transition: FileCommentTransition, role: &str) -> bool {
191        if !self.can_transition(transition) {
192            return false;
193        }
194
195        let allowed_roles = transition.allowed_roles();
196        if allowed_roles.is_empty() {
197            return true; // No role restriction
198        }
199
200        allowed_roles.iter().any(|r| *r == role || *r == "*")
201    }
202
203    /// Apply a transition, returning the new state
204    pub fn transition(&mut self, transition: FileCommentTransition) -> Result<FileCommentState, StateMachineError> {
205        if !self.can_transition(transition) {
206            return Err(StateMachineError::TransitionNotAllowed {
207                transition: transition.to_string(),
208                from: self.current_state.to_string(),
209            });
210        }
211
212        self.current_state = transition.target_state();
213        Ok(self.current_state)
214    }
215
216    /// Apply a transition with role check
217    pub fn transition_with_role(&mut self, transition: FileCommentTransition, role: &str) -> Result<FileCommentState, StateMachineError> {
218        // Check basic transition validity first
219        if !self.can_transition(transition) {
220            return Err(StateMachineError::TransitionNotAllowed {
221                transition: transition.to_string(),
222                from: self.current_state.to_string(),
223            });
224        }
225
226        // Check role authorization
227        if !self.can_transition_with_role(transition, role) {
228            return Err(StateMachineError::RoleNotAuthorized {
229                role: role.to_string(),
230                transition: transition.to_string(),
231            });
232        }
233
234        self.current_state = transition.target_state();
235        Ok(self.current_state)
236    }
237
238    /// Get all available transitions from the current state
239    pub fn available_transitions(&self) -> Vec<FileCommentTransition> {
240        FileCommentTransition::all()
241            .into_iter()
242            .filter(|t| self.can_transition(*t))
243            .collect()
244    }
245
246    /// Get all available transitions for a given role
247    pub fn available_transitions_for_role(&self, role: &str) -> Vec<FileCommentTransition> {
248        FileCommentTransition::all()
249            .into_iter()
250            .filter(|t| self.can_transition_with_role(*t, role))
251            .collect()
252    }
253
254    /// Attempt to transition directly to a target state.
255    ///
256    /// Finds any transition that leads to the target state and applies it.
257    /// Returns Err if no valid transition leads from current state to target.
258    pub fn transition_to_state(&mut self, target: FileCommentState) -> Result<FileCommentState, StateMachineError> {
259        let valid = FileCommentTransition::all().into_iter()
260            .filter(|t| self.can_transition(*t))
261            .find(|t| t.target_state() == target);
262        match valid {
263            Some(t) => self.transition(t),
264            None => Err(StateMachineError::TransitionNotAllowed {
265                transition: target.to_string(),
266                from: self.current_state.to_string(),
267            }),
268        }
269    }
270}
271
272impl Default for FileCommentStateMachine {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_initial_state() {
284        let sm = FileCommentStateMachine::new();
285        assert_eq!(sm.current_state(), FileCommentState::Active);
286        assert!(sm.current_state().is_initial());
287    }
288
289    #[test]
290    fn test_valid_transition() {
291        let mut sm = FileCommentStateMachine::from_state(FileCommentState::Active);
292        assert!(sm.can_transition(FileCommentTransition::Resolve));
293        let result = sm.transition(FileCommentTransition::Resolve);
294        assert!(result.is_ok());
295        assert_eq!(sm.current_state(), FileCommentState::Resolved);
296    }
297
298    #[test]
299    fn test_invalid_transition() {
300        let mut sm = FileCommentStateMachine::from_state(FileCommentState::Active);
301        // Reopen is not valid from Active state
302        let result = sm.transition(FileCommentTransition::Reopen);
303        assert!(result.is_err());
304    }
305
306    #[test]
307    fn test_state_parsing() {
308        let state: FileCommentState = "active".parse().unwrap();
309        assert_eq!(state, FileCommentState::Active);
310    }
311
312    #[test]
313    fn test_available_transitions() {
314        let sm = FileCommentStateMachine::new();
315        let available = sm.available_transitions();
316        // Should have at least some transitions available from initial state
317        assert!(!available.is_empty() || sm.current_state().is_final());
318    }
319}