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, PartialEq)]
20#[cfg_attr(feature = "json", derive(Deserialize, Serialize))]
21pub struct NodeJSRelInfo {
22 pub os: NodeJSOS,
24 pub arch: NodeJSArch,
26 pub ext: NodeJSPkgExt,
28 pub version: String,
30 pub filename: String,
32 pub sha256: String,
34 pub url: String,
36 #[cfg_attr(feature = "json", serde(skip))]
37 url_fmt: NodeJSURLFormatter,
38}
39
40impl NodeJSRelInfo {
41 pub fn new<T: AsRef<str>>(semver: T) -> Self {
54 NodeJSRelInfo {
55 version: semver.as_ref().to_owned(),
56 ..Default::default()
57 }
58 }
59
60 pub fn from_env<T: AsRef<str>>(semver: T) -> Result<NodeJSRelInfo, NodeJSRelInfoError> {
73 let mut info = NodeJSRelInfo::new(semver);
74 info.os = NodeJSOS::from_env()?;
75 info.arch = NodeJSArch::from_env()?;
76 info.ext = match info.os {
77 NodeJSOS::Windows => NodeJSPkgExt::Zip,
78 _ => NodeJSPkgExt::Targz,
79 };
80 Ok(info)
81 }
82
83 pub fn macos(&mut self) -> &mut Self {
92 self.os = NodeJSOS::Darwin;
93 self
94 }
95
96 pub fn linux(&mut self) -> &mut Self {
105 self.os = NodeJSOS::Linux;
106 self
107 }
108
109 pub fn windows(&mut self) -> &mut Self {
118 self.os = NodeJSOS::Windows;
119 self
120 }
121
122 pub fn aix(&mut self) -> &mut Self {
131 self.os = NodeJSOS::AIX;
132 self
133 }
134
135 pub fn x64(&mut self) -> &mut Self {
144 self.arch = NodeJSArch::X64;
145 self
146 }
147
148 pub fn x86(&mut self) -> &mut Self {
157 self.arch = NodeJSArch::X86;
158 self
159 }
160
161 pub fn arm64(&mut self) -> &mut Self {
170 self.arch = NodeJSArch::ARM64;
171 self
172 }
173
174 pub fn armv7l(&mut self) -> &mut Self {
183 self.arch = NodeJSArch::ARMV7L;
184 self
185 }
186
187 pub fn ppc64(&mut self) -> &mut Self {
196 self.arch = NodeJSArch::PPC64;
197 self
198 }
199
200 pub fn ppc64le(&mut self) -> &mut Self {
209 self.arch = NodeJSArch::PPC64LE;
210 self
211 }
212
213 pub fn s390x(&mut self) -> &mut Self {
222 self.arch = NodeJSArch::S390X;
223 self
224 }
225
226 pub fn tar_gz(&mut self) -> &mut Self {
235 self.ext = NodeJSPkgExt::Targz;
236 self
237 }
238
239 pub fn tar_xz(&mut self) -> &mut Self {
248 self.ext = NodeJSPkgExt::Tarxz;
249 self
250 }
251
252 pub fn zip(&mut self) -> &mut Self {
261 self.ext = NodeJSPkgExt::Zip;
262 self
263 }
264
265 pub fn s7z(&mut self) -> &mut Self {
274 self.ext = NodeJSPkgExt::S7z;
275 self
276 }
277
278 pub fn msi(&mut self) -> &mut Self {
287 self.ext = NodeJSPkgExt::Msi;
288 self
289 }
290
291 pub fn to_owned(&self) -> Self {
300 self.clone()
301 }
302
303 pub async fn fetch(&mut self) -> Result<Self, NodeJSRelInfoError> {
322 let version = specs::validate_version(self.version.as_str())?;
323 let specs = specs::fetch(&version, &self.url_fmt).await?;
324 let filename = self.filename();
325 let info = specs.lines().find(|&line| line.contains(filename.as_str()));
326
327 let mut specs = match info {
328 None => return Err(NodeJSRelInfoError::UnrecognizedConfiguration(filename))?,
329 Some(s) => s.split_whitespace(),
330 };
331
332 self.filename = filename;
333 self.sha256 = specs.nth(0).unwrap().to_string();
334 self.url = self.url_fmt.pkg(&self.version, &self.filename);
335 Ok(self.to_owned())
336 }
337
338 pub async fn fetch_all(&self) -> Result<Vec<NodeJSRelInfo>, NodeJSRelInfoError> {
359 let version = specs::validate_version(self.version.as_str())?;
360 let specs = specs::fetch(&version, &self.url_fmt).await?;
361 let specs = match specs::parse(&version, specs) {
362 Some(s) => s,
363 None => {
364 return Err(NodeJSRelInfoError::UnrecognizedVersion(version.clone()));
365 }
366 };
367
368 let mut all: Vec<NodeJSRelInfo> = vec![];
369 for (os, arch, ext, sha256, filename) in specs.into_iter() {
370 let version = version.clone();
371 let mut info = NodeJSRelInfo {
372 os,
373 arch,
374 version,
375 ext,
376 filename,
377 sha256,
378 ..Default::default()
379 };
380
381 info.url = info.url_fmt.pkg(&info.version, &info.filename);
382 all.push(info);
383 }
384
385 Ok(all)
386 }
387
388 fn filename(&self) -> String {
389 let arch = self.arch.to_string();
390 let ext = self.ext.to_string();
391
392 if self.ext == NodeJSPkgExt::Msi {
393 return format!("node-v{}-{}.{}", self.version, arch, ext);
394 }
395
396 format!("node-v{}-{}-{}.{}", self.version, self.os, arch, ext)
397 }
398}
399
400#[cfg(test)]
403mod tests {
404 use super::*;
405 use mockito::Server;
406
407 fn is_thread_safe<T: Sized + Send + Sync + Unpin>() {}
408
409 #[test]
410 fn it_initializes() {
411 let info = NodeJSRelInfo::new("1.0.0");
412 assert_eq!(info.os, NodeJSOS::Linux);
413 assert_eq!(info.arch, NodeJSArch::X64);
414 assert_eq!(info.ext, NodeJSPkgExt::Targz);
415 assert_eq!(info.version, "1.0.0".to_string());
416 assert_eq!(info.filename, "".to_string());
417 assert_eq!(info.sha256, "".to_string());
418 assert_eq!(info.url, "".to_string());
419 is_thread_safe::<NodeJSRelInfo>();
420 }
421
422 #[test]
423 fn it_initializes_with_defaults() {
424 let info = NodeJSRelInfo::default();
425 assert_eq!(info.os, NodeJSOS::Linux);
426 assert_eq!(info.arch, NodeJSArch::X64);
427 assert_eq!(info.ext, NodeJSPkgExt::Targz);
428 assert_eq!(info.version, "".to_string());
429 assert_eq!(info.filename, "".to_string());
430 assert_eq!(info.sha256, "".to_string());
431 assert_eq!(info.url, "".to_string());
432 }
433
434 #[test]
435 #[cfg_attr(not(target_os = "macos"), ignore)]
436 fn it_initializes_using_current_environment_on_macos() {
437 let info = NodeJSRelInfo::from_env("1.0.0").unwrap();
438 assert_eq!(info.ext, NodeJSPkgExt::Targz);
439 }
440
441 #[test]
442 #[cfg_attr(not(target_os = "linux"), ignore)]
443 fn it_initializes_using_current_environment_on_linux() {
444 let info = NodeJSRelInfo::from_env("1.0.0").unwrap();
445 assert_eq!(info.ext, NodeJSPkgExt::Targz);
446 }
447
448 #[test]
449 #[cfg_attr(not(target_os = "windows"), ignore)]
450 fn it_initializes_using_current_environment_on_windows() {
451 let info = NodeJSRelInfo::from_env("1.0.0").unwrap();
452 assert_eq!(info.ext, NodeJSPkgExt::Zip);
453 }
454
455 #[test]
456 fn it_sets_os() {
457 let mut info = NodeJSRelInfo::new("1.0.0");
458
459 assert_eq!(info.os, NodeJSOS::Linux);
460
461 info.windows();
462
463 assert_eq!(info.os, NodeJSOS::Windows);
464
465 info.macos();
466
467 assert_eq!(info.os, NodeJSOS::Darwin);
468
469 info.linux();
470
471 assert_eq!(info.os, NodeJSOS::Linux);
472
473 info.aix();
474
475 assert_eq!(info.os, NodeJSOS::AIX);
476 }
477
478 #[test]
479 fn it_sets_arch() {
480 let mut info = NodeJSRelInfo::new("1.0.0");
481
482 info.x86();
483
484 assert_eq!(info.arch, NodeJSArch::X86);
485
486 info.x64();
487
488 assert_eq!(info.arch, NodeJSArch::X64);
489
490 info.arm64();
491
492 assert_eq!(info.arch, NodeJSArch::ARM64);
493
494 info.armv7l();
495
496 assert_eq!(info.arch, NodeJSArch::ARMV7L);
497
498 info.ppc64();
499
500 assert_eq!(info.arch, NodeJSArch::PPC64);
501
502 info.ppc64le();
503
504 assert_eq!(info.arch, NodeJSArch::PPC64LE);
505
506 info.s390x();
507
508 assert_eq!(info.arch, NodeJSArch::S390X);
509 }
510
511 #[test]
512 fn it_sets_ext() {
513 let mut info = NodeJSRelInfo::new("1.0.0");
514
515 info.zip();
516
517 assert_eq!(info.ext, NodeJSPkgExt::Zip);
518
519 info.tar_gz();
520
521 assert_eq!(info.ext, NodeJSPkgExt::Targz);
522
523 info.tar_xz();
524
525 assert_eq!(info.ext, NodeJSPkgExt::Tarxz);
526
527 info.msi();
528
529 assert_eq!(info.ext, NodeJSPkgExt::Msi);
530
531 info.s7z();
532
533 assert_eq!(info.ext, NodeJSPkgExt::S7z);
534 }
535
536 #[test]
537 fn it_gets_owned_copy() {
538 let mut info1 = NodeJSRelInfo::new("1.0.0");
539 let info2 = info1.to_owned();
540
541 assert_eq!(info1, info2);
542
543 info1.windows();
544
545 assert_ne!(info1, info2);
546 }
547
548 #[test]
549 fn it_formats_filename() {
550 let info = NodeJSRelInfo::new("1.0.0").macos().x64().zip().to_owned();
551
552 assert_eq!(info.filename(), "node-v1.0.0-darwin-x64.zip");
553
554 let info = NodeJSRelInfo::new("1.0.0").windows().x64().msi().to_owned();
555
556 assert_eq!(info.filename(), "node-v1.0.0-x64.msi");
557 }
558
559 #[test]
560 fn it_serializes_and_deserializes() {
561 let version = "20.6.1".to_string();
562 let filename = "node-v20.6.1-darwin-arm64.tar.gz".to_string();
563 let sha256 = "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46".to_string();
564 let url = "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz"
565 .to_string();
566 let info_orig = NodeJSRelInfo {
567 os: NodeJSOS::Darwin,
568 arch: NodeJSArch::ARM64,
569 ext: NodeJSPkgExt::Targz,
570 version: version.clone(),
571 filename: filename.clone(),
572 sha256: sha256.clone(),
573 url: url.clone(),
574 ..Default::default()
575 };
576 let info_json = serde_json::to_string(&info_orig).unwrap();
577 let info: NodeJSRelInfo = serde_json::from_str(&info_json).unwrap();
578 assert_eq!(info.os, NodeJSOS::Darwin);
579 assert_eq!(info.arch, NodeJSArch::ARM64);
580 assert_eq!(info.ext, NodeJSPkgExt::Targz);
581 assert_eq!(info.version, "20.6.1".to_string());
582 assert_eq!(
583 info.filename,
584 "node-v20.6.1-darwin-arm64.tar.gz".to_string()
585 );
586 assert_eq!(
587 info.sha256,
588 "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46".to_string()
589 );
590 assert_eq!(
591 info.url,
592 "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz"
593 .to_string()
594 );
595 }
596
597 #[tokio::test]
598 #[should_panic(
599 expected = "called `Result::unwrap()` on an `Err` value: InvalidVersion(\"NOPE!\")"
600 )]
601 async fn it_fails_to_fetch_info_when_version_is_invalid() {
602 let mut info = NodeJSRelInfo::new("NOPE!");
603 info.fetch().await.unwrap();
604 }
605
606 #[tokio::test]
607 #[should_panic(
608 expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedVersion(\"1.0.0\")"
609 )]
610 async fn it_fails_to_fetch_info_when_version_is_unrecognized() {
611 let mut info = NodeJSRelInfo::new("1.0.0");
612 let mut server = Server::new_async().await;
613 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
614 .with_body(specs::get_fake_specs())
615 .with_status(404)
616 .create_async()
617 .await;
618
619 info.fetch().await.unwrap();
620 mock.assert_async().await;
621 }
622
623 #[tokio::test]
624 #[should_panic(
625 expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedConfiguration(\"node-v20.6.1-linux-x64.zip\")"
626 )]
627 async fn it_fails_to_fetch_info_when_configuration_is_unrecognized() {
628 let mut server = Server::new_async().await;
629 let mut info = NodeJSRelInfo::new("20.6.1").linux().zip().to_owned();
630 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
631 .with_body(specs::get_fake_specs())
632 .create_async()
633 .await;
634
635 info.fetch().await.unwrap();
636 mock.assert_async().await;
637 }
638
639 #[tokio::test]
640 async fn it_fetches_node_js_release_info() {
641 let mut info = NodeJSRelInfo::new("20.6.1");
642 let mut server = Server::new_async().await;
643 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
644 .with_body(specs::get_fake_specs())
645 .create_async()
646 .await;
647
648 info.fetch().await.unwrap();
649 mock.assert_async().await;
650
651 assert_eq!(info.filename, "node-v20.6.1-linux-x64.tar.gz");
652 assert_eq!(
653 info.url,
654 format!(
655 "{}{}",
656 server.url(),
657 "/download/release/v20.6.1/node-v20.6.1-linux-x64.tar.gz"
658 )
659 );
660 assert_eq!(
661 info.sha256,
662 "26dd13a6f7253f0ab9bcab561353985a297d927840771d905566735b792868da"
663 );
664 }
665
666 #[tokio::test]
667 async fn it_fetches_node_js_release_info_when_ext_is_msi() {
668 let mut info = NodeJSRelInfo::new("20.6.1").arm64().msi().to_owned();
669 let mut server = Server::new_async().await;
670 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
671 .with_body(specs::get_fake_specs())
672 .create_async()
673 .await;
674
675 info.fetch().await.unwrap();
676 mock.assert_async().await;
677
678 assert_eq!(info.filename, "node-v20.6.1-arm64.msi");
679 assert_eq!(
680 info.url,
681 format!(
682 "{}{}",
683 server.url(),
684 "/download/release/v20.6.1/node-v20.6.1-arm64.msi"
685 )
686 );
687 assert_eq!(
688 info.sha256,
689 "9471bd6dc491e09c31b0f831f5953284b8a6842ed4ccb98f5c62d13e6086c471"
690 );
691 }
692
693 #[tokio::test]
694 async fn it_fetches_all_supported_node_js_configurations() {
695 let mut info = NodeJSRelInfo::new("20.6.1");
696 let mut server = Server::new_async().await;
697 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
698 .with_body(specs::get_fake_specs())
699 .create_async()
700 .await;
701
702 let all = info.fetch_all().await.unwrap();
703 mock.assert_async().await;
704
705 assert_eq!(all.len(), 24);
706 assert_eq!(all[2].version, "20.6.1");
707 assert_eq!(all[2].os, NodeJSOS::Darwin);
708 assert_eq!(all[2].arch, NodeJSArch::ARM64);
709 assert_eq!(all[2].ext, NodeJSPkgExt::Targz);
710 assert_eq!(all[2].filename, "node-v20.6.1-darwin-arm64.tar.gz");
711 assert_eq!(
712 all[2].sha256,
713 "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46"
714 );
715 assert_eq!(
716 all[2].url,
717 "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz"
718 );
719 }
720
721 #[tokio::test]
722 #[should_panic(
723 expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedVersion(\"1.0.0\")"
724 )]
725 async fn it_fails_to_fetch_all_supported_node_js_configurations_when_version_is_unrecognized() {
726 let mut info = NodeJSRelInfo::new("1.0.0");
727 let mut server = Server::new_async().await;
728 let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
729 .with_body(String::from(""))
730 .create_async()
731 .await;
732
733 info.fetch_all().await.unwrap();
734 mock.assert_async().await;
735 }
736}