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 verify_projection(before, after, projections, false)
71}
72
73pub fn verify_workspace_release_projection(
84 before: &str,
85 after: &str,
86 projections: &[WorkspaceVersionProjection<'_>],
87) -> Result<(), LockProjectionError> {
88 verify_projection(before, after, projections, true)
89}
90
91fn verify_projection(
92 before: &str,
93 after: &str,
94 projections: &[WorkspaceVersionProjection<'_>],
95 references: bool,
96) -> Result<(), LockProjectionError> {
97 if projections.is_empty() {
98 return Err(LockProjectionError::InvalidSelection);
99 }
100 let mut selected = HashMap::with_capacity(projections.len());
101 for item in projections {
102 if item.package.is_empty()
103 || item.previous.is_empty()
104 || item.next.is_empty()
105 || selected.insert(item.package, Vec::new()).is_some()
106 {
107 return Err(LockProjectionError::InvalidSelection);
108 }
109 }
110 let mut expected: toml::Value =
111 toml::from_str(before).map_err(|source| LockProjectionError::Parse {
112 input: "before",
113 source,
114 })?;
115 let packages = expected
116 .get_mut("package")
117 .and_then(toml::Value::as_array_mut)
118 .ok_or(LockProjectionError::MissingPackages)?;
119 for (index, package) in packages.iter().enumerate() {
120 if package.get("source").is_none()
121 && let Some(matches) = package
122 .get("name")
123 .and_then(toml::Value::as_str)
124 .and_then(|name| selected.get_mut(name))
125 {
126 matches.push(index);
127 }
128 }
129 for projection in projections {
130 let matches = &selected[projection.package];
131 for &index in matches {
132 let package = &mut packages[index];
133 if package.get("version").and_then(toml::Value::as_str) != Some(projection.previous) {
134 return Err(LockProjectionError::PreviousVersion {
135 package: projection.package.to_owned(),
136 });
137 }
138 package["version"] = toml::Value::String(projection.next.to_owned());
139 }
140 if matches.len() != 1 {
141 return Err(LockProjectionError::PackageSelection {
142 package: projection.package.to_owned(),
143 matches: matches.len(),
144 });
145 }
146 }
147 if references {
148 for package in packages {
149 if let Some(dependencies) = package
150 .get_mut("dependencies")
151 .and_then(toml::Value::as_array_mut)
152 {
153 for dependency in dependencies {
154 for projection in projections {
155 if dependency.as_str()
156 == Some(
157 format!("{} {}", projection.package, projection.previous).as_str(),
158 )
159 {
160 *dependency = toml::Value::String(format!(
161 "{} {}",
162 projection.package, projection.next
163 ));
164 break;
165 }
166 }
167 }
168 }
169 }
170 }
171 let actual: toml::Value =
172 toml::from_str(after).map_err(|source| LockProjectionError::Parse {
173 input: "after",
174 source,
175 })?;
176 if expected != actual {
177 return Err(LockProjectionError::ChangedInputs);
178 }
179 Ok(())
180}
181
182#[cfg(test)]
183mod tests;