use crate::{Lockfile, Manifest};
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub enum ValidationError {
MissingDependency {
name: String,
},
InvalidDependency {
package: String,
dependency: String,
},
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationError::MissingDependency { name } => {
write!(
f,
"Package '{}' is in the lockfile but not in the manifest",
name
)
}
ValidationError::InvalidDependency {
package,
dependency,
} => {
write!(
f,
"Package '{}' depends on '{}' which doesn't exist in the lockfile",
package, dependency
)
}
}
}
}
impl std::error::Error for ValidationError {}
pub fn validate(manifest: &Manifest, lockfile: &Lockfile) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
let manifest_deps: HashSet<&str> = manifest
.all_dependencies()
.map(|(name, _, _)| name.as_str())
.collect();
let lockfile_packages: HashSet<&str> = lockfile
.all_packages()
.map(|(p, _)| p.name.as_str())
.collect();
for (package, _pkg_type) in lockfile.all_packages() {
if !manifest_deps.contains(package.name.as_str()) {
errors.push(ValidationError::MissingDependency {
name: package.name.clone(),
});
}
for dep in &package.dependencies {
if !lockfile_packages.contains(dep.name.as_str()) {
errors.push(ValidationError::InvalidDependency {
package: package.name.clone(),
dependency: dep.name.clone(),
});
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Dependency, Package, PackageDependency};
use std::collections::HashMap;
#[test]
fn test_validate_success() {
let mut interfaces = HashMap::new();
interfaces.insert(
"wasi:logging".to_string(),
Dependency::Compact("ghcr.io/webassembly/wasi-logging:1.0.0".to_string()),
);
interfaces.insert(
"wasi:key-value".to_string(),
Dependency::Compact("ghcr.io/webassembly/wasi-key-value:2.0.0".to_string()),
);
let manifest = Manifest {
interfaces,
..Default::default()
};
let lockfile = Lockfile {
lockfile_version: 2,
components: vec![],
interfaces: vec![
Package {
name: "wasi:logging".to_string(),
version: "1.0.0".to_string(),
registry: "ghcr.io/webassembly/wasi-logging".to_string(),
digest: "sha256:abc123".to_string(),
dependencies: vec![],
},
Package {
name: "wasi:key-value".to_string(),
version: "2.0.0".to_string(),
registry: "ghcr.io/webassembly/wasi-key-value".to_string(),
digest: "sha256:def456".to_string(),
dependencies: vec![PackageDependency {
name: "wasi:logging".to_string(),
version: "1.0.0".to_string(),
}],
},
],
};
assert!(validate(&manifest, &lockfile).is_ok());
}
#[test]
fn test_validate_missing_dependency() {
let mut interfaces = HashMap::new();
interfaces.insert(
"wasi:logging".to_string(),
Dependency::Compact("ghcr.io/webassembly/wasi-logging:1.0.0".to_string()),
);
let manifest = Manifest {
interfaces,
..Default::default()
};
let lockfile = Lockfile {
lockfile_version: 2,
components: vec![],
interfaces: vec![
Package {
name: "wasi:logging".to_string(),
version: "1.0.0".to_string(),
registry: "ghcr.io/webassembly/wasi-logging".to_string(),
digest: "sha256:abc123".to_string(),
dependencies: vec![],
},
Package {
name: "wasi:key-value".to_string(),
version: "2.0.0".to_string(),
registry: "ghcr.io/webassembly/wasi-key-value".to_string(),
digest: "sha256:def456".to_string(),
dependencies: vec![],
},
],
};
let result = validate(&manifest, &lockfile);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0],
ValidationError::MissingDependency {
name: "wasi:key-value".to_string()
}
);
}
#[test]
fn test_validate_invalid_dependency() {
let mut interfaces = HashMap::new();
interfaces.insert(
"wasi:logging".to_string(),
Dependency::Compact("ghcr.io/webassembly/wasi-logging:1.0.0".to_string()),
);
interfaces.insert(
"wasi:key-value".to_string(),
Dependency::Compact("ghcr.io/webassembly/wasi-key-value:2.0.0".to_string()),
);
let manifest = Manifest {
interfaces,
..Default::default()
};
let lockfile = Lockfile {
lockfile_version: 2,
components: vec![],
interfaces: vec![
Package {
name: "wasi:logging".to_string(),
version: "1.0.0".to_string(),
registry: "ghcr.io/webassembly/wasi-logging".to_string(),
digest: "sha256:abc123".to_string(),
dependencies: vec![],
},
Package {
name: "wasi:key-value".to_string(),
version: "2.0.0".to_string(),
registry: "ghcr.io/webassembly/wasi-key-value".to_string(),
digest: "sha256:def456".to_string(),
dependencies: vec![
PackageDependency {
name: "wasi:logging".to_string(),
version: "1.0.0".to_string(),
},
PackageDependency {
name: "wasi:http".to_string(), version: "1.0.0".to_string(),
},
],
},
],
};
let result = validate(&manifest, &lockfile);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0],
ValidationError::InvalidDependency {
package: "wasi:key-value".to_string(),
dependency: "wasi:http".to_string()
}
);
}
#[test]
fn test_validate_empty() {
let manifest = Manifest::default();
let lockfile = Lockfile {
lockfile_version: 2,
components: vec![],
interfaces: vec![],
};
assert!(validate(&manifest, &lockfile).is_ok());
}
#[test]
fn test_validation_error_display() {
let err1 = ValidationError::MissingDependency {
name: "wasi:logging".to_string(),
};
assert_eq!(
err1.to_string(),
"Package 'wasi:logging' is in the lockfile but not in the manifest"
);
let err2 = ValidationError::InvalidDependency {
package: "wasi:key-value".to_string(),
dependency: "wasi:http".to_string(),
};
assert_eq!(
err2.to_string(),
"Package 'wasi:key-value' depends on 'wasi:http' which doesn't exist in the lockfile"
);
}
#[test]
fn test_validate_components_and_interfaces() {
let mut components = HashMap::new();
components.insert(
"root:component".to_string(),
Dependency::Compact("ghcr.io/example/component:0.1.0".to_string()),
);
let mut interfaces = HashMap::new();
interfaces.insert(
"wasi:logging".to_string(),
Dependency::Compact("ghcr.io/webassembly/wasi-logging:1.0.0".to_string()),
);
let manifest = Manifest {
components,
interfaces,
};
let lockfile = Lockfile {
lockfile_version: 2,
components: vec![Package {
name: "root:component".to_string(),
version: "0.1.0".to_string(),
registry: "ghcr.io/example/component".to_string(),
digest: "sha256:comp123".to_string(),
dependencies: vec![],
}],
interfaces: vec![Package {
name: "wasi:logging".to_string(),
version: "1.0.0".to_string(),
registry: "ghcr.io/webassembly/wasi-logging".to_string(),
digest: "sha256:abc123".to_string(),
dependencies: vec![],
}],
};
assert!(validate(&manifest, &lockfile).is_ok());
}
}