ferrous_forge/rust_version/
mod.rs

1//! Rust version management and checking
2//!
3//! This module provides functionality to detect installed Rust versions,
4//! check for updates from GitHub releases, and provide recommendations.
5
6use crate::Result;
7use semver::Version;
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use std::time::Duration;
11use tokio::sync::RwLock;
12
13pub mod cache;
14pub mod detector;
15pub mod github;
16
17pub use detector::RustVersion;
18pub use github::{GitHubClient, GitHubRelease};
19
20/// Rust release channel
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub enum Channel {
23    /// Stable releases
24    Stable,
25    /// Beta releases
26    Beta,
27    /// Nightly builds
28    Nightly,
29    /// Custom or unknown channel
30    Custom(String),
31}
32
33impl std::fmt::Display for Channel {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            Self::Stable => write!(f, "stable"),
37            Self::Beta => write!(f, "beta"),
38            Self::Nightly => write!(f, "nightly"),
39            Self::Custom(s) => write!(f, "{}", s),
40        }
41    }
42}
43
44/// Version update recommendation
45#[derive(Debug, Clone)]
46pub enum UpdateRecommendation {
47    /// Already on latest version
48    UpToDate,
49    /// Minor update available
50    MinorUpdate {
51        current: Version,
52        latest: Version,
53        release_url: String,
54    },
55    /// Major update available
56    MajorUpdate {
57        current: Version,
58        latest: Version,
59        release_url: String,
60    },
61    /// Security update available
62    SecurityUpdate {
63        current: Version,
64        latest: Version,
65        release_url: String,
66        details: String,
67    },
68}
69
70/// Version manager for checking and recommending updates
71pub struct VersionManager {
72    github_client: GitHubClient,
73    cache: Arc<RwLock<cache::Cache<String, Vec<u8>>>>,
74}
75
76impl VersionManager {
77    /// Create a new version manager
78    pub fn new() -> Result<Self> {
79        let github_client = GitHubClient::new(None)?;
80        let cache = Arc::new(RwLock::new(cache::Cache::new(Duration::from_secs(3600))));
81        
82        Ok(Self {
83            github_client,
84            cache,
85        })
86    }
87    
88    /// Check current Rust installation
89    pub async fn check_current(&self) -> Result<RustVersion> {
90        detector::detect_rust_version()
91    }
92    
93    /// Get latest stable release
94    pub async fn get_latest_stable(&self) -> Result<GitHubRelease> {
95        // Check cache first
96        let cache_key = "latest_stable";
97        
98        {
99            let cache = self.cache.read().await;
100            if let Some(cached_bytes) = cache.get(&cache_key.to_string()) {
101                if let Ok(release) = serde_json::from_slice::<GitHubRelease>(&cached_bytes) {
102                    return Ok(release);
103                }
104            }
105        }
106        
107        // Fetch from GitHub
108        let release = self.github_client.get_latest_release().await?;
109        
110        // Cache the result
111        if let Ok(bytes) = serde_json::to_vec(&release) {
112            let mut cache = self.cache.write().await;
113            cache.insert(cache_key.to_string(), bytes);
114        }
115        
116        Ok(release)
117    }
118    
119    /// Get update recommendation
120    pub async fn get_recommendation(&self) -> Result<UpdateRecommendation> {
121        let current = self.check_current().await?;
122        let latest = self.get_latest_stable().await?;
123        
124        // Compare versions
125        if latest.version <= current.version {
126            return Ok(UpdateRecommendation::UpToDate);
127        }
128        
129        // Check if it's a security update
130        let is_security = latest.body.to_lowercase().contains("security") ||
131                         latest.name.to_lowercase().contains("security");
132        
133        if is_security {
134            return Ok(UpdateRecommendation::SecurityUpdate {
135                current: current.version.clone(),
136                latest: latest.version.clone(),
137                release_url: latest.html_url.clone(),
138                details: self.extract_security_details(&latest.body),
139            });
140        }
141        
142        // Check if it's a major update
143        if latest.version.major > current.version.major {
144            return Ok(UpdateRecommendation::MajorUpdate {
145                current: current.version.clone(),
146                latest: latest.version.clone(),
147                release_url: latest.html_url.clone(),
148            });
149        }
150        
151        // It's a minor/patch update
152        Ok(UpdateRecommendation::MinorUpdate {
153            current: current.version.clone(),
154            latest: latest.version.clone(),
155            release_url: latest.html_url.clone(),
156        })
157    }
158    
159    /// Get multiple recent releases
160    pub async fn get_recent_releases(&self, count: usize) -> Result<Vec<GitHubRelease>> {
161        self.github_client.get_releases(count).await
162    }
163    
164    fn extract_security_details(&self, body: &str) -> String {
165        // Extract security-related information from release notes
166        body.lines()
167            .filter(|line| {
168                let lower = line.to_lowercase();
169                lower.contains("security") || 
170                lower.contains("vulnerability") ||
171                lower.contains("cve-")
172            })
173            .take(3)
174            .collect::<Vec<_>>()
175            .join("\n")
176    }
177}
178
179impl Default for VersionManager {
180    fn default() -> Self {
181        Self::new().expect("Failed to create version manager")
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    
189    #[test]
190    fn test_channel_display() {
191        assert_eq!(Channel::Stable.to_string(), "stable");
192        assert_eq!(Channel::Beta.to_string(), "beta");
193        assert_eq!(Channel::Nightly.to_string(), "nightly");
194        assert_eq!(Channel::Custom("custom".to_string()).to_string(), "custom");
195    }
196}