1use anyhow::Result;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use tokio::sync::RwLock;
8use uuid::Uuid;
9
10pub struct VersionLifecycle {
12 version_status: RwLock<HashMap<Uuid, VersionStatus>>,
14 lifecycle_history: RwLock<HashMap<Uuid, Vec<LifecycleEvent>>>,
16 policies: RwLock<LifecyclePolicies>,
18}
19
20impl VersionLifecycle {
21 pub fn new() -> Self {
23 Self {
24 version_status: RwLock::new(HashMap::new()),
25 lifecycle_history: RwLock::new(HashMap::new()),
26 policies: RwLock::new(LifecyclePolicies::default()),
27 }
28 }
29
30 pub async fn initialize_version(&self, version_id: Uuid) -> Result<()> {
32 {
33 let mut status_map = self.version_status.write().await;
34 status_map.insert(version_id, VersionStatus::Development);
35 }
36
37 {
38 let mut history_map = self.lifecycle_history.write().await;
39 let event = LifecycleEvent {
40 transition: VersionTransition::Initialize,
41 from_status: None,
42 to_status: VersionStatus::Development,
43 timestamp: Utc::now(),
44 reason: "Version initialized".to_string(),
45 triggered_by: "system".to_string(),
46 };
47 history_map.insert(version_id, vec![event]);
48 }
49
50 tracing::info!("Initialized version {} in Development status", version_id);
51 Ok(())
52 }
53
54 pub async fn get_status(&self, version_id: Uuid) -> Result<VersionStatus> {
56 let status_map = self.version_status.read().await;
57 status_map
58 .get(&version_id)
59 .copied()
60 .ok_or_else(|| anyhow::anyhow!("Version {} not found", version_id))
61 }
62
63 pub async fn transition(&self, version_id: Uuid, transition: VersionTransition) -> Result<()> {
65 self.transition_with_reason(version_id, transition, "Manual transition", "user")
66 .await
67 }
68
69 pub async fn transition_with_reason(
71 &self,
72 version_id: Uuid,
73 transition: VersionTransition,
74 reason: &str,
75 triggered_by: &str,
76 ) -> Result<()> {
77 let current_status = self.get_status(version_id).await?;
78 let new_status = self.validate_transition(current_status, &transition)?;
79
80 let policies = self.policies.read().await;
82 if !policies.allows_transition(current_status, new_status) {
83 anyhow::bail!(
84 "Transition from {:?} to {:?} is not allowed by policy",
85 current_status,
86 new_status
87 );
88 }
89
90 {
92 let mut status_map = self.version_status.write().await;
93 status_map.insert(version_id, new_status);
94 }
95
96 {
98 let mut history_map = self.lifecycle_history.write().await;
99 let event = LifecycleEvent {
100 transition,
101 from_status: Some(current_status),
102 to_status: new_status,
103 timestamp: Utc::now(),
104 reason: reason.to_string(),
105 triggered_by: triggered_by.to_string(),
106 };
107
108 history_map.entry(version_id).or_default().push(event);
109 }
110
111 tracing::info!(
112 "Transitioned version {} from {:?} to {:?}: {}",
113 version_id,
114 current_status,
115 new_status,
116 reason
117 );
118
119 Ok(())
120 }
121
122 pub async fn get_history(&self, version_id: Uuid) -> Result<Vec<LifecycleEvent>> {
124 let history_map = self.lifecycle_history.read().await;
125 Ok(history_map.get(&version_id).cloned().unwrap_or_default())
126 }
127
128 pub async fn get_versions_by_status(&self, status: VersionStatus) -> Result<Vec<Uuid>> {
130 let status_map = self.version_status.read().await;
131 let versions: Vec<Uuid> =
132 status_map.iter().filter(|(_, &s)| s == status).map(|(&id, _)| id).collect();
133 Ok(versions)
134 }
135
136 pub async fn can_promote(&self, version_id: Uuid) -> Result<bool> {
138 let current_status = self.get_status(version_id).await?;
139 Ok(matches!(current_status, VersionStatus::Staging))
140 }
141
142 pub async fn can_archive(&self, version_id: Uuid) -> Result<bool> {
144 let current_status = self.get_status(version_id).await?;
145 Ok(!matches!(current_status, VersionStatus::Production))
146 }
147
148 pub async fn auto_archive(&self) -> Result<Vec<Uuid>> {
150 let policies = self.policies.read().await;
151 let mut archived_versions = Vec::new();
152
153 if let Some(max_age_days) = policies.auto_archive_after_days {
154 let cutoff_date = Utc::now() - chrono::Duration::days(max_age_days as i64);
155
156 let versions_to_archive = {
158 let history_map = self.lifecycle_history.read().await;
159 let mut to_archive = Vec::new();
160
161 for (&version_id, history) in history_map.iter() {
162 if let Some(creation_event) = history.first() {
164 if creation_event.timestamp < cutoff_date {
165 to_archive.push(version_id);
166 }
167 }
168 }
169 to_archive
170 };
171
172 for version_id in versions_to_archive {
174 if self.can_archive(version_id).await? {
175 self.transition_with_reason(
176 version_id,
177 VersionTransition::Archive,
178 "Auto-archived due to age policy",
179 "system",
180 )
181 .await?;
182 archived_versions.push(version_id);
183 }
184 }
185 }
186
187 if !archived_versions.is_empty() {
188 tracing::info!("Auto-archived {} versions", archived_versions.len());
189 }
190
191 Ok(archived_versions)
192 }
193
194 pub async fn update_policies(&self, policies: LifecyclePolicies) -> Result<()> {
196 let mut current_policies = self.policies.write().await;
197 *current_policies = policies;
198 tracing::info!("Updated lifecycle policies");
199 Ok(())
200 }
201
202 pub async fn get_policies(&self) -> Result<LifecyclePolicies> {
204 let policies = self.policies.read().await;
205 Ok(policies.clone())
206 }
207
208 pub async fn cleanup_version(&self, version_id: Uuid) -> Result<()> {
210 {
211 let mut status_map = self.version_status.write().await;
212 status_map.remove(&version_id);
213 }
214
215 {
216 let mut history_map = self.lifecycle_history.write().await;
217 history_map.remove(&version_id);
218 }
219
220 tracing::debug!("Cleaned up lifecycle tracking for version {}", version_id);
221 Ok(())
222 }
223
224 pub async fn get_statistics(&self) -> Result<LifecycleStatistics> {
226 let status_map = self.version_status.read().await;
227
228 let mut counts = HashMap::new();
229 for status in [
230 VersionStatus::Development,
231 VersionStatus::Staging,
232 VersionStatus::Production,
233 VersionStatus::Archived,
234 VersionStatus::Deprecated,
235 ] {
236 counts.insert(status, 0);
237 }
238
239 for &status in status_map.values() {
240 *counts.entry(status).or_insert(0) += 1;
241 }
242
243 Ok(LifecycleStatistics {
244 development_count: counts[&VersionStatus::Development],
245 staging_count: counts[&VersionStatus::Staging],
246 production_count: counts[&VersionStatus::Production],
247 archived_count: counts[&VersionStatus::Archived],
248 deprecated_count: counts[&VersionStatus::Deprecated],
249 total_versions: status_map.len(),
250 })
251 }
252
253 fn validate_transition(
256 &self,
257 current_status: VersionStatus,
258 transition: &VersionTransition,
259 ) -> Result<VersionStatus> {
260 let new_status = match (current_status, transition) {
261 (VersionStatus::Development, VersionTransition::ToStaging) => VersionStatus::Staging,
262 (VersionStatus::Staging, VersionTransition::Promote) => VersionStatus::Production,
263 (VersionStatus::Staging, VersionTransition::ToTesting) => VersionStatus::Testing,
264 (VersionStatus::Testing, VersionTransition::ToStaging) => VersionStatus::Staging,
265 (VersionStatus::Production, VersionTransition::Deprecate) => VersionStatus::Deprecated,
266 (_, VersionTransition::Archive) => VersionStatus::Archived,
267 (_, VersionTransition::ToTesting) => VersionStatus::Testing,
268 (VersionStatus::Archived, VersionTransition::Restore) => VersionStatus::Staging,
269 _ => {
270 anyhow::bail!(
271 "Invalid transition {:?} from status {:?}",
272 transition,
273 current_status
274 );
275 },
276 };
277
278 Ok(new_status)
279 }
280}
281
282impl Default for VersionLifecycle {
283 fn default() -> Self {
284 Self::new()
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
290pub enum VersionStatus {
291 Development,
293 Testing,
295 Staging,
297 Production,
299 Deprecated,
301 Archived,
303}
304
305impl std::fmt::Display for VersionStatus {
306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307 match self {
308 VersionStatus::Development => write!(f, "Development"),
309 VersionStatus::Testing => write!(f, "Testing"),
310 VersionStatus::Staging => write!(f, "Staging"),
311 VersionStatus::Production => write!(f, "Production"),
312 VersionStatus::Deprecated => write!(f, "Deprecated"),
313 VersionStatus::Archived => write!(f, "Archived"),
314 }
315 }
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub enum VersionTransition {
321 Initialize,
323 ToTesting,
325 ToStaging,
327 Promote,
329 Deprecate,
331 Archive,
333 Restore,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct LifecycleEvent {
340 pub transition: VersionTransition,
341 pub from_status: Option<VersionStatus>,
342 pub to_status: VersionStatus,
343 pub timestamp: DateTime<Utc>,
344 pub reason: String,
345 pub triggered_by: String,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct LifecyclePolicies {
351 pub allowed_transitions: HashMap<VersionStatus, Vec<VersionStatus>>,
353 pub max_production_versions: Option<usize>,
355 pub auto_archive_after_days: Option<usize>,
357 pub require_approval_for_production: bool,
359 pub allow_production_rollback: bool,
361}
362
363impl Default for LifecyclePolicies {
364 fn default() -> Self {
365 let mut allowed_transitions = HashMap::new();
366
367 allowed_transitions.insert(
368 VersionStatus::Development,
369 vec![
370 VersionStatus::Testing,
371 VersionStatus::Staging,
372 VersionStatus::Archived,
373 ],
374 );
375 allowed_transitions.insert(
376 VersionStatus::Testing,
377 vec![
378 VersionStatus::Staging,
379 VersionStatus::Development,
380 VersionStatus::Archived,
381 ],
382 );
383 allowed_transitions.insert(
384 VersionStatus::Staging,
385 vec![
386 VersionStatus::Production,
387 VersionStatus::Testing,
388 VersionStatus::Archived,
389 ],
390 );
391 allowed_transitions.insert(VersionStatus::Production, vec![VersionStatus::Deprecated]);
392 allowed_transitions.insert(VersionStatus::Deprecated, vec![VersionStatus::Archived]);
393 allowed_transitions.insert(VersionStatus::Archived, vec![VersionStatus::Staging]);
394
395 Self {
396 allowed_transitions,
397 max_production_versions: Some(3),
398 auto_archive_after_days: Some(365),
399 require_approval_for_production: true,
400 allow_production_rollback: true,
401 }
402 }
403}
404
405impl LifecyclePolicies {
406 pub fn allows_transition(&self, from: VersionStatus, to: VersionStatus) -> bool {
408 self.allowed_transitions.get(&from).is_some_and(|allowed| allowed.contains(&to))
409 }
410
411 pub fn set_allowed_transitions(&mut self, from: VersionStatus, to: Vec<VersionStatus>) {
413 self.allowed_transitions.insert(from, to);
414 }
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct LifecycleStatistics {
420 pub development_count: usize,
421 pub staging_count: usize,
422 pub production_count: usize,
423 pub archived_count: usize,
424 pub deprecated_count: usize,
425 pub total_versions: usize,
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[tokio::test]
433 async fn test_version_lifecycle() {
434 let lifecycle = VersionLifecycle::new();
435 let version_id = Uuid::new_v4();
436
437 lifecycle.initialize_version(version_id).await.expect("Async operation failed");
439 let status = lifecycle.get_status(version_id).await.expect("Async operation failed");
440 assert_eq!(status, VersionStatus::Development);
441
442 lifecycle
444 .transition(version_id, VersionTransition::ToStaging)
445 .await
446 .expect("Async operation failed");
447 let status = lifecycle.get_status(version_id).await.expect("Async operation failed");
448 assert_eq!(status, VersionStatus::Staging);
449
450 lifecycle
452 .transition(version_id, VersionTransition::Promote)
453 .await
454 .expect("Async operation failed");
455 let status = lifecycle.get_status(version_id).await.expect("Async operation failed");
456 assert_eq!(status, VersionStatus::Production);
457
458 let history = lifecycle.get_history(version_id).await.expect("Async operation failed");
460 assert_eq!(history.len(), 3); }
462
463 #[tokio::test]
464 async fn test_invalid_transition() {
465 let lifecycle = VersionLifecycle::new();
466 let version_id = Uuid::new_v4();
467
468 lifecycle.initialize_version(version_id).await.expect("Async operation failed");
469
470 let result = lifecycle.transition(version_id, VersionTransition::Promote).await;
472 assert!(result.is_err());
473 }
474
475 #[tokio::test]
476 async fn test_versions_by_status() {
477 let lifecycle = VersionLifecycle::new();
478
479 let version1 = Uuid::new_v4();
480 let version2 = Uuid::new_v4();
481
482 lifecycle.initialize_version(version1).await.expect("Async operation failed");
483 lifecycle.initialize_version(version2).await.expect("Async operation failed");
484
485 lifecycle
486 .transition(version1, VersionTransition::ToStaging)
487 .await
488 .expect("Async operation failed");
489
490 let dev_versions = lifecycle
491 .get_versions_by_status(VersionStatus::Development)
492 .await
493 .expect("Async operation failed");
494 assert_eq!(dev_versions.len(), 1);
495 assert!(dev_versions.contains(&version2));
496
497 let staging_versions = lifecycle
498 .get_versions_by_status(VersionStatus::Staging)
499 .await
500 .expect("Async operation failed");
501 assert_eq!(staging_versions.len(), 1);
502 assert!(staging_versions.contains(&version1));
503 }
504
505 #[tokio::test]
506 async fn test_lifecycle_policies() {
507 let mut policies = LifecyclePolicies::default();
508
509 assert!(policies.allows_transition(VersionStatus::Development, VersionStatus::Staging));
511 assert!(!policies.allows_transition(VersionStatus::Development, VersionStatus::Production));
512
513 policies.set_allowed_transitions(
515 VersionStatus::Development,
516 vec![VersionStatus::Production], );
518
519 assert!(policies.allows_transition(VersionStatus::Development, VersionStatus::Production));
520 assert!(!policies.allows_transition(VersionStatus::Development, VersionStatus::Staging));
521 }
522
523 #[tokio::test]
524 async fn test_lifecycle_statistics() {
525 let lifecycle = VersionLifecycle::new();
526
527 let version1 = Uuid::new_v4();
528 let version2 = Uuid::new_v4();
529
530 lifecycle.initialize_version(version1).await.expect("Async operation failed");
531 lifecycle.initialize_version(version2).await.expect("Async operation failed");
532 lifecycle
533 .transition(version1, VersionTransition::ToStaging)
534 .await
535 .expect("Async operation failed");
536
537 let stats = lifecycle.get_statistics().await.expect("Async operation failed");
538 assert_eq!(stats.development_count, 1);
539 assert_eq!(stats.staging_count, 1);
540 assert_eq!(stats.total_versions, 2);
541 }
542
543 #[tokio::test]
544 async fn test_promotion_capability() {
545 let lifecycle = VersionLifecycle::new();
546 let version_id = Uuid::new_v4();
547
548 lifecycle.initialize_version(version_id).await.expect("Async operation failed");
549
550 assert!(!lifecycle.can_promote(version_id).await.expect("Async operation failed"));
552
553 lifecycle
555 .transition(version_id, VersionTransition::ToStaging)
556 .await
557 .expect("Async operation failed");
558
559 assert!(lifecycle.can_promote(version_id).await.expect("Async operation failed"));
561 }
562}