ic_build/cargo/lock_projection/
mod.rs1use std::collections::HashMap;
4
5#[derive(Clone, Copy, Debug)]
8pub struct WorkspaceVersionProjection<'a> {
9 pub package: &'a str,
11 pub previous: &'a str,
13 pub next: &'a str,
15}
16
17#[derive(Debug, thiserror::Error)]
19#[non_exhaustive]
20pub enum LockProjectionError {
21 #[error("invalid workspace version selection")]
23 InvalidSelection,
24 #[error("invalid {input} lockfile: {source}")]
26 Parse {
27 input: &'static str,
29 source: toml::de::Error,
31 },
32 #[error("lockfile has no package array")]
34 MissingPackages,
35 #[error("workspace package {package} has {matches} unsourced records; expected one")]
37 PackageSelection {
38 package: String,
40 matches: usize,
42 },
43 #[error("unexpected previous workspace version for {package}")]
45 PreviousVersion {
46 package: String,
48 },
49 #[error("lockfile differs from the selected workspace version projection")]
51 ChangedInputs,
52}
53
54pub fn verify_workspace_version_projection(
66 before: &str,
67 after: &str,
68 projections: &[WorkspaceVersionProjection<'_>],
69) -> Result<(), LockProjectionError> {
70 if projections.is_empty() {
71 return Err(LockProjectionError::InvalidSelection);
72 }
73 let mut selected = HashMap::with_capacity(projections.len());
74 for item in projections {
75 if item.package.is_empty()
76 || item.previous.is_empty()
77 || item.next.is_empty()
78 || selected.insert(item.package, Vec::new()).is_some()
79 {
80 return Err(LockProjectionError::InvalidSelection);
81 }
82 }
83 let mut expected: toml::Value =
84 toml::from_str(before).map_err(|source| LockProjectionError::Parse {
85 input: "before",
86 source,
87 })?;
88 let packages = expected
89 .get_mut("package")
90 .and_then(toml::Value::as_array_mut)
91 .ok_or(LockProjectionError::MissingPackages)?;
92 for (index, package) in packages.iter().enumerate() {
93 if package.get("source").is_none()
94 && let Some(matches) = package
95 .get("name")
96 .and_then(toml::Value::as_str)
97 .and_then(|name| selected.get_mut(name))
98 {
99 matches.push(index);
100 }
101 }
102 for projection in projections {
103 let matches = &selected[projection.package];
104 for &index in matches {
105 let package = &mut packages[index];
106 if package.get("version").and_then(toml::Value::as_str) != Some(projection.previous) {
107 return Err(LockProjectionError::PreviousVersion {
108 package: projection.package.to_owned(),
109 });
110 }
111 package["version"] = toml::Value::String(projection.next.to_owned());
112 }
113 if matches.len() != 1 {
114 return Err(LockProjectionError::PackageSelection {
115 package: projection.package.to_owned(),
116 matches: matches.len(),
117 });
118 }
119 }
120 let actual: toml::Value =
121 toml::from_str(after).map_err(|source| LockProjectionError::Parse {
122 input: "after",
123 source,
124 })?;
125 if expected != actual {
126 return Err(LockProjectionError::ChangedInputs);
127 }
128 Ok(())
129}
130
131#[cfg(test)]
132mod tests;