vx_version/
parser.rs

1//! Version parsing utilities for different tools
2
3use crate::{Result, VersionError, VersionInfo};
4use serde_json::Value;
5
6/// Trait for parsing versions from JSON responses
7pub trait VersionParser: Send + Sync {
8    /// Parse versions from JSON data
9    fn parse_versions(&self, json: &Value, include_prerelease: bool) -> Result<Vec<VersionInfo>>;
10}
11
12/// Version parser for Node.js releases
13#[derive(Debug, Clone)]
14pub struct NodeVersionParser;
15
16impl Default for NodeVersionParser {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl NodeVersionParser {
23    /// Create a new NodeVersionParser instance
24    pub fn new() -> Self {
25        Self
26    }
27
28    /// Parse Node.js versions from the official API response
29    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/// Version parser for Go releases
93#[derive(Debug, Clone)]
94pub struct GoVersionParser;
95
96impl GoVersionParser {
97    /// Create a new GoVersionParser instance
98    pub fn new() -> Self {
99        Self
100    }
101
102    /// Parse Go versions from the official API response
103    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                // Check if it's stable
126                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/// Version parser for GitHub releases (used by Rust, Python, etc.)
163#[derive(Debug, Clone)]
164pub struct GitHubVersionParser {
165    owner: String,
166    repo: String,
167}
168
169impl GitHubVersionParser {
170    /// Create a new GitHubVersionParser instance
171    pub fn new(owner: &str, repo: &str) -> Self {
172        Self {
173            owner: owner.to_string(),
174            repo: repo.to_string(),
175        }
176    }
177
178    /// Get the versions URL for this repository
179    pub fn versions_url(&self) -> String {
180        format!(
181            "https://api.github.com/repos/{}/{}/releases",
182            self.owner, self.repo
183        )
184    }
185
186    /// Parse versions from GitHub releases API response
187    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                    // Truncate long release notes
210                    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        // Sort versions in descending order (latest first)
236        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), // Descending order
242                _ => b.version.cmp(&a.version),  // Fallback to string comparison
243            }
244        });
245
246        Ok(versions)
247    }
248
249    /// Parse a semantic version string into comparable components
250    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}
309/// Generic version parser utilities
310pub struct VersionParserUtils;
311
312impl VersionParserUtils {
313    /// Check if a version string indicates a prerelease
314    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    /// Clean version string (remove prefixes like 'v', 'go', etc.)
324    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    /// Sort versions in descending order (latest first)
336    pub fn sort_versions_desc(mut versions: Vec<VersionInfo>) -> Vec<VersionInfo> {
337        versions.sort_by(|a, b| {
338            // Simple string comparison for now
339            // TODO: Implement proper semantic version comparison
340            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        // Should be sorted in descending order (latest first)
411        assert_eq!(versions[0].version, "0.7.13");
412        assert_eq!(versions[1].version, "0.7.10");
413    }
414}