1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! Download packages from cargo registry, similar to the `git clone` behavior.

use std::path::{Path, PathBuf};

use anyhow::{anyhow, Context};
use cargo_metadata::Package;
use tracing::{info, instrument, warn};

use crate::{
    clone::{Cloner, ClonerSource, Crate},
    CARGO_TOML,
};

#[derive(Debug)]
pub struct PackageDownloader {
    packages: Vec<String>,
    directory: String,
    registry: Option<String>,
    cargo_cwd: Option<PathBuf>,
}

impl PackageDownloader {
    pub fn new(
        packages: impl IntoIterator<Item = impl Into<String>>,
        directory: impl Into<String>,
    ) -> Self {
        Self {
            packages: packages.into_iter().map(Into::into).collect(),
            directory: directory.into(),
            registry: None,
            cargo_cwd: None,
        }
    }

    pub fn with_registry(self, registry: String) -> Self {
        Self {
            registry: Some(registry),
            ..self
        }
    }

    pub fn with_cargo_cwd(self, cargo_cwd: PathBuf) -> Self {
        Self {
            cargo_cwd: Some(cargo_cwd),
            ..self
        }
    }

    #[instrument]
    pub fn download(&self) -> anyhow::Result<Vec<Package>> {
        info!("downloading packages from cargo registry");
        let source: ClonerSource = match &self.registry {
            Some(registry) => ClonerSource::registry(registry),
            None => ClonerSource::crates_io(),
        };
        let crates: Vec<Crate> = self
            .packages
            .iter()
            .map(|package_name| Crate::new(package_name.to_string(), None))
            .collect();
        let mut cloner_builder = Cloner::builder()
            .with_directory(&self.directory)
            .with_source(source);
        if let Some(cwd) = &self.cargo_cwd {
            cloner_builder = cloner_builder.with_cargo_cwd(cwd.clone());
        }
        let downloaded_packages = cloner_builder
            .build()
            .context("can't build cloner")?
            .clone(&crates)
            .context("error while downloading packages")?;

        downloaded_packages
            .iter()
            .map(|(_package, path)| read_package(path))
            .collect()
    }
}

/// Read a package from file system
pub fn read_package(directory: impl AsRef<Path>) -> anyhow::Result<Package> {
    let manifest_path = directory.as_ref().join(CARGO_TOML);
    let metadata = cargo_metadata::MetadataCommand::new()
        .no_deps()
        .manifest_path(manifest_path)
        .exec()
        .context("failed to execute cargo_metadata")?;
    let package = metadata
        .packages
        .first()
        .ok_or_else(|| anyhow!("cannot retrieve package at {:?}", directory.as_ref()))?;
    Ok(package.clone())
}

#[cfg(test)]
mod tests {
    use fake::Fake;
    use tempfile::tempdir;

    use super::*;

    #[test]
    #[ignore]
    fn one_package_is_downloaded() {
        let package_name = "rand";
        let temp_dir = tempdir().unwrap();
        let directory = temp_dir.as_ref().to_str().expect("invalid tempdir path");
        let packages = PackageDownloader::new([package_name], directory)
            .download()
            .unwrap();
        let rand = &packages[0];
        assert_eq!(rand.name, package_name);
    }

    #[test]
    #[ignore]
    fn two_packages_are_downloaded() {
        let first_package = "rand";
        let second_package = "rust-gh-example";
        let temp_dir = tempdir().unwrap();
        let directory = temp_dir.as_ref().to_str().expect("invalid tempdir path");
        let packages = PackageDownloader::new([first_package, second_package], directory)
            .download()
            .unwrap();
        assert_eq!(&packages[0].name, first_package);
        assert_eq!(&packages[1].name, second_package);
    }

    #[test]
    #[ignore]
    fn downloading_non_existing_package_does_not_error() {
        // Generate random string 15 characters long.
        let package: String = 15.fake();
        let temp_dir = tempdir().unwrap();
        let directory = temp_dir.as_ref().to_str().expect("invalid tempdir path");
        PackageDownloader::new([&package], directory)
            .download()
            .unwrap();
    }
}