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;
14pub mod detector;
15pub mod github;
16
17pub use detector::RustVersion;
18pub use github::{GitHubClient, GitHubRelease};
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub enum Channel {
23 Stable,
25 Beta,
27 Nightly,
29 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#[derive(Debug, Clone)]
46pub struct UpdateInfo {
47 pub current: Version,
49 pub latest: Version,
51 pub release_url: String,
53 pub security_details: Option<String>,
55}
56
57#[derive(Debug, Clone)]
59pub enum UpdateRecommendation {
60 UpToDate,
62 MinorUpdate(UpdateInfo),
64 MajorUpdate(UpdateInfo),
66 SecurityUpdate(UpdateInfo),
68}
69
70pub struct VersionManager {
72 github_client: GitHubClient,
73 cache: Arc<RwLock<cache::Cache<String, Vec<u8>>>>,
74}
75
76impl VersionManager {
77 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 pub async fn check_current(&self) -> Result<RustVersion> {
90 detector::detect_rust_version()
91 }
92
93 pub async fn get_latest_stable(&self) -> Result<GitHubRelease> {
95 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 let release = self.github_client.get_latest_release().await?;
109
110 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 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 if latest.version <= current.version {
126 return Ok(UpdateRecommendation::UpToDate);
127 }
128
129 self.determine_update_type(¤t, &latest)
131 }
132
133 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 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 fn is_major_update(&self, current: &RustVersion, latest: &GitHubRelease) -> bool {
157 latest.version.major > current.version.major
158 }
159
160 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 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 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 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 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}