1use crate::{Result, VersionInfo};
4use serde_json::Value;
5
6pub trait VersionParser: Send + Sync {
8 fn parse_versions(&self, json: &Value, include_prerelease: bool) -> Result<Vec<VersionInfo>>;
9}
10
11#[derive(Debug, Clone)]
13pub struct NodeVersionParser;
14
15impl Default for NodeVersionParser {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl NodeVersionParser {
22 pub fn new() -> Self {
24 Self
25 }
26 pub fn parse_versions(json: &Value, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
28 let mut versions = Vec::new();
29
30 if let Some(releases_array) = json.as_array() {
31 for release in releases_array {
32 let version = release["version"]
33 .as_str()
34 .unwrap_or("")
35 .trim_start_matches('v')
36 .to_string();
37
38 if version.is_empty() {
39 continue;
40 }
41
42 let is_prerelease =
43 version.contains("alpha") || version.contains("beta") || version.contains("rc");
44
45 if !include_prerelease && is_prerelease {
46 continue;
47 }
48
49 let release_date = release["date"].as_str().map(|s| s.to_string());
50 let lts_info = release["lts"].as_str();
51 let is_lts = lts_info.is_some() && lts_info != Some("false");
52
53 let mut version_info = VersionInfo::new(version).with_prerelease(is_prerelease);
54
55 if let Some(date) = release_date {
56 version_info = version_info.with_release_date(date);
57 }
58
59 if is_lts {
60 let release_notes = format!("LTS release ({})", lts_info.unwrap_or("LTS"));
61 version_info = version_info
62 .with_release_notes(release_notes)
63 .with_metadata("lts".to_string(), "true".to_string());
64
65 if let Some(lts_name) = lts_info {
66 version_info = version_info
67 .with_metadata("lts_name".to_string(), lts_name.to_string());
68 }
69 } else {
70 version_info = version_info.with_release_notes("Current release".to_string());
71 }
72
73 if let Some(download_url) =
75 crate::url_builder::NodeUrlBuilder::download_url(&version_info.version)
76 {
77 version_info = version_info.with_download_url(download_url);
78 }
79
80 versions.push(version_info);
81 }
82 }
83
84 Ok(versions)
85 }
86}
87
88impl VersionParser for NodeVersionParser {
89 fn parse_versions(&self, json: &Value, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
90 Self::parse_versions(json, include_prerelease)
91 }
92}
93
94pub struct GoVersionParser;
96
97impl GoVersionParser {
98 pub fn parse_versions(json: &Value, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
100 let mut versions = Vec::new();
101
102 if let Some(releases_array) = json.as_array() {
103 for release in releases_array {
104 let version = release["version"]
105 .as_str()
106 .unwrap_or("")
107 .trim_start_matches("go")
108 .to_string();
109
110 if version.is_empty() {
111 continue;
112 }
113
114 let is_prerelease =
115 version.contains("beta") || version.contains("rc") || version.contains("alpha");
116
117 if !include_prerelease && is_prerelease {
118 continue;
119 }
120
121 let stable = release["stable"].as_bool().unwrap_or(false);
123 if !include_prerelease && !stable {
124 continue;
125 }
126
127 let mut version_info = VersionInfo::new(version)
128 .with_prerelease(is_prerelease)
129 .with_release_notes("Go release".to_string());
130
131 if stable {
132 version_info =
133 version_info.with_metadata("stable".to_string(), "true".to_string());
134 }
135
136 if let Some(download_url) =
138 crate::url_builder::GoUrlBuilder::download_url(&version_info.version)
139 {
140 version_info = version_info.with_download_url(download_url);
141 }
142
143 versions.push(version_info);
144 }
145 }
146
147 Ok(versions)
148 }
149}
150
151impl VersionParser for GoVersionParser {
152 fn parse_versions(&self, json: &Value, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
153 Self::parse_versions(json, include_prerelease)
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct GitHubVersionParser {
160 owner: String,
161 repo: String,
162}
163
164impl GitHubVersionParser {
165 pub fn new(owner: &str, repo: &str) -> Self {
167 Self {
168 owner: owner.to_string(),
169 repo: repo.to_string(),
170 }
171 }
172
173 pub fn versions_url(&self) -> String {
175 format!(
176 "https://api.github.com/repos/{}/{}/releases",
177 self.owner, self.repo
178 )
179 }
180 pub fn parse_versions(json: &Value, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
182 let mut versions = Vec::new();
183
184 if let Some(releases_array) = json.as_array() {
185 for release in releases_array {
186 let version = release["tag_name"].as_str().unwrap_or("").to_string();
187
188 if version.is_empty() {
189 continue;
190 }
191
192 let is_prerelease = release["prerelease"].as_bool().unwrap_or(false);
193
194 if !include_prerelease && is_prerelease {
195 continue;
196 }
197
198 let release_date = release["published_at"]
199 .as_str()
200 .map(|s| s.split('T').next().unwrap_or(s).to_string());
201
202 let release_notes = release["body"].as_str().map(|s| {
203 if s.len() > 200 {
205 format!("{}...", &s[..197])
206 } else {
207 s.to_string()
208 }
209 });
210
211 let mut version_info = VersionInfo::new(version).with_prerelease(is_prerelease);
212
213 if let Some(date) = release_date {
214 version_info = version_info.with_release_date(date);
215 }
216
217 if let Some(notes) = release_notes {
218 version_info = version_info.with_release_notes(notes);
219 }
220
221 versions.push(version_info);
222 }
223 }
224
225 versions.sort_by(|a, b| {
228 let version_a = Self::parse_semantic_version(&a.version);
231 let version_b = Self::parse_semantic_version(&b.version);
232
233 match (version_a, version_b) {
234 (Ok(va), Ok(vb)) => vb.cmp(&va), _ => {
236 b.version.cmp(&a.version)
238 }
239 }
240 });
241
242 Ok(versions)
243 }
244
245 fn parse_semantic_version(version: &str) -> Result<(u32, u32, u32, String)> {
247 let clean_version = version.trim_start_matches('v');
248 let parts: Vec<&str> = clean_version.split('.').collect();
249
250 if parts.len() < 2 {
251 return Err(crate::VxError::Other {
252 message: format!("Invalid version format: {}", version),
253 });
254 }
255
256 let major = parts[0].parse::<u32>().map_err(|_| crate::VxError::Other {
257 message: format!("Invalid major version: {}", parts[0]),
258 })?;
259
260 let minor = parts[1].parse::<u32>().map_err(|_| crate::VxError::Other {
261 message: format!("Invalid minor version: {}", parts[1]),
262 })?;
263
264 let (patch, suffix) = if parts.len() > 2 {
265 let patch_part = parts[2];
267 if let Some(dash_pos) = patch_part.find('-') {
268 let patch_num =
269 patch_part[..dash_pos]
270 .parse::<u32>()
271 .map_err(|_| crate::VxError::Other {
272 message: format!("Invalid patch version: {}", &patch_part[..dash_pos]),
273 })?;
274 let suffix = patch_part[dash_pos..].to_string();
275 (patch_num, suffix)
276 } else {
277 let patch_num = patch_part
278 .parse::<u32>()
279 .map_err(|_| crate::VxError::Other {
280 message: format!("Invalid patch version: {}", patch_part),
281 })?;
282 (patch_num, String::new())
283 }
284 } else {
285 (0, String::new())
286 };
287
288 Ok((major, minor, patch, suffix))
289 }
290}
291
292pub struct VersionParserUtils;
294
295impl VersionParserUtils {
296 pub fn is_prerelease(version: &str) -> bool {
298 version.contains("alpha")
299 || version.contains("beta")
300 || version.contains("rc")
301 || version.contains("pre")
302 || version.contains("dev")
303 || version.contains("snapshot")
304 }
305
306 pub fn clean_version(version: &str, prefixes: &[&str]) -> String {
308 let mut cleaned = version.to_string();
309 for prefix in prefixes {
310 if cleaned.starts_with(prefix) {
311 cleaned = cleaned[prefix.len()..].to_string();
312 break;
313 }
314 }
315 cleaned
316 }
317
318 pub fn sort_versions_desc(mut versions: Vec<VersionInfo>) -> Vec<VersionInfo> {
320 versions.sort_by(|a, b| {
321 b.version.cmp(&a.version)
324 });
325 versions
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use serde_json::json;
333
334 #[test]
335 fn test_version_parser_utils() {
336 assert!(VersionParserUtils::is_prerelease("1.0.0-alpha"));
337 assert!(VersionParserUtils::is_prerelease("2.0.0-beta.1"));
338 assert!(VersionParserUtils::is_prerelease("3.0.0-rc.1"));
339 assert!(!VersionParserUtils::is_prerelease("1.0.0"));
340
341 assert_eq!(VersionParserUtils::clean_version("v1.0.0", &["v"]), "1.0.0");
342 assert_eq!(
343 VersionParserUtils::clean_version("go1.21.0", &["go"]),
344 "1.21.0"
345 );
346 assert_eq!(
347 VersionParserUtils::clean_version("1.0.0", &["v", "go"]),
348 "1.0.0"
349 );
350 }
351
352 #[test]
353 fn test_node_version_parser() {
354 let json = json!([
355 {
356 "version": "v18.0.0",
357 "date": "2022-04-19",
358 "lts": false
359 },
360 {
361 "version": "v16.20.0",
362 "date": "2023-03-28",
363 "lts": "Gallium"
364 }
365 ]);
366
367 let versions = NodeVersionParser::parse_versions(&json, false).unwrap();
368 assert_eq!(versions.len(), 2);
369 assert_eq!(versions[0].version, "18.0.0");
370 assert_eq!(versions[1].version, "16.20.0");
371 assert_eq!(versions[1].metadata.get("lts"), Some(&"true".to_string()));
372 }
373
374 #[test]
375 fn test_github_version_parser_sorting() {
376 let json = json!([
377 {
378 "tag_name": "0.7.10",
379 "prerelease": false,
380 "published_at": "2024-01-10T00:00:00Z",
381 "body": "Release notes for 0.7.10"
382 },
383 {
384 "tag_name": "0.7.13",
385 "prerelease": false,
386 "published_at": "2024-01-13T00:00:00Z",
387 "body": "Release notes for 0.7.13"
388 },
389 {
390 "tag_name": "0.7.11",
391 "prerelease": false,
392 "published_at": "2024-01-11T00:00:00Z",
393 "body": "Release notes for 0.7.11"
394 }
395 ]);
396
397 let versions = GitHubVersionParser::parse_versions(&json, false).unwrap();
398 assert_eq!(versions.len(), 3);
399 assert_eq!(versions[0].version, "0.7.13");
401 assert_eq!(versions[1].version, "0.7.11");
402 assert_eq!(versions[2].version, "0.7.10");
403 }
404
405 #[test]
406 fn test_semantic_version_parsing() {
407 let result = GitHubVersionParser::parse_semantic_version("1.2.3");
409 assert!(result.is_ok());
410 assert_eq!(result.unwrap(), (1, 2, 3, String::new()));
411
412 let result = GitHubVersionParser::parse_semantic_version("v2.0.1");
413 assert!(result.is_ok());
414 assert_eq!(result.unwrap(), (2, 0, 1, String::new()));
415
416 let result = GitHubVersionParser::parse_semantic_version("0.7.13-alpha");
417 assert!(result.is_ok());
418 assert_eq!(result.unwrap(), (0, 7, 13, "-alpha".to_string()));
419
420 let result = GitHubVersionParser::parse_semantic_version("invalid");
422 assert!(result.is_err());
423 }
424}