Skip to main content

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
13/// Version information caching with TTL support.
14pub mod cache;
15/// Installed Rust version detection.
16pub mod detector;
17/// GitHub API client for fetching Rust releases.
18pub mod github;
19
20pub use detector::RustVersion;
21pub use github::{GitHubClient, GitHubRelease};
22
23/// Rust release channel
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub enum Channel {
26    /// Stable releases
27    Stable,
28    /// Beta releases
29    Beta,
30    /// Nightly builds
31    Nightly,
32    /// Custom or unknown channel
33    Custom(String),
34}
35
36impl std::fmt::Display for Channel {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            Self::Stable => write!(f, "stable"),
40            Self::Beta => write!(f, "beta"),
41            Self::Nightly => write!(f, "nightly"),
42            Self::Custom(s) => write!(f, "{}", s),
43        }
44    }
45}
46
47/// Update information for available version
48#[derive(Debug, Clone)]
49pub struct UpdateInfo {
50    /// Current Rust version
51    pub current: Version,
52    /// Latest available version
53    pub latest: Version,
54    /// URL to the release page
55    pub release_url: String,
56    /// Security update details (if applicable)
57    pub security_details: Option<String>,
58}
59
60/// Version update recommendation
61#[derive(Debug, Clone)]
62pub enum UpdateRecommendation {
63    /// Already on latest version
64    UpToDate,
65    /// Minor update available
66    MinorUpdate(UpdateInfo),
67    /// Major update available
68    MajorUpdate(UpdateInfo),
69    /// Security update available
70    SecurityUpdate(UpdateInfo),
71}
72
73/// Version manager for checking and recommending updates
74pub struct VersionManager {
75    github_client: GitHubClient,
76    cache: Arc<RwLock<cache::Cache<String, Vec<u8>>>>,
77}
78
79impl VersionManager {
80    /// Create a new version manager
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if the HTTP client cannot be constructed.
85    pub fn new() -> Result<Self> {
86        let github_client = GitHubClient::new(None)?;
87        let cache = Arc::new(RwLock::new(cache::Cache::new(Duration::from_secs(3600))));
88
89        Ok(Self {
90            github_client,
91            cache,
92        })
93    }
94
95    /// Check current Rust installation
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if `rustc` is not found or its output cannot be parsed.
100    pub async fn check_current(&self) -> Result<RustVersion> {
101        detector::detect_rust_version()
102    }
103
104    /// Get latest stable release
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if the GitHub API request fails or the response cannot be parsed.
109    pub async fn get_latest_stable(&self) -> Result<GitHubRelease> {
110        // Check cache first
111        let cache_key = "latest_stable";
112
113        {
114            let cache = self.cache.read().await;
115            if let Some(cached_bytes) = cache.get(&cache_key.to_string())
116                && let Ok(release) = serde_json::from_slice::<GitHubRelease>(&cached_bytes)
117            {
118                return Ok(release);
119            }
120        }
121
122        // Fetch from GitHub
123        let release = self.github_client.get_latest_release().await?;
124
125        // Cache the result
126        if let Ok(bytes) = serde_json::to_vec(&release) {
127            let mut cache = self.cache.write().await;
128            cache.insert(cache_key.to_string(), bytes);
129        }
130
131        Ok(release)
132    }
133
134    /// Get update recommendation
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if the current version cannot be detected or the latest
139    /// release cannot be fetched from GitHub.
140    pub async fn get_recommendation(&self) -> Result<UpdateRecommendation> {
141        let current = self.check_current().await?;
142        let latest = self.get_latest_stable().await?;
143
144        // Check if already up to date
145        if latest.version <= current.version {
146            return Ok(UpdateRecommendation::UpToDate);
147        }
148
149        // Determine update type based on release content and version difference
150        self.determine_update_type(&current, &latest)
151    }
152
153    /// Determine the type of update based on version comparison and release content
154    fn determine_update_type(
155        &self,
156        current: &RustVersion,
157        latest: &GitHubRelease,
158    ) -> Result<UpdateRecommendation> {
159        if self.is_security_update(latest) {
160            Ok(self.create_security_update(current, latest))
161        } else if self.is_major_update(current, latest) {
162            Ok(self.create_major_update(current, latest))
163        } else {
164            Ok(self.create_minor_update(current, latest))
165        }
166    }
167
168    /// Check if the release contains security-related updates
169    fn is_security_update(&self, release: &GitHubRelease) -> bool {
170        let body_lower = release.body.to_lowercase();
171        let name_lower = release.name.to_lowercase();
172        body_lower.contains("security") || name_lower.contains("security")
173    }
174
175    /// Check if this is a major version update
176    fn is_major_update(&self, current: &RustVersion, latest: &GitHubRelease) -> bool {
177        latest.version.major > current.version.major
178    }
179
180    /// Create a security update recommendation
181    fn create_security_update(
182        &self,
183        current: &RustVersion,
184        latest: &GitHubRelease,
185    ) -> UpdateRecommendation {
186        let info = UpdateInfo {
187            current: current.version.clone(),
188            latest: latest.version.clone(),
189            release_url: latest.html_url.clone(),
190            security_details: Some(self.extract_security_details(&latest.body)),
191        };
192        UpdateRecommendation::SecurityUpdate(info)
193    }
194
195    /// Create a major update recommendation
196    fn create_major_update(
197        &self,
198        current: &RustVersion,
199        latest: &GitHubRelease,
200    ) -> UpdateRecommendation {
201        let info = UpdateInfo {
202            current: current.version.clone(),
203            latest: latest.version.clone(),
204            release_url: latest.html_url.clone(),
205            security_details: None,
206        };
207        UpdateRecommendation::MajorUpdate(info)
208    }
209
210    /// Create a minor/patch update recommendation
211    fn create_minor_update(
212        &self,
213        current: &RustVersion,
214        latest: &GitHubRelease,
215    ) -> UpdateRecommendation {
216        let info = UpdateInfo {
217            current: current.version.clone(),
218            latest: latest.version.clone(),
219            release_url: latest.html_url.clone(),
220            security_details: None,
221        };
222        UpdateRecommendation::MinorUpdate(info)
223    }
224
225    /// Get multiple recent releases
226    ///
227    /// # Errors
228    ///
229    /// Returns an error if the GitHub API request fails or the response cannot be parsed.
230    pub async fn get_recent_releases(&self, count: usize) -> Result<Vec<GitHubRelease>> {
231        self.github_client.get_releases(count).await
232    }
233
234    fn extract_security_details(&self, body: &str) -> String {
235        // Extract security-related information from release notes
236        body.lines()
237            .filter(|line| {
238                let lower = line.to_lowercase();
239                lower.contains("security")
240                    || lower.contains("vulnerability")
241                    || lower.contains("cve-")
242            })
243            .take(3)
244            .collect::<Vec<_>>()
245            .join("\n")
246    }
247}
248
249#[cfg(test)]
250#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_channel_display() {
256        assert_eq!(Channel::Stable.to_string(), "stable");
257        assert_eq!(Channel::Beta.to_string(), "beta");
258        assert_eq!(Channel::Nightly.to_string(), "nightly");
259        assert_eq!(Channel::Custom("custom".to_string()).to_string(), "custom");
260    }
261}