vx_core/
version_parser.rs

1//! Version parsing utilities for different tools
2
3use crate::{Result, VersionInfo};
4use serde_json::Value;
5
6/// Temporary VersionParser trait during migration
7pub trait VersionParser: Send + Sync {
8    fn parse_versions(&self, json: &Value, include_prerelease: bool) -> Result<Vec<VersionInfo>>;
9}
10
11/// Version parser for Node.js
12#[derive(Debug, Clone)]
13pub struct NodeVersionParser;
14
15impl Default for NodeVersionParser {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl NodeVersionParser {
22    /// Create a new NodeVersionParser instance
23    pub fn new() -> Self {
24        Self
25    }
26    /// Parse Node.js versions from the official API response
27    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                // Add download URL
74                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
94/// Version parser for Go
95pub struct GoVersionParser;
96
97impl GoVersionParser {
98    /// Parse Go versions from the official API response
99    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                // Check if it's stable
122                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                // Add download URL
137                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/// Version parser for GitHub releases (used by Rust, Python, etc.)
158#[derive(Debug, Clone)]
159pub struct GitHubVersionParser {
160    owner: String,
161    repo: String,
162}
163
164impl GitHubVersionParser {
165    /// Create a new GitHubVersionParser instance
166    pub fn new(owner: &str, repo: &str) -> Self {
167        Self {
168            owner: owner.to_string(),
169            repo: repo.to_string(),
170        }
171    }
172
173    /// Get the versions URL for this repository
174    pub fn versions_url(&self) -> String {
175        format!(
176            "https://api.github.com/repos/{}/{}/releases",
177            self.owner, self.repo
178        )
179    }
180    /// Parse versions from GitHub releases API response
181    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                    // Truncate long release notes
204                    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        // Sort versions in descending order (latest first)
226        // This ensures that the first version in the list is the latest
227        versions.sort_by(|a, b| {
228            // Use semantic version comparison for better sorting
229            // Parse versions for comparison
230            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), // Descending order
235                _ => {
236                    // Fallback to string comparison if semantic parsing fails
237                    b.version.cmp(&a.version)
238                }
239            }
240        });
241
242        Ok(versions)
243    }
244
245    /// Parse a semantic version string into comparable components
246    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            // Handle patch version with possible suffix (e.g., "1-alpha")
266            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
292/// Generic version parser utilities
293pub struct VersionParserUtils;
294
295impl VersionParserUtils {
296    /// Check if a version string indicates a prerelease
297    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    /// Clean version string (remove prefixes like 'v', 'go', etc.)
307    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    /// Sort versions in descending order (latest first)
319    pub fn sort_versions_desc(mut versions: Vec<VersionInfo>) -> Vec<VersionInfo> {
320        versions.sort_by(|a, b| {
321            // Simple string comparison for now
322            // TODO: Implement proper semantic version comparison
323            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        // Should be sorted in descending order (latest first)
400        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        // Test valid semantic versions
408        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        // Test invalid versions
421        let result = GitHubVersionParser::parse_semantic_version("invalid");
422        assert!(result.is_err());
423    }
424}