Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion crates/wasm-pkg-common/src/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,25 @@ impl std::fmt::Display for PackageSpec {
}

/// A package spec combines a [`PackageRef`] with an optional version.
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub struct PackageSpec {
pub package: PackageRef,
pub version: Option<Version>,
}

impl PackageSpec {
/// The requirement used to fetch this package from a registry: exactly its version, or any
/// version (`*`) for a package that names none.
pub fn version_req(&self) -> VersionReq {
match &self.version {
Some(version) => format!("={version}")
.parse()
.expect("an exact version is always a valid requirement"),
None => VersionReq::STAR,
}
}
}

impl PartialEq<str> for PackageSpec {
fn eq(&self, other: &str) -> bool {
// clippy --fix will create a recursive callsite here if `self.to_string()` is used instead
Expand Down
121 changes: 119 additions & 2 deletions crates/wasm-pkg-core/src/manifest.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
//! Type definitions and functions for working with `wkg.toml` files.

use std::{
collections::HashMap,
collections::{BTreeSet, HashMap},
path::{Path, PathBuf},
};

use anyhow::{Context, Result};
use semver::VersionReq;
use serde::{Deserialize, Serialize};
use wasm_pkg_common::package::{PackageRef, PackageSpec};
mod paths;
pub mod workspace;

Expand Down Expand Up @@ -42,7 +43,8 @@ pub struct Manifest {
}

impl Manifest {
fn from_toml(contents: &str) -> Result<Manifest> {
/// Parses and validates a manifest from its TOML contents.
pub fn from_toml(contents: &str) -> Result<Manifest> {
let manifest: Manifest = toml::from_str(contents)?;
manifest.validate()?;
Ok(manifest)
Expand Down Expand Up @@ -79,10 +81,61 @@ impl Manifest {
// `Manifest` validations, mirrors cargo's `Workspace::validate`
fn validate(&self) -> Result<()> {
self.validate_workspace_exclusivity()?;
self.validate_override_keys()?;
// Add new validation rules with `self.validate_*()?;`
Ok(())
}

/// Checks that override keys parse and that no package is covered by both a bare and a
/// versioned key.
pub(crate) fn validate_override_keys(&self) -> Result<()> {
let Some(overrides) = self.overrides.as_ref() else {
return Ok(());
};
// `overrides` is a map, so walk it in a stable order
let sorted_keys: BTreeSet<&str> = overrides.keys().map(String::as_str).collect();

// A bare key (`ns:pkg`) covers every version of its package, a versioned key
// (`ns:pkg@1.2.3`) exactly one, so a package may be named by one kind or the other but
// not both
let mut bare: BTreeSet<PackageRef> = BTreeSet::new();
let mut versioned: Vec<(&str, PackageRef)> = Vec::new();
for key in sorted_keys {
let spec: PackageSpec = key
.parse()
.with_context(|| format!("invalid override key `{key}`"))?;
match spec.version {
Some(_) => versioned.push((key, spec.package)),
None => {
bare.insert(spec.package);
}
}
}
let conflicts: Vec<String> = bare
.iter()
.filter_map(|package| {
let keys: Vec<&str> = versioned
.iter()
.filter(|(_, p)| p == package)
.map(|(key, _)| *key)
.collect();
(!keys.is_empty()).then(|| {
format!(
"`{package}` covers every version and overlaps `{}`",
keys.join("`, `")
)
})
})
.collect();
if conflicts.is_empty() {
return Ok(());
}
anyhow::bail!(
"overrides must not overlap: {} - remove one or the other",
conflicts.join("; ")
);
}

// no overrides or top-level metadata when workspace is present
fn validate_workspace_exclusivity(&self) -> Result<()> {
if self.workspace.is_none() {
Expand Down Expand Up @@ -241,4 +294,68 @@ mod tests {
"manifest loaded from file does not match original manifest"
);
}

#[test]
fn override_keys_may_carry_a_version() {
let manifest = Manifest::from_toml(
r#"
[overrides]
"foo:bar@0.1.0" = { path = "bar-0.1.0" }
"foo:bar@0.2.0" = { path = "bar-0.2.0" }
"foo:baz" = { path = "baz" }
"#,
)
.expect("versioned override keys should be accepted");
assert_eq!(manifest.overrides.unwrap().len(), 3);
}

#[test]
fn override_keys_conflict_when_bare_and_versioned() {
let err = Manifest::from_toml(
r#"
[overrides]
"foo:bar" = { path = "bar" }
"foo:bar@0.1.0" = { path = "bar-0.1.0" }
"#,
)
.expect_err("a bare key alongside a versioned one is ambiguous");
let err = format!("{err:#}");
assert!(err.contains("foo:bar@0.1.0"), "unexpected error: {err}");
}

#[test]
fn override_key_conflicts_are_all_reported_in_a_stable_order() {
// Two conflicting packages: both must appear, and always in the same order, rather than
// whichever the underlying map happened to yield first.
let err = Manifest::from_toml(
r#"
[overrides]
"zzz:two" = { path = "z" }
"zzz:two@0.2.0" = { path = "z2" }
"aaa:one" = { path = "a" }
"aaa:one@0.1.0" = { path = "a1" }
"#,
)
.expect_err("both packages conflict");
let err = format!("{err:#}");
let aaa = err.find("aaa:one").expect("aaa:one should be reported");
let zzz = err.find("zzz:two").expect("zzz:two should be reported");
assert!(aaa < zzz, "conflicts should be sorted: {err}");
}

#[test]
fn override_keys_must_parse() {
let err = Manifest::from_toml(
r#"
[overrides]
"not a package ref" = { path = "bar" }
"#,
)
.expect_err("an unparseable override key should be rejected");
let err = format!("{err:#}");
assert!(
err.contains("invalid override key"),
"unexpected error: {err}"
);
}
}
Loading