Skip to main content

pacha/model/
stage.rs

1//! Model lifecycle stages.
2
3use crate::error::{PachaError, Result};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::str::FromStr;
7
8/// Lifecycle stage of a model.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
10#[serde(rename_all = "lowercase")]
11pub enum ModelStage {
12    /// Model is in active development.
13    #[default]
14    Development,
15    /// Model is in staging for testing.
16    Staging,
17    /// Model is in production.
18    Production,
19    /// Model is archived (no longer in use).
20    Archived,
21}
22
23impl ModelStage {
24    /// Get all valid stages.
25    #[must_use]
26    pub fn all() -> &'static [ModelStage] {
27        &[
28            ModelStage::Development,
29            ModelStage::Staging,
30            ModelStage::Production,
31            ModelStage::Archived,
32        ]
33    }
34
35    /// Check if transition to another stage is valid.
36    ///
37    /// Valid transitions:
38    /// - Development -> Staging, Archived
39    /// - Staging -> Development, Production, Archived
40    /// - Production -> Staging, Archived
41    /// - Archived -> Development (for resurrection)
42    #[must_use]
43    pub fn can_transition_to(&self, target: ModelStage) -> bool {
44        if *self == target {
45            return true; // Same stage is always valid
46        }
47
48        match self {
49            Self::Development => matches!(target, Self::Staging | Self::Archived),
50            Self::Staging => {
51                matches!(target, Self::Development | Self::Production | Self::Archived)
52            }
53            Self::Production => matches!(target, Self::Staging | Self::Archived),
54            Self::Archived => matches!(target, Self::Development),
55        }
56    }
57
58    /// Attempt to transition to another stage.
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the transition is invalid.
63    pub fn transition_to(&self, target: ModelStage) -> Result<ModelStage> {
64        if self.can_transition_to(target) {
65            Ok(target)
66        } else {
67            Err(PachaError::InvalidStageTransition {
68                from: self.to_string(),
69                to: target.to_string(),
70            })
71        }
72    }
73
74    /// Check if this stage allows modification.
75    #[must_use]
76    pub fn is_mutable(&self) -> bool {
77        matches!(self, Self::Development)
78    }
79
80    /// Check if this stage is active (not archived).
81    #[must_use]
82    pub fn is_active(&self) -> bool {
83        !matches!(self, Self::Archived)
84    }
85}
86
87impl fmt::Display for ModelStage {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        let s = match self {
90            Self::Development => "development",
91            Self::Staging => "staging",
92            Self::Production => "production",
93            Self::Archived => "archived",
94        };
95        write!(f, "{s}")
96    }
97}
98
99impl FromStr for ModelStage {
100    type Err = PachaError;
101
102    fn from_str(s: &str) -> Result<Self> {
103        match s.to_lowercase().as_str() {
104            "development" | "dev" => Ok(Self::Development),
105            "staging" | "stage" => Ok(Self::Staging),
106            "production" | "prod" => Ok(Self::Production),
107            "archived" | "archive" => Ok(Self::Archived),
108            _ => Err(PachaError::Validation(format!("unknown stage: {s}"))),
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_stage_display() {
119        assert_eq!(ModelStage::Development.to_string(), "development");
120        assert_eq!(ModelStage::Staging.to_string(), "staging");
121        assert_eq!(ModelStage::Production.to_string(), "production");
122        assert_eq!(ModelStage::Archived.to_string(), "archived");
123    }
124
125    #[test]
126    fn test_stage_parse() {
127        assert_eq!("development".parse::<ModelStage>().unwrap(), ModelStage::Development);
128        assert_eq!("dev".parse::<ModelStage>().unwrap(), ModelStage::Development);
129        assert_eq!("staging".parse::<ModelStage>().unwrap(), ModelStage::Staging);
130        assert_eq!("stage".parse::<ModelStage>().unwrap(), ModelStage::Staging);
131        assert_eq!("production".parse::<ModelStage>().unwrap(), ModelStage::Production);
132        assert_eq!("prod".parse::<ModelStage>().unwrap(), ModelStage::Production);
133        assert_eq!("archived".parse::<ModelStage>().unwrap(), ModelStage::Archived);
134    }
135
136    #[test]
137    fn test_stage_parse_error() {
138        assert!("invalid".parse::<ModelStage>().is_err());
139        assert!("".parse::<ModelStage>().is_err());
140    }
141
142    #[test]
143    fn test_valid_transitions_from_development() {
144        let dev = ModelStage::Development;
145        assert!(dev.can_transition_to(ModelStage::Development));
146        assert!(dev.can_transition_to(ModelStage::Staging));
147        assert!(!dev.can_transition_to(ModelStage::Production)); // Must go through staging
148        assert!(dev.can_transition_to(ModelStage::Archived));
149    }
150
151    #[test]
152    fn test_valid_transitions_from_staging() {
153        let staging = ModelStage::Staging;
154        assert!(staging.can_transition_to(ModelStage::Development)); // Rollback
155        assert!(staging.can_transition_to(ModelStage::Staging));
156        assert!(staging.can_transition_to(ModelStage::Production));
157        assert!(staging.can_transition_to(ModelStage::Archived));
158    }
159
160    #[test]
161    fn test_valid_transitions_from_production() {
162        let prod = ModelStage::Production;
163        assert!(!prod.can_transition_to(ModelStage::Development)); // Can't go back to dev
164        assert!(prod.can_transition_to(ModelStage::Staging)); // Rollback to staging
165        assert!(prod.can_transition_to(ModelStage::Production));
166        assert!(prod.can_transition_to(ModelStage::Archived));
167    }
168
169    #[test]
170    fn test_valid_transitions_from_archived() {
171        let archived = ModelStage::Archived;
172        assert!(archived.can_transition_to(ModelStage::Development)); // Resurrection
173        assert!(!archived.can_transition_to(ModelStage::Staging));
174        assert!(!archived.can_transition_to(ModelStage::Production));
175        assert!(archived.can_transition_to(ModelStage::Archived));
176    }
177
178    #[test]
179    fn test_transition_to_success() {
180        let dev = ModelStage::Development;
181        let result = dev.transition_to(ModelStage::Staging);
182        assert_eq!(result.unwrap(), ModelStage::Staging);
183    }
184
185    #[test]
186    fn test_transition_to_error() {
187        let dev = ModelStage::Development;
188        let result = dev.transition_to(ModelStage::Production);
189        assert!(matches!(result, Err(PachaError::InvalidStageTransition { .. })));
190    }
191
192    #[test]
193    fn test_is_mutable() {
194        assert!(ModelStage::Development.is_mutable());
195        assert!(!ModelStage::Staging.is_mutable());
196        assert!(!ModelStage::Production.is_mutable());
197        assert!(!ModelStage::Archived.is_mutable());
198    }
199
200    #[test]
201    fn test_is_active() {
202        assert!(ModelStage::Development.is_active());
203        assert!(ModelStage::Staging.is_active());
204        assert!(ModelStage::Production.is_active());
205        assert!(!ModelStage::Archived.is_active());
206    }
207
208    #[test]
209    fn test_serialization() {
210        let stage = ModelStage::Production;
211        let json = serde_json::to_string(&stage).unwrap();
212        assert_eq!(json, "\"production\"");
213
214        let deserialized: ModelStage = serde_json::from_str(&json).unwrap();
215        assert_eq!(deserialized, ModelStage::Production);
216    }
217
218    #[test]
219    fn test_all_stages() {
220        let all = ModelStage::all();
221        assert_eq!(all.len(), 4);
222        assert!(all.contains(&ModelStage::Development));
223        assert!(all.contains(&ModelStage::Staging));
224        assert!(all.contains(&ModelStage::Production));
225        assert!(all.contains(&ModelStage::Archived));
226    }
227}