vouch_js_lib/
lib.rs

1use anyhow::{format_err, Context, Result};
2use std::io::Read;
3use strum::IntoEnumIterator;
4
5mod npm;
6
7#[derive(Clone, Debug)]
8pub struct JsExtension {
9    name_: String,
10    registry_host_names_: Vec<String>,
11    root_url_: url::Url,
12    registry_human_url_template_: String,
13}
14
15impl vouch_lib::extension::FromLib for JsExtension {
16    fn new() -> Self {
17        Self {
18            name_: "js".to_string(),
19            registry_host_names_: vec!["npmjs.com".to_owned()],
20            root_url_: url::Url::parse("https://www.npmjs.com").unwrap(),
21            registry_human_url_template_:
22                "https://www.npmjs.com/package/{{package_name}}/v/{{package_version}}".to_string(),
23        }
24    }
25}
26
27impl vouch_lib::extension::Extension for JsExtension {
28    fn name(&self) -> String {
29        self.name_.clone()
30    }
31
32    fn registries(&self) -> Vec<String> {
33        self.registry_host_names_.clone()
34    }
35
36    fn identify_local_dependencies(
37        &self,
38        working_directory: &std::path::PathBuf,
39    ) -> Result<Vec<vouch_lib::extension::DependenciesSpec>> {
40        // Identify all dependency definition files.
41        let dependency_files = match identify_dependency_files(&working_directory) {
42            Some(v) => v,
43            None => return Ok(Vec::new()),
44        };
45
46        // Read all dependencies definitions files.
47        let mut all_dependency_specs = Vec::new();
48        for dependency_file in dependency_files {
49            // TODO: Add support for parsing all definition file types.
50            let (dependencies, registry_host_name) = match dependency_file.r#type {
51                DependencyFileType::Npm => (
52                    npm::get_dependencies(&dependency_file.path)?,
53                    npm::get_registry_host_name(),
54                ),
55            };
56            all_dependency_specs.push(vouch_lib::extension::DependenciesSpec {
57                path: dependency_file.path,
58                registry_host_name: registry_host_name,
59                dependencies: dependencies.into_iter().collect(),
60            });
61        }
62
63        Ok(all_dependency_specs)
64    }
65
66    fn registries_package_metadata(
67        &self,
68        package_name: &str,
69        package_version: &Option<&str>,
70    ) -> Result<Vec<vouch_lib::extension::RegistryPackageMetadata>> {
71        let package_version = match package_version {
72            Some(v) => Some(v.to_string()),
73            None => get_latest_version(&package_name)?,
74        }
75        .ok_or(format_err!("Failed to find package version."))?;
76
77        // Query remote package registry for given package.
78        let human_url = get_registry_human_url(&self, &package_name, &package_version)?;
79
80        // Currently, only one registry is supported. Therefore simply extract.
81        let registry_host_name = self
82            .registries()
83            .first()
84            .ok_or(format_err!(
85                "Code error: vector of registry host names is empty."
86            ))?
87            .clone();
88
89        let entry_json = get_registry_entry_json(&package_name)?;
90        let artifact_url = get_archive_url(&entry_json, &package_version)?;
91
92        Ok(vec![vouch_lib::extension::RegistryPackageMetadata {
93            registry_host_name: registry_host_name,
94            human_url: human_url.to_string(),
95            artifact_url: artifact_url.to_string(),
96            is_primary: true,
97            package_version: package_version,
98        }])
99    }
100}
101
102/// Given package name, return latest version.
103fn get_latest_version(package_name: &str) -> Result<Option<String>> {
104    let json = get_registry_entry_json(&package_name)?;
105    let versions = json["versions"]
106        .as_object()
107        .ok_or(format_err!("Failed to find versions JSON section."))?;
108    let latest_version = versions.keys().last();
109    Ok(latest_version.cloned())
110}
111
112fn get_registry_human_url(
113    extension: &JsExtension,
114    package_name: &str,
115    package_version: &str,
116) -> Result<url::Url> {
117    // Example return value: https://www.npmjs.com/package/d3/v/6.5.0
118    let handlebars_registry = handlebars::Handlebars::new();
119    let url = handlebars_registry.render_template(
120        &extension.registry_human_url_template_,
121        &maplit::btreemap! {
122            "package_name" => package_name,
123            "package_version" => package_version,
124        },
125    )?;
126    Ok(url::Url::parse(url.as_str())?)
127}
128
129fn get_registry_entry_json(package_name: &str) -> Result<serde_json::Value> {
130    let handlebars_registry = handlebars::Handlebars::new();
131    let json_url = handlebars_registry.render_template(
132        "https://registry.npmjs.com/{{package_name}}",
133        &maplit::btreemap! {"package_name" => package_name},
134    )?;
135
136    let mut result = reqwest::blocking::get(&json_url.to_string())?;
137    let mut body = String::new();
138    result.read_to_string(&mut body)?;
139
140    Ok(serde_json::from_str(&body).context(format!("JSON was not well-formatted:\n{}", body))?)
141}
142
143fn get_archive_url(
144    registry_entry_json: &serde_json::Value,
145    package_version: &str,
146) -> Result<url::Url> {
147    Ok(url::Url::parse(
148        registry_entry_json["versions"][package_version]["dist"]["tarball"]
149            .as_str()
150            .ok_or(format_err!("Failed to parse package archive URL."))?,
151    )?)
152}
153
154/// Package dependency file types.
155#[derive(Debug, Copy, Clone, strum_macros::EnumIter)]
156enum DependencyFileType {
157    Npm,
158}
159
160impl DependencyFileType {
161    /// Return file name associated with dependency type.
162    pub fn file_name(&self) -> std::path::PathBuf {
163        match self {
164            Self::Npm => std::path::PathBuf::from("package-lock.json"),
165        }
166    }
167}
168
169/// Package dependency file type and file path.
170#[derive(Debug, Clone)]
171struct DependencyFile {
172    r#type: DependencyFileType,
173    path: std::path::PathBuf,
174}
175
176/// Returns a vector of identified package dependency definition files.
177///
178/// Walks up the directory tree directory tree until the first positive result is found.
179fn identify_dependency_files(
180    working_directory: &std::path::PathBuf,
181) -> Option<Vec<DependencyFile>> {
182    assert!(working_directory.is_absolute());
183    let mut working_directory = working_directory.clone();
184
185    loop {
186        // If at least one target is found, assume package is present.
187        let mut found_dependency_file = false;
188
189        let mut dependency_files: Vec<DependencyFile> = Vec::new();
190        for dependency_file_type in DependencyFileType::iter() {
191            let target_absolute_path = working_directory.join(dependency_file_type.file_name());
192            if target_absolute_path.is_file() {
193                found_dependency_file = true;
194                dependency_files.push(DependencyFile {
195                    r#type: dependency_file_type,
196                    path: target_absolute_path,
197                })
198            }
199        }
200        if found_dependency_file {
201            return Some(dependency_files);
202        }
203
204        // No need to move further up the directory tree after this loop.
205        if working_directory == std::path::PathBuf::from("/") {
206            break;
207        }
208
209        // Move further up the directory tree.
210        working_directory.pop();
211    }
212    None
213}