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,
52 latest: Version,
53 release_url: String,
54 },
55 MajorUpdate {
57 current: Version,
58 latest: Version,
59 release_url: String,
60 },
61 SecurityUpdate {
63 current: Version,
64 latest: Version,
65 release_url: String,
66 details: String,
67 },
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 let is_security = latest.body.to_lowercase().contains("security") ||
131 latest.name.to_lowercase().contains("security");
132
133 if is_security {
134 return Ok(UpdateRecommendation::SecurityUpdate {
135 current: current.version.clone(),
136 latest: latest.version.clone(),
137 release_url: latest.html_url.clone(),
138 details: self.extract_security_details(&latest.body),
139 });
140 }
141
142 if latest.version.major > current.version.major {
144 return Ok(UpdateRecommendation::MajorUpdate {
145 current: current.version.clone(),
146 latest: latest.version.clone(),
147 release_url: latest.html_url.clone(),
148 });
149 }
150
151 Ok(UpdateRecommendation::MinorUpdate {
153 current: current.version.clone(),
154 latest: latest.version.clone(),
155 release_url: latest.html_url.clone(),
156 })
157 }
158
159 pub async fn get_recent_releases(&self, count: usize) -> Result<Vec<GitHubRelease>> {
161 self.github_client.get_releases(count).await
162 }
163
164 fn extract_security_details(&self, body: &str) -> String {
165 body.lines()
167 .filter(|line| {
168 let lower = line.to_lowercase();
169 lower.contains("security") ||
170 lower.contains("vulnerability") ||
171 lower.contains("cve-")
172 })
173 .take(3)
174 .collect::<Vec<_>>()
175 .join("\n")
176 }
177}
178
179impl Default for VersionManager {
180 fn default() -> Self {
181 Self::new().expect("Failed to create version manager")
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_channel_display() {
191 assert_eq!(Channel::Stable.to_string(), "stable");
192 assert_eq!(Channel::Beta.to_string(), "beta");
193 assert_eq!(Channel::Nightly.to_string(), "nightly");
194 assert_eq!(Channel::Custom("custom".to_string()).to_string(), "custom");
195 }
196}