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