ic_build/cargo/lock_projection/
mod.rs1use std::{collections::HashMap, fmt};
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)]
19#[non_exhaustive]
20pub enum LockProjectionError {
21 InvalidSelection,
23 Parse {
25 input: &'static str,
27 source: toml::de::Error,
29 },
30 MissingPackages,
32 PackageSelection {
34 package: String,
36 matches: usize,
38 },
39 PreviousVersion {
41 package: String,
43 },
44 ChangedInputs,
46}
47
48impl fmt::Display for LockProjectionError {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 Self::InvalidSelection => f.write_str("invalid workspace version selection"),
52 Self::Parse { input, source } => write!(f, "invalid {input} lockfile: {source}"),
53 Self::MissingPackages => f.write_str("lockfile has no package array"),
54 Self::PackageSelection { package, matches } => write!(
55 f,
56 "workspace package {package} has {matches} unsourced records; expected one"
57 ),
58 Self::PreviousVersion { package } => {
59 write!(f, "unexpected previous workspace version for {package}")
60 }
61 Self::ChangedInputs => {
62 f.write_str("lockfile differs from the selected workspace version projection")
63 }
64 }
65 }
66}
67impl std::error::Error for LockProjectionError {
68 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69 match self {
70 Self::Parse { source, .. } => Some(source),
71 _ => None,
72 }
73 }
74}
75
76pub fn verify_workspace_version_projection(
88 before: &str,
89 after: &str,
90 projections: &[WorkspaceVersionProjection<'_>],
91) -> Result<(), LockProjectionError> {
92 if projections.is_empty() {
93 return Err(LockProjectionError::InvalidSelection);
94 }
95 let mut selected = HashMap::with_capacity(projections.len());
96 for item in projections {
97 if item.package.is_empty()
98 || item.previous.is_empty()
99 || item.next.is_empty()
100 || selected.insert(item.package, Vec::new()).is_some()
101 {
102 return Err(LockProjectionError::InvalidSelection);
103 }
104 }
105 let mut expected: toml::Value =
106 toml::from_str(before).map_err(|source| LockProjectionError::Parse {
107 input: "before",
108 source,
109 })?;
110 let packages = expected
111 .get_mut("package")
112 .and_then(toml::Value::as_array_mut)
113 .ok_or(LockProjectionError::MissingPackages)?;
114 for (index, package) in packages.iter().enumerate() {
115 if package.get("source").is_none()
116 && let Some(matches) = package
117 .get("name")
118 .and_then(toml::Value::as_str)
119 .and_then(|name| selected.get_mut(name))
120 {
121 matches.push(index);
122 }
123 }
124 for projection in projections {
125 let matches = &selected[projection.package];
126 for &index in matches {
127 let package = &mut packages[index];
128 if package.get("version").and_then(toml::Value::as_str) != Some(projection.previous) {
129 return Err(LockProjectionError::PreviousVersion {
130 package: projection.package.to_owned(),
131 });
132 }
133 package["version"] = toml::Value::String(projection.next.to_owned());
134 }
135 if matches.len() != 1 {
136 return Err(LockProjectionError::PackageSelection {
137 package: projection.package.to_owned(),
138 matches: matches.len(),
139 });
140 }
141 }
142 let actual: toml::Value =
143 toml::from_str(after).map_err(|source| LockProjectionError::Parse {
144 input: "after",
145 source,
146 })?;
147 if expected != actual {
148 return Err(LockProjectionError::ChangedInputs);
149 }
150 Ok(())
151}
152
153#[cfg(test)]
154mod tests;