use std::collections::HashMap;
#[derive(Clone, Copy, Debug)]
pub struct WorkspaceVersionProjection<'a> {
pub package: &'a str,
pub previous: &'a str,
pub next: &'a str,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum LockProjectionError {
#[error("invalid workspace version selection")]
InvalidSelection,
#[error("invalid {input} lockfile: {source}")]
Parse {
input: &'static str,
source: toml::de::Error,
},
#[error("lockfile has no package array")]
MissingPackages,
#[error("workspace package {package} has {matches} unsourced records; expected one")]
PackageSelection {
package: String,
matches: usize,
},
#[error("unexpected previous workspace version for {package}")]
PreviousVersion {
package: String,
},
#[error("lockfile differs from the selected workspace version projection")]
ChangedInputs,
}
pub fn verify_workspace_version_projection(
before: &str,
after: &str,
projections: &[WorkspaceVersionProjection<'_>],
) -> Result<(), LockProjectionError> {
if projections.is_empty() {
return Err(LockProjectionError::InvalidSelection);
}
let mut selected = HashMap::with_capacity(projections.len());
for item in projections {
if item.package.is_empty()
|| item.previous.is_empty()
|| item.next.is_empty()
|| selected.insert(item.package, Vec::new()).is_some()
{
return Err(LockProjectionError::InvalidSelection);
}
}
let mut expected: toml::Value =
toml::from_str(before).map_err(|source| LockProjectionError::Parse {
input: "before",
source,
})?;
let packages = expected
.get_mut("package")
.and_then(toml::Value::as_array_mut)
.ok_or(LockProjectionError::MissingPackages)?;
for (index, package) in packages.iter().enumerate() {
if package.get("source").is_none()
&& let Some(matches) = package
.get("name")
.and_then(toml::Value::as_str)
.and_then(|name| selected.get_mut(name))
{
matches.push(index);
}
}
for projection in projections {
let matches = &selected[projection.package];
for &index in matches {
let package = &mut packages[index];
if package.get("version").and_then(toml::Value::as_str) != Some(projection.previous) {
return Err(LockProjectionError::PreviousVersion {
package: projection.package.to_owned(),
});
}
package["version"] = toml::Value::String(projection.next.to_owned());
}
if matches.len() != 1 {
return Err(LockProjectionError::PackageSelection {
package: projection.package.to_owned(),
matches: matches.len(),
});
}
}
let actual: toml::Value =
toml::from_str(after).map_err(|source| LockProjectionError::Parse {
input: "after",
source,
})?;
if expected != actual {
return Err(LockProjectionError::ChangedInputs);
}
Ok(())
}
#[cfg(test)]
mod tests;