extern crate env_logger;
extern crate serde_json;
use std::collections::HashMap;
use std::convert::AsRef;
use std::fs;
use std::fs::File;
use std::path::{Path, PathBuf};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Package {
pub name: String,
pub version: String,
pub description: Option<String>,
pub dependencies: Option<HashMap<String, String>>,
#[serde(skip_serializing)]
pub root: Option<PathBuf>,
}
impl Package {
pub fn load<P: AsRef<Path>>(path: P) -> Package {
let path = path.as_ref();
debug!("loading {:?}", path);
let file = File::open(path.join("package.json")).unwrap();
let mut package: Package = serde_json::from_reader(file).unwrap();
package.root = Some(path.to_owned());
package
}
pub fn validate(&self) -> Vec<Issue> {
fn validate_package(root: &PackageTree, package: &Package, at: &[&String]) -> Vec<Issue> {
let mut issues = vec![];
let empty = HashMap::new();
let deps = package.dependencies.as_ref().unwrap_or(&empty);
for (name, version) in deps {
let mut next = at.to_owned();
next.push(&name);
let mut node_issues: Vec<Issue> = match root.get(&name, &next) {
Some(dep_node) => {
let expected_version = version.clone();
let actual_version = dep_node.package.version.clone();
if expected_version != actual_version {
return vec![Issue::WrongVersionInstalled {
package: name.clone(),
expected_version,
actual_version,
}];
}
validate_package(root, &dep_node.package, &next)
}
None => vec![Issue::PackageNotInstalled {
package: name.clone(),
}],
};
issues.append(&mut node_issues);
}
issues
}
fn validate_package_lock(
root: &PackageTree,
lock: &PackageLock,
pkg: &Package,
at: &[&String],
) -> Vec<Issue> {
let mut issues = vec![];
let empty = HashMap::new();
let deps = pkg.dependencies.as_ref().unwrap_or(&empty);
for name in deps.keys() {
issues.append(&mut match root.get(name, &at) {
Some(node) => match lock.get(&name, &at) {
Some(_dep) => {
let mut next = at.to_vec();
next.push(&name);
validate_package_lock(root, lock, &node.package, &next)
}
None => vec![Issue::MissingPackageFromLock {
package: name.clone(),
}],
},
None => vec![],
});
}
issues
}
let lock = PackageLock::load(self.root.as_ref().unwrap());
let root = package_file_tree(self.root.as_ref().unwrap());
let mut issues = validate_package(&root, self, &[]);
issues.append(&mut validate_package_lock(&root, &lock, &self, &[]));
issues
}
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageLock {
pub name: String,
pub version: String,
pub lockfile_version: u8,
pub description: Option<String>,
pub dependencies: Option<HashMap<String, PackageLockDependency>>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageLockDependency {
#[serde(skip_serializing)]
pub name: Option<String>,
pub version: String,
pub resolved: String,
pub integrity: String,
pub requires: Option<HashMap<String, String>>,
pub dependencies: Option<HashMap<String, PackageLockDependency>>,
}
impl PackageLock {
fn load<P: AsRef<Path>>(path: P) -> PackageLock {
let path = path.as_ref().join("package-lock.json");
let file = File::open(path).unwrap();
let package: PackageLock = serde_json::from_reader(file).unwrap();
package
}
fn get(&self, name: &str, at: &[&String]) -> Option<&PackageLockDependency> {
match &self.dependencies {
Some(deps) => find_lock_dependency(&deps, name, &at),
None => None,
}
}
}
fn find_lock_dependency<'a>(
deps: &'a HashMap<String, PackageLockDependency>,
name: &str,
at: &[&String],
) -> Option<&'a PackageLockDependency> {
if ! at.is_empty() {
let next = at[0];
let at = &at[1..].to_vec();
if let Some(lock) = deps.get(next) {
if let Some(deps) = &lock.dependencies {
return find_lock_dependency(deps, name, at)
}
}
}
deps.get(name)
}
#[derive(Debug)]
pub enum Issue {
MissingPackageFromLock {
package: String,
},
PackageNotInstalled {
package: String,
},
WrongVersionInstalled {
package: String,
expected_version: String,
actual_version: String,
},
}
struct PackageTree {
package: Package,
children: HashMap<String, PackageTree>,
}
impl PackageTree {
fn get(&self, name: &str, at: &[&String]) -> Option<&PackageTree> {
if ! at.is_empty() {
let next = at[0];
let at = &at[1..].to_vec();
if let Some(child) = self.children.get(next) {
if let Some(node) = child.get(name, at) {
return Some(node);
}
}
}
self.children.get(name)
}
}
fn package_file_tree<P: AsRef<Path>>(root: P) -> PackageTree {
let root = root.as_ref();
let mut node = PackageTree {
package: Package::load(root),
children: HashMap::new(),
};
let files = match fs::read_dir(root.join("node_modules")) {
Ok(files) => files.collect(),
Err(_) => vec![],
};
let packages = files
.into_iter()
.map(|f| f.unwrap().path())
.filter(|f| f.is_dir())
.map(|d| {
if d.file_name().unwrap().to_str().unwrap().starts_with('@') {
fs::read_dir(d)
.unwrap()
.into_iter()
.map(|f| f.unwrap().path())
.filter(|f| f.is_dir())
.collect()
} else {
vec![d]
}
}).flatten();
for pkg in packages {
let child = package_file_tree(pkg);
node.children.insert(child.package.name.clone(), child);
}
node
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loads_package() {
let p = Package::load("fixtures/example");
assert_eq!(p.name, "example");
assert_eq!(p.version, "0.0.0");
assert_eq!(p.dependencies.unwrap().get("edon-test-a").unwrap(), "0.0.1");
}
#[test]
fn loads_package_lock() {
let p = PackageLock::load("fixtures/example");
assert_eq!(p.name, "example");
assert_eq!(p.version, "0.0.0");
assert_eq!(
p.dependencies.unwrap().get("edon-test-a").unwrap().version,
"0.0.1"
);
}
#[test]
fn finds_missing_deps_from_lock() {
let p = Package::load("fixtures/missing-dep-from-lock");
let issues = p.validate();
match &issues[0] {
Issue::MissingPackageFromLock { ref package } => assert_eq!(package, "edon-test-c"),
_ => panic!("invalid issue {:?}", &issues[0]),
}
assert_eq!(issues.len(), 1);
}
#[test]
fn finds_missing_subdeps_from_lock() {
let p = Package::load("fixtures/missing-subdep-from-lock");
let issues = p.validate();
match &issues[0] {
Issue::MissingPackageFromLock { ref package } => assert_eq!(package, "edon-test-c"),
_ => panic!("invalid issue {:?}", &issues[0]),
}
assert_eq!(issues.len(), 1);
}
#[test]
fn does_not_error_if_no_deps() {
let p = Package::load("fixtures/no_deps");
let issues = p.validate();
assert_eq!(issues.len(), 0);
}
#[test]
fn test_package_file_tree() {
let tree = package_file_tree("fixtures/example");
assert_eq!(tree.package.name, "example");
assert_eq!(
tree.children.get("edon-test-a").unwrap().package.name,
"edon-test-a"
);
assert_eq!(
tree.children.get("edon-test-a").unwrap().package.version,
"0.0.1"
);
}
#[test]
fn wrong_package_installed_1() {
let p = Package::load("fixtures/1-wrong-package-version-installed");
let issues = p.validate();
match &issues[0] {
Issue::WrongVersionInstalled {
ref package,
ref expected_version,
ref actual_version,
} => {
assert_eq!(package, "edon-test-c");
assert_eq!(expected_version, "0.0.0");
assert_eq!(actual_version, "0.0.1");
}
_ => panic!("invalid issue"),
}
assert_eq!(issues.len(), 1);
}
#[test]
fn valid_multiple_versions() {
let p = Package::load("fixtures/2-valid-multiple-versions");
let issues = p.validate();
assert_eq!(issues.len(), 0);
}
#[test]
fn dep_not_installed_3() {
let p = Package::load("fixtures/3-dep-not-installed");
let issues = p.validate();
match &issues[0] {
Issue::PackageNotInstalled { ref package } => assert_eq!(package, "edon-test-c"),
_ => panic!("invalid issue"),
}
assert_eq!(issues.len(), 1);
}
}