1use crate::{Result, VersionError, VersionInfo};
4use async_trait::async_trait;
5
6#[async_trait]
8pub trait VersionFetcher: Send + Sync {
9 fn tool_name(&self) -> &str;
11
12 async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>>;
14
15 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 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 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#[derive(Debug, Clone)]
36pub struct GitHubVersionFetcher {
37 owner: String,
38 repo: String,
39 tool_name: String,
40}
41
42impl GitHubVersionFetcher {
43 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 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 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#[derive(Debug, Clone)]
101pub struct NodeVersionFetcher {
102 tool_name: String,
103}
104
105impl NodeVersionFetcher {
106 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}