ferrous_forge/rust_version/
mod.rs1use 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;
15pub mod detector;
17pub mod github;
19
20pub use detector::RustVersion;
21pub use github::{GitHubClient, GitHubRelease};
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub enum Channel {
26 Stable,
28 Beta,
30 Nightly,
32 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#[derive(Debug, Clone)]
49pub struct UpdateInfo {
50 pub current: Version,
52 pub latest: Version,
54 pub release_url: String,
56 pub security_details: Option<String>,
58}
59
60#[derive(Debug, Clone)]
62pub enum UpdateRecommendation {
63 UpToDate,
65 MinorUpdate(UpdateInfo),
67 MajorUpdate(UpdateInfo),
69 SecurityUpdate(UpdateInfo),
71}
72
73pub struct VersionManager {
75 github_client: GitHubClient,
76 cache: Arc<RwLock<cache::Cache<String, Vec<u8>>>>,
77}
78
79impl VersionManager {
80 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 pub async fn check_current(&self) -> Result<RustVersion> {
101 detector::detect_rust_version()
102 }
103
104 pub async fn get_latest_stable(&self) -> Result<GitHubRelease> {
110 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 let release = self.github_client.get_latest_release().await?;
124
125 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 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 if latest.version <= current.version {
146 return Ok(UpdateRecommendation::UpToDate);
147 }
148
149 self.determine_update_type(¤t, &latest)
151 }
152
153 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 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 fn is_major_update(&self, current: &RustVersion, latest: &GitHubRelease) -> bool {
177 latest.version.major > current.version.major
178 }
179
180 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 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 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 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 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}