1use camino::Utf8PathBuf;
2
3use cabin_core::PackageName;
4
5pub const LOCKFILE_VERSION: u32 = 1;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Lockfile {
18 pub version: u32,
20 pub packages: Vec<LockedPackage>,
22 pub patches: Vec<LockedPatch>,
28 pub source_replacements: Vec<LockedSourceReplacement>,
31}
32
33impl Default for Lockfile {
34 fn default() -> Self {
35 Self::empty()
36 }
37}
38
39impl Lockfile {
40 pub fn empty() -> Self {
42 Self {
43 version: LOCKFILE_VERSION,
44 packages: Vec::new(),
45 patches: Vec::new(),
46 source_replacements: Vec::new(),
47 }
48 }
49
50 pub fn find(&self, name: &PackageName) -> Option<&LockedPackage> {
53 self.packages.iter().find(|p| &p.name == name)
54 }
55
56 pub fn matches_patch_state(
65 &self,
66 active_patches: &[LockedPatch],
67 active_source_replacements: &[LockedSourceReplacement],
68 ) -> bool {
69 active_patches == self.patches.as_slice()
70 && active_source_replacements == self.source_replacements.as_slice()
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct LockedPackage {
77 pub name: PackageName,
78 pub version: semver::Version,
79 pub source: LockedSource,
80 pub checksum: Option<String>,
84 pub dependencies: Vec<PackageName>,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum LockedSource {
91 Index,
93}
94
95impl LockedSource {
96 pub fn as_str(self) -> &'static str {
97 match self {
98 LockedSource::Index => "index",
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct LockedPatch {
110 pub package: PackageName,
111 pub version: semver::Version,
112 pub kind: LockedPatchKind,
113 pub provenance: String,
114 pub path: Utf8PathBuf,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum LockedPatchKind {
122 Path,
124}
125
126impl LockedPatchKind {
127 pub fn as_str(self) -> &'static str {
128 match self {
129 LockedPatchKind::Path => "path",
130 }
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct LockedSourceReplacement {
138 pub original: String,
139 pub original_kind: LockedSourceLocatorKind,
140 pub replacement: String,
141 pub replacement_kind: LockedSourceLocatorKind,
142 pub provenance: String,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum LockedSourceLocatorKind {
148 IndexPath,
149 IndexUrl,
150}
151
152impl LockedSourceLocatorKind {
153 pub fn as_str(self) -> &'static str {
154 match self {
155 LockedSourceLocatorKind::IndexPath => "index-path",
156 LockedSourceLocatorKind::IndexUrl => "index-url",
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 fn pkg(name: &str) -> PackageName {
166 PackageName::new(name).unwrap()
167 }
168
169 fn ver(s: &str) -> semver::Version {
170 semver::Version::parse(s).unwrap()
171 }
172
173 fn patch(name: &str, version: &str) -> LockedPatch {
174 LockedPatch {
175 package: pkg(name),
176 version: ver(version),
177 kind: LockedPatchKind::Path,
178 provenance: "manifest".into(),
179 path: Utf8PathBuf::from("../").join(name),
180 }
181 }
182
183 fn replacement(original: &str, replacement: &str) -> LockedSourceReplacement {
184 LockedSourceReplacement {
185 original: original.into(),
186 original_kind: LockedSourceLocatorKind::IndexUrl,
187 replacement: replacement.into(),
188 replacement_kind: LockedSourceLocatorKind::IndexPath,
189 provenance: "user-config".into(),
190 }
191 }
192
193 #[test]
194 fn matches_patch_state_returns_true_for_equal_slices() {
195 let lock = Lockfile {
196 version: LOCKFILE_VERSION,
197 packages: Vec::new(),
198 patches: vec![patch("fmt", "10.2.1")],
199 source_replacements: vec![replacement("https://example.com/index", "../mirror")],
200 };
201 assert!(lock.matches_patch_state(
202 &[patch("fmt", "10.2.1")],
203 &[replacement("https://example.com/index", "../mirror")],
204 ));
205 }
206
207 #[test]
208 fn matches_patch_state_detects_added_patch() {
209 let lock = Lockfile {
210 version: LOCKFILE_VERSION,
211 packages: Vec::new(),
212 patches: Vec::new(),
213 source_replacements: Vec::new(),
214 };
215 assert!(!lock.matches_patch_state(&[patch("fmt", "10.2.1")], &[]));
216 }
217
218 #[test]
219 fn matches_patch_state_detects_removed_replacement() {
220 let lock = Lockfile {
221 version: LOCKFILE_VERSION,
222 packages: Vec::new(),
223 patches: Vec::new(),
224 source_replacements: vec![replacement("https://example.com/index", "../mirror")],
225 };
226 assert!(!lock.matches_patch_state(&[], &[]));
227 }
228
229 #[test]
230 fn matches_patch_state_is_order_sensitive() {
231 let lock = Lockfile {
238 version: LOCKFILE_VERSION,
239 packages: Vec::new(),
240 patches: vec![patch("a", "1.0.0"), patch("b", "1.0.0")],
241 source_replacements: Vec::new(),
242 };
243 assert!(!lock.matches_patch_state(&[patch("b", "1.0.0"), patch("a", "1.0.0")], &[],));
244 }
245}