Skip to main content

trustformers_core/versioning/
lifecycle.rs

1//! Version lifecycle management
2
3use anyhow::Result;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use tokio::sync::RwLock;
8use uuid::Uuid;
9
10/// Version lifecycle manager
11pub struct VersionLifecycle {
12    /// Current status of each version
13    version_status: RwLock<HashMap<Uuid, VersionStatus>>,
14    /// Lifecycle history for each version
15    lifecycle_history: RwLock<HashMap<Uuid, Vec<LifecycleEvent>>>,
16    /// Lifecycle policies
17    policies: RwLock<LifecyclePolicies>,
18}
19
20impl VersionLifecycle {
21    /// Create a new version lifecycle manager
22    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    /// Initialize a new version in the lifecycle
31    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    /// Get current status of a version
55    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    /// Transition a version to a new status
64    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    /// Transition with reason and triggerer
70    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        // Check policies
81        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        // Update status
91        {
92            let mut status_map = self.version_status.write().await;
93            status_map.insert(version_id, new_status);
94        }
95
96        // Record history
97        {
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    /// Get lifecycle history for a version
123    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    /// Get all versions in a specific status
129    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    /// Check if a version can be promoted
137    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    /// Check if a version can be archived
143    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    /// Auto-archive old versions based on policies
149    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            // Collect versions to archive first
157            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                    // Find the creation event
163                    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            // Now archive the versions
173            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    /// Update lifecycle policies
195    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    /// Get current policies
203    pub async fn get_policies(&self) -> Result<LifecyclePolicies> {
204        let policies = self.policies.read().await;
205        Ok(policies.clone())
206    }
207
208    /// Cleanup version from lifecycle tracking
209    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    /// Get lifecycle statistics
225    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    // Helper methods
254
255    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/// Version status in the lifecycle
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
290pub enum VersionStatus {
291    /// Under development
292    Development,
293    /// In testing phase
294    Testing,
295    /// Ready for production testing
296    Staging,
297    /// Currently deployed in production
298    Production,
299    /// No longer recommended for use
300    Deprecated,
301    /// Archived (kept for historical purposes)
302    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/// Version lifecycle transitions
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub enum VersionTransition {
321    /// Initialize a new version
322    Initialize,
323    /// Move to testing
324    ToTesting,
325    /// Move to staging
326    ToStaging,
327    /// Promote to production
328    Promote,
329    /// Deprecate version
330    Deprecate,
331    /// Archive version
332    Archive,
333    /// Restore from archive
334    Restore,
335}
336
337/// Lifecycle event record
338#[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/// Lifecycle policies
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct LifecyclePolicies {
351    /// Allowed transitions between statuses
352    pub allowed_transitions: HashMap<VersionStatus, Vec<VersionStatus>>,
353    /// Maximum number of production versions allowed
354    pub max_production_versions: Option<usize>,
355    /// Auto-archive versions after this many days
356    pub auto_archive_after_days: Option<usize>,
357    /// Require approval for production promotion
358    pub require_approval_for_production: bool,
359    /// Allow rollback from production
360    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    /// Check if a transition is allowed
407    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    /// Set allowed transitions for a status
412    pub fn set_allowed_transitions(&mut self, from: VersionStatus, to: Vec<VersionStatus>) {
413        self.allowed_transitions.insert(from, to);
414    }
415}
416
417/// Lifecycle statistics
418#[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        // Initialize version
438        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        // Transition to staging
443        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        // Promote to production
451        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        // Check history
459        let history = lifecycle.get_history(version_id).await.expect("Async operation failed");
460        assert_eq!(history.len(), 3); // Initialize + 2 transitions
461    }
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        // Try invalid transition (Development -> Production)
471        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        // Test default policy
510        assert!(policies.allows_transition(VersionStatus::Development, VersionStatus::Staging));
511        assert!(!policies.allows_transition(VersionStatus::Development, VersionStatus::Production));
512
513        // Modify policy
514        policies.set_allowed_transitions(
515            VersionStatus::Development,
516            vec![VersionStatus::Production], // Allow direct promotion
517        );
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        // Cannot promote from development
551        assert!(!lifecycle.can_promote(version_id).await.expect("Async operation failed"));
552
553        // Move to staging
554        lifecycle
555            .transition(version_id, VersionTransition::ToStaging)
556            .await
557            .expect("Async operation failed");
558
559        // Can promote from staging
560        assert!(lifecycle.can_promote(version_id).await.expect("Async operation failed"));
561    }
562}