use std::fmt;
#[derive(Clone, Copy, Debug)]
pub struct WorkspaceVersionProjection<'a> {
pub package: &'a str,
pub previous: &'a str,
pub next: &'a str,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum LockProjectionError {
InvalidSelection,
Parse {
input: &'static str,
source: toml::de::Error,
},
MissingPackages,
PackageSelection {
package: String,
matches: usize,
},
PreviousVersion {
package: String,
},
ChangedInputs,
}
impl fmt::Display for LockProjectionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidSelection => f.write_str("invalid workspace version selection"),
Self::Parse { input, source } => write!(f, "invalid {input} lockfile: {source}"),
Self::MissingPackages => f.write_str("lockfile has no package array"),
Self::PackageSelection { package, matches } => write!(
f,
"workspace package {package} has {matches} unsourced records; expected one"
),
Self::PreviousVersion { package } => {
write!(f, "unexpected previous workspace version for {package}")
}
Self::ChangedInputs => {
f.write_str("lockfile differs from the selected workspace version projection")
}
}
}
}
impl std::error::Error for LockProjectionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Parse { source, .. } => Some(source),
_ => None,
}
}
}
pub fn verify_workspace_version_projection(
before: &str,
after: &str,
projections: &[WorkspaceVersionProjection<'_>],
) -> Result<(), LockProjectionError> {
if projections.is_empty()
|| projections.iter().enumerate().any(|(index, item)| {
item.package.is_empty()
|| item.previous.is_empty()
|| item.next.is_empty()
|| projections[..index]
.iter()
.any(|other| other.package == item.package)
})
{
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 projection in projections {
let mut matches = 0;
for package in packages.iter_mut().filter(|package| {
package.get("name").and_then(toml::Value::as_str) == Some(projection.package)
&& package.get("source").is_none()
}) {
matches += 1;
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 != 1 {
return Err(LockProjectionError::PackageSelection {
package: projection.package.to_owned(),
matches,
});
}
}
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;