1#![doc = include_str!("../README.md")]
2
3mod arch;
4mod error;
5mod ext;
6mod os;
7mod specs;
8mod url;
9
10pub use crate::arch::NodeJsArch;
11pub use crate::error::NodeJsRelInfoError;
12pub use crate::ext::NodeJsPkgExt;
13pub use crate::os::NodeJsOs;
14use crate::url::NodeJsUrlFormatter;
15#[cfg(feature = "json")]
16use serde::{Deserialize, Serialize};
17use std::string::ToString;
18
19#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
27#[cfg_attr(feature = "json", derive(Deserialize, Serialize))]
28pub struct NodeJsRelInfo {
29 pub os: NodeJsOs,
31 pub arch: NodeJsArch,
33 pub ext: NodeJsPkgExt,
35 pub version: String,
37 pub filename: String,
39 pub sha256: String,
41 pub url: String,
43 #[cfg_attr(feature = "json", serde(skip))]
44 url_fmt: NodeJsUrlFormatter,
45}
46
47impl NodeJsRelInfo {
48 pub fn new<T: AsRef<str>>(semver: T) -> Self {
61 NodeJsRelInfo {
62 version: semver.as_ref().to_owned(),
63 ..Default::default()
64 }
65 }
66
67 pub fn from_env<T: AsRef<str>>(semver: T) -> Result<NodeJsRelInfo, NodeJsRelInfoError> {
80 let mut info = NodeJsRelInfo::new(semver);
81 info.os = NodeJsOs::from_env()?;
82 info.arch = NodeJsArch::from_env()?;
83 info.ext = match info.os {
84 NodeJsOs::Windows => NodeJsPkgExt::Zip,
85 _ => NodeJsPkgExt::Targz,
86 };
87 Ok(info)
88 }
89
90 pub fn macos(mut self) -> Self {
99 self.os = NodeJsOs::Darwin;
100 self
101 }
102
103 pub fn linux(mut self) -> Self {
112 self.os = NodeJsOs::Linux;
113 self
114 }
115
116 pub fn windows(mut self) -> Self {
125 self.os = NodeJsOs::Windows;
126 self
127 }
128
129 pub fn aix(mut self) -> Self {
138 self.os = NodeJsOs::Aix;
139 self
140 }
141
142 pub fn x64(mut self) -> Self {
151 self.arch = NodeJsArch::X64;
152 self
153 }
154
155 pub fn x86(mut self) -> Self {
164 self.arch = NodeJsArch::X86;
165 self
166 }
167
168 pub fn arm64(mut self) -> Self {
177 self.arch = NodeJsArch::Arm64;
178 self
179 }
180
181 pub fn armv7l(mut self) -> Self {
190 self.arch = NodeJsArch::Armv7l;
191 self
192 }
193
194 pub fn ppc64(mut self) -> Self {
203 self.arch = NodeJsArch::Ppc64;
204 self
205 }
206
207 pub fn ppc64le(mut self) -> Self {
216 self.arch = NodeJsArch::Ppc64le;
217 self
218 }
219
220 pub fn s390x(mut self) -> Self {
229 self.arch = NodeJsArch::S390x;
230 self
231 }
232
233 pub fn tar_gz(mut self) -> Self {
242 self.ext = NodeJsPkgExt::Targz;
243 self
244 }
245
246 pub fn tar_xz(mut self) -> Self {
255 self.ext = NodeJsPkgExt::Tarxz;
256 self
257 }
258
259 pub fn zip(mut self) -> Self {
268 self.ext = NodeJsPkgExt::Zip;
269 self
270 }
271
272 pub fn s7z(mut self) -> Self {
281 self.ext = NodeJsPkgExt::S7z;
282 self
283 }
284
285 pub fn msi(mut self) -> Self {
294 self.ext = NodeJsPkgExt::Msi;
295 self
296 }
297
298 pub async fn fetch(mut self) -> Result<Self, NodeJsRelInfoError> {
329 let version = specs::validate_version(self.version.as_str())?;
330 let specs = specs::fetch(&version, &self.url_fmt).await?;
331 let filename = self.filename();
332 let info = specs.lines().find(|&line| line.contains(filename.as_str()));
333
334 let Some(line) = info else {
335 return Err(NodeJsRelInfoError::UnrecognizedConfiguration(filename));
336 };
337
338 let Some(sha256) = line.split_whitespace().next() else {
339 return Err(NodeJsRelInfoError::UnrecognizedConfiguration(filename));
340 };
341
342 self.filename = filename;
343 self.sha256 = sha256.to_string();
344 self.url = self.url_fmt.pkg(&self.version, &self.filename);
345 Ok(self)
346 }
347
348 pub async fn fetch_all(&self) -> Result<Vec<NodeJsRelInfo>, NodeJsRelInfoError> {
369 let version = specs::validate_version(self.version.as_str())?;
370 let specs = specs::fetch(&version, &self.url_fmt).await?;
371 let specs = match specs::parse(&version, specs) {
372 Some(s) => s,
373 None => {
374 return Err(NodeJsRelInfoError::UnrecognizedVersion(version.clone()));
375 }
376 };
377
378 let mut all: Vec<NodeJsRelInfo> = vec![];
379 for (os, arch, ext, sha256, filename) in specs.into_iter() {
380 let version = version.clone();
381 let mut info = NodeJsRelInfo {
382 os,
383 arch,
384 version,
385 ext,
386 filename,
387 sha256,
388 ..Default::default()
389 };
390
391 info.url = info.url_fmt.pkg(&info.version, &info.filename);
392 all.push(info);
393 }
394
395 Ok(all)
396 }
397
398 fn filename(&self) -> String {
399 let arch = self.arch.to_string();
400 let ext = self.ext.to_string();
401
402 if self.ext == NodeJsPkgExt::Msi {
403 return format!("node-v{}-{}.{}", self.version, arch, ext);
404 }
405
406 format!("node-v{}-{}-{}.{}", self.version, self.os, arch, ext)
407 }
408}
409
410#[cfg(test)]
413mod tests {
414 use super::*;
415 use mockito::Server;
416
417 fn is_thread_safe<T: Sized + Send + Sync + Unpin>() {}
418
419 #[test]
420 fn it_initializes() {
421 let info = NodeJsRelInfo::new("1.0.0");
422 assert_eq!(info.os, NodeJsOs::Linux);
423 assert_eq!(info.arch, NodeJsArch::X64);
424 assert_eq!(info.ext, NodeJsPkgExt::Targz);
425 assert_eq!(info.version, "1.0.0".to_string());
426 assert_eq!(info.filename, "".to_string());
427 assert_eq!(info.sha256, "".to_string());
428 assert_eq!(info.url, "".to_string());
429 is_thread_safe::<NodeJsRelInfo>();
430 }
431
432 #[test]
433 fn it_initializes_with_defaults() {
434 let info = NodeJsRelInfo::default();
435 assert_eq!(info.os, NodeJsOs::Linux);
436 assert_eq!(info.arch, NodeJsArch::X64);
437 assert_eq!(info.ext, NodeJsPkgExt::Targz);
438 assert_eq!(info.version, "".to_string());
439 assert_eq!(info.filename, "".to_string());
440 assert_eq!(info.sha256, "".to_string());
441 assert_eq!(info.url, "".to_string());
442 }
443
444 #[test]
445 #[cfg_attr(not(target_os = "macos"), ignore)]
446 fn it_initializes_using_current_environment_on_macos() {
447 let info = NodeJsRelInfo::from_env("1.0.0").unwrap();
448 assert_eq!(info.ext, NodeJsPkgExt::Targz);
449 }
450
451 #[test]
452 #[cfg_attr(not(target_os = "linux"), ignore)]
453 fn it_initializes_using_current_environment_on_linux() {
454 let info = NodeJsRelInfo::from_env("1.0.0").unwrap();
455 assert_eq!(info.ext, NodeJsPkgExt::Targz);
456 }
457
458 #[test]
459 #[cfg_attr(not(target_os = "windows"), ignore)]
460 fn it_initializes_using_current_environment_on_windows() {
461 let info = NodeJsRelInfo::from_env("1.0.0").unwrap();
462 assert_eq!(info.ext, NodeJsPkgExt::Zip);
463 }
464
465 #[test]
466 fn it_sets_os() {
467 let info = NodeJsRelInfo::new("1.0.0");
468
469 assert_eq!(info.os, NodeJsOs::Linux);
470 assert_eq!(info.clone().windows().os, NodeJsOs::Windows);
471 assert_eq!(info.clone().macos().os, NodeJsOs::Darwin);
472 assert_eq!(info.clone().linux().os, NodeJsOs::Linux);
473 assert_eq!(info.clone().aix().os, NodeJsOs::Aix);
474 }
475
476 #[test]
477 fn it_sets_arch() {
478 let info = NodeJsRelInfo::new("1.0.0");
479
480 assert_eq!(info.clone().x86().arch, NodeJsArch::X86);
481 assert_eq!(info.clone().x64().arch, NodeJsArch::X64);
482 assert_eq!(info.clone().arm64().arch, NodeJsArch::Arm64);
483 assert_eq!(info.clone().armv7l().arch, NodeJsArch::Armv7l);
484 assert_eq!(info.clone().ppc64().arch, NodeJsArch::Ppc64);
485 assert_eq!(info.clone().ppc64le().arch, NodeJsArch::Ppc64le);
486 assert_eq!(info.clone().s390x().arch, NodeJsArch::S390x);
487 }
488
489 #[test]
490 fn it_sets_ext() {
491 let info = NodeJsRelInfo::new("1.0.0");
492
493 assert_eq!(info.clone().zip().ext, NodeJsPkgExt::Zip);
494 assert_eq!(info.clone().tar_gz().ext, NodeJsPkgExt::Targz);
495 assert_eq!(info.clone().tar_xz().ext, NodeJsPkgExt::Tarxz);
496 assert_eq!(info.clone().msi().ext, NodeJsPkgExt::Msi);
497 assert_eq!(info.clone().s7z().ext, NodeJsPkgExt::S7z);
498 }
499
500 #[test]
501 fn it_clones() {
502 let info1 = NodeJsRelInfo::new("1.0.0");
503 let info2 = info1.clone();
504
505 assert_eq!(info1, info2);
506 assert_ne!(info1.windows(), info2);
508 }
509
510 #[test]
511 fn it_formats_filename() {
512 let info = NodeJsRelInfo::new("1.0.0").macos().x64().zip();
513
514 assert_eq!(info.filename(), "node-v1.0.0-darwin-x64.zip");
515
516 let info = NodeJsRelInfo::new("1.0.0").windows().x64().msi();
517
518 assert_eq!(info.filename(), "node-v1.0.0-x64.msi");
519 }
520
521 #[test]
522 #[cfg(feature = "json")]
523 fn it_serializes_and_deserializes() {
524 let version = "20.6.1".to_string();
525 let filename = "node-v20.6.1-darwin-arm64.tar.gz".to_string();
526 let sha256 = "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46".to_string();
527 let url = "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz"
528 .to_string();
529 let info_orig = NodeJsRelInfo {
530 os: NodeJsOs::Darwin,
531 arch: NodeJsArch::Arm64,
532 ext: NodeJsPkgExt::Targz,
533 version: version.clone(),
534 filename: filename.clone(),
535 sha256: sha256.clone(),
536 url: url.clone(),
537 ..Default::default()
538 };
539 let info_json = serde_json::to_string(&info_orig).unwrap();
540 let info: NodeJsRelInfo = serde_json::from_str(&info_json).unwrap();
541 assert_eq!(info.os, NodeJsOs::Darwin);
542 assert_eq!(info.arch, NodeJsArch::Arm64);
543 assert_eq!(info.ext, NodeJsPkgExt::Targz);
544 assert_eq!(info.version, "20.6.1".to_string());
545 assert_eq!(
546 info.filename,
547 "node-v20.6.1-darwin-arm64.tar.gz".to_string()
548 );
549 assert_eq!(
550 info.sha256,
551 "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46".to_string()
552 );
553 assert_eq!(
554 info.url,
555 "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz"
556 .to_string()
557 );
558 }
559
560 #[tokio::test]
561 async fn it_fails_to_fetch_info_when_version_is_invalid() {
562 let info = NodeJsRelInfo::new("NOPE!");
563 let err = info.fetch().await.unwrap_err();
564
565 assert!(matches!(err, NodeJsRelInfoError::InvalidVersion(x) if x == "NOPE!"));
566 }
567
568 #[tokio::test]
569 async fn it_fails_to_fetch_info_when_version_is_unrecognized() {
570 let mut info = NodeJsRelInfo::new("1.0.0");
571 let mut server = Server::new_async().await;
572 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
573 .with_body(specs::get_fake_specs())
574 .with_status(404)
575 .create_async()
576 .await;
577
578 let err = info.fetch().await.unwrap_err();
579 mock.assert_async().await;
580
581 assert!(matches!(err, NodeJsRelInfoError::UnrecognizedVersion(x) if x == "1.0.0"));
582 }
583
584 #[tokio::test]
585 async fn it_fails_to_fetch_info_when_configuration_is_unrecognized() {
586 let mut server = Server::new_async().await;
587 let mut info = NodeJsRelInfo::new("20.6.1").linux().zip();
588 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
589 .with_body(specs::get_fake_specs())
590 .create_async()
591 .await;
592
593 let err = info.fetch().await.unwrap_err();
594 mock.assert_async().await;
595
596 assert!(
597 matches!(err, NodeJsRelInfoError::UnrecognizedConfiguration(x) if x == "node-v20.6.1-linux-x64.zip")
598 );
599 }
600
601 #[tokio::test]
602 async fn it_fetches_node_js_release_info() {
603 let mut info = NodeJsRelInfo::new("20.6.1");
604 let mut server = Server::new_async().await;
605 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
606 .with_body(specs::get_fake_specs())
607 .create_async()
608 .await;
609
610 let info = info.fetch().await.unwrap();
611 mock.assert_async().await;
612
613 assert_eq!(info.filename, "node-v20.6.1-linux-x64.tar.gz");
614 assert_eq!(
615 info.url,
616 format!(
617 "{}{}",
618 server.url(),
619 "/download/release/v20.6.1/node-v20.6.1-linux-x64.tar.gz"
620 )
621 );
622 assert_eq!(
623 info.sha256,
624 "26dd13a6f7253f0ab9bcab561353985a297d927840771d905566735b792868da"
625 );
626 }
627
628 #[tokio::test]
629 async fn it_fetches_node_js_release_info_when_ext_is_msi() {
630 let mut info = NodeJsRelInfo::new("20.6.1").arm64().msi();
631 let mut server = Server::new_async().await;
632 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
633 .with_body(specs::get_fake_specs())
634 .create_async()
635 .await;
636
637 let info = info.fetch().await.unwrap();
638 mock.assert_async().await;
639
640 assert_eq!(info.filename, "node-v20.6.1-arm64.msi");
641 assert_eq!(
642 info.url,
643 format!(
644 "{}{}",
645 server.url(),
646 "/download/release/v20.6.1/node-v20.6.1-arm64.msi"
647 )
648 );
649 assert_eq!(
650 info.sha256,
651 "9471bd6dc491e09c31b0f831f5953284b8a6842ed4ccb98f5c62d13e6086c471"
652 );
653 }
654
655 #[tokio::test]
656 async fn it_fetches_all_supported_node_js_configurations() {
657 let mut info = NodeJsRelInfo::new("20.6.1");
658 let mut server = Server::new_async().await;
659 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
660 .with_body(specs::get_fake_specs())
661 .create_async()
662 .await;
663
664 let all = info.fetch_all().await.unwrap();
665 mock.assert_async().await;
666
667 assert_eq!(all.len(), 24);
668 assert_eq!(all[2].version, "20.6.1");
669 assert_eq!(all[2].os, NodeJsOs::Darwin);
670 assert_eq!(all[2].arch, NodeJsArch::Arm64);
671 assert_eq!(all[2].ext, NodeJsPkgExt::Targz);
672 assert_eq!(all[2].filename, "node-v20.6.1-darwin-arm64.tar.gz");
673 assert_eq!(
674 all[2].sha256,
675 "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46"
676 );
677 assert_eq!(
678 all[2].url,
679 "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz"
680 );
681 }
682
683 #[tokio::test]
684 async fn it_fails_to_fetch_all_supported_node_js_configurations_when_version_is_unrecognized() {
685 let mut info = NodeJsRelInfo::new("1.0.0");
686 let mut server = Server::new_async().await;
687 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
688 .with_body(String::from(""))
689 .create_async()
690 .await;
691
692 let err = info.fetch_all().await.unwrap_err();
693 mock.assert_async().await;
694
695 assert!(matches!(err, NodeJsRelInfoError::UnrecognizedVersion(x) if x == "1.0.0"));
696 }
697}