ic_build/cargo/lock_projection/
mod.rs1use std::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 || projections.iter().enumerate().any(|(index, item)| {
94 item.package.is_empty()
95 || item.previous.is_empty()
96 || item.next.is_empty()
97 || projections[..index]
98 .iter()
99 .any(|other| other.package == item.package)
100 })
101 {
102 return Err(LockProjectionError::InvalidSelection);
103 }
104 let mut expected: toml::Value =
105 toml::from_str(before).map_err(|source| LockProjectionError::Parse {
106 input: "before",
107 source,
108 })?;
109 let packages = expected
110 .get_mut("package")
111 .and_then(toml::Value::as_array_mut)
112 .ok_or(LockProjectionError::MissingPackages)?;
113 for projection in projections {
114 let mut matches = 0;
115 for package in packages.iter_mut().filter(|package| {
116 package.get("name").and_then(toml::Value::as_str) == Some(projection.package)
117 && package.get("source").is_none()
118 }) {
119 matches += 1;
120 if package.get("version").and_then(toml::Value::as_str) != Some(projection.previous) {
121 return Err(LockProjectionError::PreviousVersion {
122 package: projection.package.to_owned(),
123 });
124 }
125 package["version"] = toml::Value::String(projection.next.to_owned());
126 }
127 if matches != 1 {
128 return Err(LockProjectionError::PackageSelection {
129 package: projection.package.to_owned(),
130 matches,
131 });
132 }
133 }
134 let actual: toml::Value =
135 toml::from_str(after).map_err(|source| LockProjectionError::Parse {
136 input: "after",
137 source,
138 })?;
139 if expected != actual {
140 return Err(LockProjectionError::ChangedInputs);
141 }
142 Ok(())
143}
144
145#[cfg(test)]
146mod tests;