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 enum UpdateRecommendation {
47 UpToDate,
49 MinorUpdate {
51 current: Version,
53 latest: Version,
55 release_url: String,
57 },
58 MajorUpdate {
60 current: Version,
62 latest: Version,
64 release_url: String,
66 },
67 SecurityUpdate {
69 current: Version,
71 latest: Version,
73 release_url: String,
75 details: String,
77 },
78}
79
80pub struct VersionManager {
82 github_client: GitHubClient,
83 cache: Arc<RwLock<cache::Cache<String, Vec<u8>>>>,
84}
85
86impl VersionManager {
87 pub fn new() -> Result<Self> {
89 let github_client = GitHubClient::new(None)?;
90 let cache = Arc::new(RwLock::new(cache::Cache::new(Duration::from_secs(3600))));
91
92 Ok(Self {
93 github_client,
94 cache,
95 })
96 }
97
98 pub async fn check_current(&self) -> Result<RustVersion> {
100 detector::detect_rust_version()
101 }
102
103 pub async fn get_latest_stable(&self) -> Result<GitHubRelease> {
105 let cache_key = "latest_stable";
107
108 {
109 let cache = self.cache.read().await;
110 if let Some(cached_bytes) = cache.get(&cache_key.to_string()) {
111 if let Ok(release) = serde_json::from_slice::<GitHubRelease>(&cached_bytes) {
112 return Ok(release);
113 }
114 }
115 }
116
117 let release = self.github_client.get_latest_release().await?;
119
120 if let Ok(bytes) = serde_json::to_vec(&release) {
122 let mut cache = self.cache.write().await;
123 cache.insert(cache_key.to_string(), bytes);
124 }
125
126 Ok(release)
127 }
128
129 pub async fn get_recommendation(&self) -> Result<UpdateRecommendation> {
131 let current = self.check_current().await?;
132 let latest = self.get_latest_stable().await?;
133
134 if latest.version <= current.version {
136 return Ok(UpdateRecommendation::UpToDate);
137 }
138
139 let is_security = latest.body.to_lowercase().contains("security")
141 || latest.name.to_lowercase().contains("security");
142
143 if is_security {
144 return Ok(UpdateRecommendation::SecurityUpdate {
145 current: current.version.clone(),
146 latest: latest.version.clone(),
147 release_url: latest.html_url.clone(),
148 details: self.extract_security_details(&latest.body),
149 });
150 }
151
152 if latest.version.major > current.version.major {
154 return Ok(UpdateRecommendation::MajorUpdate {
155 current: current.version.clone(),
156 latest: latest.version.clone(),
157 release_url: latest.html_url.clone(),
158 });
159 }
160
161 Ok(UpdateRecommendation::MinorUpdate {
163 current: current.version.clone(),
164 latest: latest.version.clone(),
165 release_url: latest.html_url.clone(),
166 })
167 }
168
169 pub async fn get_recent_releases(&self, count: usize) -> Result<Vec<GitHubRelease>> {
171 self.github_client.get_releases(count).await
172 }
173
174 fn extract_security_details(&self, body: &str) -> String {
175 body.lines()
177 .filter(|line| {
178 let lower = line.to_lowercase();
179 lower.contains("security")
180 || lower.contains("vulnerability")
181 || lower.contains("cve-")
182 })
183 .take(3)
184 .collect::<Vec<_>>()
185 .join("\n")
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_channel_display() {
195 assert_eq!(Channel::Stable.to_string(), "stable");
196 assert_eq!(Channel::Beta.to_string(), "beta");
197 assert_eq!(Channel::Nightly.to_string(), "nightly");
198 assert_eq!(Channel::Custom("custom".to_string()).to_string(), "custom");
199 }
200}