vx_version/
fetcher.rs

1//! Version fetching traits and implementations
2
3use crate::{Result, VersionError, VersionInfo};
4use async_trait::async_trait;
5
6/// Trait for fetching version information from external sources
7#[async_trait]
8pub trait VersionFetcher: Send + Sync {
9    /// Get the name of the tool this fetcher supports
10    fn tool_name(&self) -> &str;
11
12    /// Fetch available versions for the tool
13    async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>>;
14
15    /// Get the latest stable version
16    async fn get_latest_version(&self) -> Result<Option<VersionInfo>> {
17        let versions = self.fetch_versions(false).await?;
18        Ok(versions.into_iter().next())
19    }
20
21    /// Get the latest version (including prereleases)
22    async fn get_latest_version_including_prerelease(&self) -> Result<Option<VersionInfo>> {
23        let versions = self.fetch_versions(true).await?;
24        Ok(versions.into_iter().next())
25    }
26
27    /// Check if a specific version exists
28    async fn version_exists(&self, version: &str) -> Result<bool> {
29        let versions = self.fetch_versions(true).await?;
30        Ok(versions.iter().any(|v| v.version == version))
31    }
32}
33
34/// Version fetcher for GitHub releases
35#[derive(Debug, Clone)]
36pub struct GitHubVersionFetcher {
37    owner: String,
38    repo: String,
39    tool_name: String,
40}
41
42impl GitHubVersionFetcher {
43    /// Create a new GitHubVersionFetcher
44    pub fn new(owner: &str, repo: &str) -> Self {
45        Self {
46            owner: owner.to_string(),
47            repo: repo.to_string(),
48            tool_name: repo.to_string(),
49        }
50    }
51
52    /// Create a new GitHubVersionFetcher with custom tool name
53    pub fn with_tool_name(owner: &str, repo: &str, tool_name: &str) -> Self {
54        Self {
55            owner: owner.to_string(),
56            repo: repo.to_string(),
57            tool_name: tool_name.to_string(),
58        }
59    }
60
61    /// Get the API URL for releases
62    pub fn releases_url(&self) -> String {
63        format!(
64            "https://api.github.com/repos/{}/{}/releases",
65            self.owner, self.repo
66        )
67    }
68}
69
70#[async_trait]
71impl VersionFetcher for GitHubVersionFetcher {
72    fn tool_name(&self) -> &str {
73        &self.tool_name
74    }
75
76    async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
77        let client = reqwest::Client::new();
78        let url = self.releases_url();
79
80        let response = client
81            .get(&url)
82            .header("User-Agent", "vx-version")
83            .send()
84            .await
85            .map_err(|e| VersionError::NetworkError {
86                url: url.clone(),
87                source: e,
88            })?;
89
90        let json: serde_json::Value = response
91            .json()
92            .await
93            .map_err(|e| VersionError::NetworkError { url, source: e })?;
94
95        crate::parser::GitHubVersionParser::parse_versions(&json, include_prerelease)
96    }
97}
98
99/// Version fetcher for Node.js releases
100#[derive(Debug, Clone)]
101pub struct NodeVersionFetcher {
102    tool_name: String,
103}
104
105impl NodeVersionFetcher {
106    /// Create a new NodeVersionFetcher
107    pub fn new() -> Self {
108        Self {
109            tool_name: "node".to_string(),
110        }
111    }
112}
113
114impl Default for NodeVersionFetcher {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120#[async_trait]
121impl VersionFetcher for NodeVersionFetcher {
122    fn tool_name(&self) -> &str {
123        &self.tool_name
124    }
125
126    async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
127        let client = reqwest::Client::new();
128        let url = "https://nodejs.org/dist/index.json";
129
130        let response = client
131            .get(url)
132            .header("User-Agent", "vx-version")
133            .send()
134            .await
135            .map_err(|e| VersionError::NetworkError {
136                url: url.to_string(),
137                source: e,
138            })?;
139
140        let json: serde_json::Value =
141            response
142                .json()
143                .await
144                .map_err(|e| VersionError::NetworkError {
145                    url: url.to_string(),
146                    source: e,
147                })?;
148
149        crate::parser::NodeVersionParser::parse_versions(&json, include_prerelease)
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_github_fetcher_creation() {
159        let fetcher = GitHubVersionFetcher::new("astral-sh", "uv");
160        assert_eq!(fetcher.tool_name(), "uv");
161        assert_eq!(
162            fetcher.releases_url(),
163            "https://api.github.com/repos/astral-sh/uv/releases"
164        );
165    }
166
167    #[test]
168    fn test_github_fetcher_with_custom_name() {
169        let fetcher = GitHubVersionFetcher::with_tool_name("astral-sh", "uv", "python-uv");
170        assert_eq!(fetcher.tool_name(), "python-uv");
171    }
172
173    #[test]
174    fn test_node_fetcher_creation() {
175        let fetcher = NodeVersionFetcher::new();
176        assert_eq!(fetcher.tool_name(), "node");
177    }
178}