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/// Update information for available version
45#[derive(Debug, Clone)]
46pub struct UpdateInfo {
47    /// Current Rust version
48    pub current: Version,
49    /// Latest available version
50    pub latest: Version,
51    /// URL to the release page
52    pub release_url: String,
53    /// Security update details (if applicable)
54    pub security_details: Option<String>,
55}
56
57/// Version update recommendation
58#[derive(Debug, Clone)]
59pub enum UpdateRecommendation {
60    /// Already on latest version
61    UpToDate,
62    /// Minor update available
63    MinorUpdate(UpdateInfo),
64    /// Major update available
65    MajorUpdate(UpdateInfo),
66    /// Security update available
67    SecurityUpdate(UpdateInfo),
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        // Check if already up to date
125        if latest.version <= current.version {
126            return Ok(UpdateRecommendation::UpToDate);
127        }
128
129        // Determine update type based on release content and version difference
130        self.determine_update_type(&current, &latest)
131    }
132
133    /// Determine the type of update based on version comparison and release content
134    fn determine_update_type(
135        &self,
136        current: &RustVersion,
137        latest: &GitHubRelease,
138    ) -> Result<UpdateRecommendation> {
139        if self.is_security_update(latest) {
140            Ok(self.create_security_update(current, latest))
141        } else if self.is_major_update(current, latest) {
142            Ok(self.create_major_update(current, latest))
143        } else {
144            Ok(self.create_minor_update(current, latest))
145        }
146    }
147
148    /// Check if the release contains security-related updates
149    fn is_security_update(&self, release: &GitHubRelease) -> bool {
150        let body_lower = release.body.to_lowercase();
151        let name_lower = release.name.to_lowercase();
152        body_lower.contains("security") || name_lower.contains("security")
153    }
154
155    /// Check if this is a major version update
156    fn is_major_update(&self, current: &RustVersion, latest: &GitHubRelease) -> bool {
157        latest.version.major > current.version.major
158    }
159
160    /// Create a security update recommendation
161    fn create_security_update(
162        &self,
163        current: &RustVersion,
164        latest: &GitHubRelease,
165    ) -> UpdateRecommendation {
166        let info = UpdateInfo {
167            current: current.version.clone(),
168            latest: latest.version.clone(),
169            release_url: latest.html_url.clone(),
170            security_details: Some(self.extract_security_details(&latest.body)),
171        };
172        UpdateRecommendation::SecurityUpdate(info)
173    }
174
175    /// Create a major update recommendation
176    fn create_major_update(
177        &self,
178        current: &RustVersion,
179        latest: &GitHubRelease,
180    ) -> UpdateRecommendation {
181        let info = UpdateInfo {
182            current: current.version.clone(),
183            latest: latest.version.clone(),
184            release_url: latest.html_url.clone(),
185            security_details: None,
186        };
187        UpdateRecommendation::MajorUpdate(info)
188    }
189
190    /// Create a minor/patch update recommendation
191    fn create_minor_update(
192        &self,
193        current: &RustVersion,
194        latest: &GitHubRelease,
195    ) -> UpdateRecommendation {
196        let info = UpdateInfo {
197            current: current.version.clone(),
198            latest: latest.version.clone(),
199            release_url: latest.html_url.clone(),
200            security_details: None,
201        };
202        UpdateRecommendation::MinorUpdate(info)
203    }
204
205    /// Get multiple recent releases
206    pub async fn get_recent_releases(&self, count: usize) -> Result<Vec<GitHubRelease>> {
207        self.github_client.get_releases(count).await
208    }
209
210    fn extract_security_details(&self, body: &str) -> String {
211        // Extract security-related information from release notes
212        body.lines()
213            .filter(|line| {
214                let lower = line.to_lowercase();
215                lower.contains("security")
216                    || lower.contains("vulnerability")
217                    || lower.contains("cve-")
218            })
219            .take(3)
220            .collect::<Vec<_>>()
221            .join("\n")
222    }
223}
224
225#[cfg(test)]
226#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_channel_display() {
232        assert_eq!(Channel::Stable.to_string(), "stable");
233        assert_eq!(Channel::Beta.to_string(), "beta");
234        assert_eq!(Channel::Nightly.to_string(), "nightly");
235        assert_eq!(Channel::Custom("custom".to_string()).to_string(), "custom");
236    }
237}