1use crate::platform::Platform;
4
5pub trait UrlBuilder: Send + Sync {
7 fn download_url(&self, version: &str) -> Option<String>;
8 fn versions_url(&self) -> &str;
9}
10
11#[derive(Debug, Clone)]
13pub struct NodeUrlBuilder;
14
15impl Default for NodeUrlBuilder {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl NodeUrlBuilder {
22 pub fn new() -> Self {
24 Self
25 }
26 pub fn download_url(version: &str) -> Option<String> {
28 let platform = Platform::current();
29 let (os, arch) = platform.node_platform_string()?;
30 let ext = platform.archive_extension();
31
32 Some(format!(
33 "https://nodejs.org/dist/v{}/node-v{}-{}-{}.{}",
34 version, version, os, arch, ext
35 ))
36 }
37
38 pub fn versions_url() -> &'static str {
40 "https://nodejs.org/dist/index.json"
41 }
42}
43
44impl UrlBuilder for NodeUrlBuilder {
45 fn download_url(&self, version: &str) -> Option<String> {
46 Self::download_url(version)
47 }
48
49 fn versions_url(&self) -> &str {
50 Self::versions_url()
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct GoUrlBuilder;
57
58impl Default for GoUrlBuilder {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl GoUrlBuilder {
65 pub fn new() -> Self {
67 Self
68 }
69 pub fn download_url(version: &str) -> Option<String> {
71 let platform = Platform::current();
72 let (os, arch) = platform.go_platform_string()?;
73 let ext = platform.archive_extension();
74
75 Some(format!(
76 "https://go.dev/dl/go{}.{}-{}.{}",
77 version, os, arch, ext
78 ))
79 }
80
81 pub fn versions_url() -> &'static str {
83 "https://go.dev/dl/?mode=json"
84 }
85}
86
87impl UrlBuilder for GoUrlBuilder {
88 fn download_url(&self, version: &str) -> Option<String> {
89 Self::download_url(version)
90 }
91
92 fn versions_url(&self) -> &str {
93 Self::versions_url()
94 }
95}
96
97#[derive(Debug, Clone)]
99pub struct RustUrlBuilder;
100
101impl Default for RustUrlBuilder {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107impl RustUrlBuilder {
108 pub fn new() -> Self {
110 Self
111 }
112 pub fn download_url(&self, _version: &str) -> Option<String> {
114 let platform = Platform::current();
115 match platform.os {
116 crate::platform::OperatingSystem::Windows => {
117 Some("https://win.rustup.rs/x86_64".to_string())
118 }
119 _ => Some("https://sh.rustup.rs".to_string()),
120 }
121 }
122
123 pub fn versions_url() -> &'static str {
125 "https://api.github.com/repos/rust-lang/rust/releases"
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct PythonUrlBuilder;
132
133impl Default for PythonUrlBuilder {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl PythonUrlBuilder {
140 pub fn new() -> Self {
142 Self
143 }
144 pub fn download_url(version: &str) -> Option<String> {
146 let platform = Platform::current();
147
148 let (os, arch) = match platform.os {
149 crate::platform::OperatingSystem::Windows => {
150 let arch = match platform.arch {
151 crate::platform::Architecture::X86_64 => "amd64",
152 crate::platform::Architecture::X86 => "win32",
153 _ => return None,
154 };
155 ("windows", arch)
156 }
157 crate::platform::OperatingSystem::MacOS => {
158 let arch = match platform.arch {
159 crate::platform::Architecture::X86_64 => "universal2",
160 crate::platform::Architecture::Aarch64 => "universal2",
161 _ => return None,
162 };
163 ("macos", arch)
164 }
165 crate::platform::OperatingSystem::Linux => {
166 let arch = match platform.arch {
167 crate::platform::Architecture::X86_64 => "x86_64",
168 crate::platform::Architecture::Aarch64 => "aarch64",
169 _ => return None,
170 };
171 ("linux", arch)
172 }
173 _ => return None,
174 };
175
176 let ext = if matches!(platform.os, crate::platform::OperatingSystem::Windows) {
177 "exe"
178 } else {
179 "tgz"
180 };
181
182 Some(format!(
183 "https://www.python.org/ftp/python/{}/python-{}-{}-{}.{}",
184 version, version, os, arch, ext
185 ))
186 }
187
188 pub fn versions_url() -> &'static str {
190 "https://api.github.com/repos/python/cpython/releases"
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct UvUrlBuilder;
197
198impl Default for UvUrlBuilder {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204impl UvUrlBuilder {
205 pub fn new() -> Self {
207 Self
208 }
209
210 pub fn download_url(version: &str) -> Option<String> {
212 let platform = Platform::current();
213
214 let filename = match platform.os {
215 crate::platform::OperatingSystem::Windows => "uv-x86_64-pc-windows-msvc.zip",
216 crate::platform::OperatingSystem::MacOS => "uv-x86_64-apple-darwin.tar.gz",
217 _ => "uv-x86_64-unknown-linux-gnu.tar.gz",
218 };
219
220 let tag = if version == "latest" {
223 return None;
225 } else {
226 version.to_string()
228 };
229
230 Some(format!(
231 "https://github.com/astral-sh/uv/releases/download/{}/{}",
232 tag, filename
233 ))
234 }
235
236 pub fn versions_url() -> &'static str {
238 "https://api.github.com/repos/astral-sh/uv/releases"
239 }
240}
241
242impl UrlBuilder for UvUrlBuilder {
243 fn download_url(&self, version: &str) -> Option<String> {
244 Self::download_url(version)
245 }
246
247 fn versions_url(&self) -> &str {
248 Self::versions_url()
249 }
250}
251
252pub struct GenericUrlBuilder;
254
255impl GenericUrlBuilder {
256 pub fn github_releases_url(owner: &str, repo: &str) -> String {
258 format!("https://api.github.com/repos/{}/{}/releases", owner, repo)
259 }
260
261 pub fn github_release_download_url(
263 owner: &str,
264 repo: &str,
265 tag: &str,
266 filename: &str,
267 ) -> String {
268 format!(
269 "https://github.com/{}/{}/releases/download/{}/{}",
270 owner, repo, tag, filename
271 )
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_node_url_builder() {
281 let url = NodeUrlBuilder::download_url("18.0.0");
282 assert!(url.is_some());
283
284 let url = url.unwrap();
285 assert!(url.contains("18.0.0"));
286 assert!(url.contains("nodejs.org"));
287
288 let versions_url = NodeUrlBuilder::versions_url();
289 assert!(versions_url.contains("nodejs.org"));
290 }
291
292 #[test]
293 fn test_go_url_builder() {
294 let url = GoUrlBuilder::download_url("1.21.0");
295 assert!(url.is_some());
296
297 let url = url.unwrap();
298 assert!(url.contains("1.21.0"));
299 assert!(url.contains("go.dev"));
300
301 let versions_url = GoUrlBuilder::versions_url();
302 assert!(versions_url.contains("go.dev"));
303 }
304
305 #[test]
306 fn test_rust_url_builder() {
307 let builder = RustUrlBuilder::new();
308 let url = builder.download_url("1.70.0");
309 assert!(url.is_some());
310
311 let url = url.unwrap();
312 assert!(url.contains("rustup"));
313
314 let versions_url = RustUrlBuilder::versions_url();
315 assert!(versions_url.contains("github.com"));
316 }
317
318 #[test]
319 fn test_python_url_builder() {
320 let url = PythonUrlBuilder::download_url("3.12.0");
321 assert!(url.is_some());
322
323 let url = url.unwrap();
324 assert!(url.contains("3.12.0"));
325 assert!(url.contains("python.org"));
326
327 let versions_url = PythonUrlBuilder::versions_url();
328 assert!(versions_url.contains("github.com"));
329 }
330
331 #[test]
332 fn test_generic_url_builder() {
333 let releases_url = GenericUrlBuilder::github_releases_url("owner", "repo");
334 assert_eq!(
335 releases_url,
336 "https://api.github.com/repos/owner/repo/releases"
337 );
338
339 let download_url = GenericUrlBuilder::github_release_download_url(
340 "owner",
341 "repo",
342 "v1.0.0",
343 "file.tar.gz",
344 );
345 assert_eq!(
346 download_url,
347 "https://github.com/owner/repo/releases/download/v1.0.0/file.tar.gz"
348 );
349 }
350}