1use std::collections::BTreeMap;
7use std::path::Path;
8
9use semver::Version;
10use serde::{Deserialize, Serialize};
11
12use crate::error::PkgError;
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub struct Lockfile {
17 #[serde(default = "default_version")]
19 pub version: u32,
20
21 #[serde(default, rename = "package")]
23 pub packages: Vec<LockedPackage>,
24}
25
26fn default_version() -> u32 {
27 1
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
32pub struct LockedPackage {
33 pub name: String,
35
36 pub version: String,
38
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub source: Option<String>,
42
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub checksum: Option<String>,
46
47 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
49 pub dependencies: BTreeMap<String, String>,
50}
51
52impl Lockfile {
53 #[must_use]
55 pub fn from_resolved(resolved: &BTreeMap<String, Version>) -> Self {
56 let packages = resolved
57 .iter()
58 .map(|(name, version)| LockedPackage {
59 name: name.clone(),
60 version: version.to_string(),
61 source: None,
62 checksum: None,
63 dependencies: BTreeMap::new(),
64 })
65 .collect();
66
67 Lockfile {
68 version: 1,
69 packages,
70 }
71 }
72
73 pub fn parse(s: &str) -> Result<Self, PkgError> {
75 toml::from_str(s).map_err(|e| PkgError::LockfileParse(e.to_string()))
76 }
77
78 pub fn from_file(path: &Path) -> Result<Self, PkgError> {
80 let content = std::fs::read_to_string(path).map_err(|e| PkgError::Io(e.to_string()))?;
81 Self::parse(&content)
82 }
83
84 pub fn to_toml_string(&self) -> Result<String, PkgError> {
86 let mut output = String::new();
87 output.push_str("# This file is automatically generated by `bock pkg`.\n");
88 output.push_str("# Do not edit manually.\n\n");
89 output.push_str(&format!("version = {}\n", self.version));
90
91 for pkg in &self.packages {
92 output.push('\n');
93 output.push_str("[[package]]\n");
94 output.push_str(&format!("name = \"{}\"\n", pkg.name));
95 output.push_str(&format!("version = \"{}\"\n", pkg.version));
96 if let Some(source) = &pkg.source {
97 output.push_str(&format!("source = \"{source}\"\n"));
98 }
99 if let Some(checksum) = &pkg.checksum {
100 output.push_str(&format!("checksum = \"{checksum}\"\n"));
101 }
102 if !pkg.dependencies.is_empty() {
103 output.push_str("\n[package.dependencies]\n");
104 for (name, ver) in &pkg.dependencies {
105 output.push_str(&format!("{name} = \"{ver}\"\n"));
106 }
107 }
108 }
109
110 Ok(output)
111 }
112
113 pub fn write_to_file(&self, path: &Path) -> Result<(), PkgError> {
115 let content = self.to_toml_string()?;
116 std::fs::write(path, content).map_err(|e| PkgError::Io(e.to_string()))
117 }
118
119 #[must_use]
121 pub fn get_version(&self, name: &str) -> Option<&str> {
122 self.packages
123 .iter()
124 .find(|p| p.name == name)
125 .map(|p| p.version.as_str())
126 }
127
128 pub fn to_resolved(&self) -> Result<BTreeMap<String, Version>, PkgError> {
130 let mut result = BTreeMap::new();
131 for pkg in &self.packages {
132 let ver = crate::version::parse_version(&pkg.version)?;
133 result.insert(pkg.name.clone(), ver);
134 }
135 Ok(result)
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn create_from_resolved() {
145 let mut resolved = BTreeMap::new();
146 resolved.insert("foo".to_string(), Version::new(1, 2, 3));
147 resolved.insert("bar".to_string(), Version::new(0, 5, 0));
148
149 let lockfile = Lockfile::from_resolved(&resolved);
150 assert_eq!(lockfile.packages.len(), 2);
151 assert_eq!(lockfile.get_version("foo"), Some("1.2.3"));
152 assert_eq!(lockfile.get_version("bar"), Some("0.5.0"));
153 }
154
155 #[test]
156 fn roundtrip_serialize() {
157 let mut resolved = BTreeMap::new();
158 resolved.insert("dep-a".to_string(), Version::new(1, 0, 0));
159 resolved.insert("dep-b".to_string(), Version::new(2, 3, 4));
160
161 let lockfile = Lockfile::from_resolved(&resolved);
162 let toml_str = lockfile.to_toml_string().unwrap();
163
164 let reparsed = Lockfile::parse(&toml_str).unwrap();
166 assert_eq!(reparsed.packages.len(), 2);
167 assert_eq!(reparsed.get_version("dep-a"), Some("1.0.0"));
168 assert_eq!(reparsed.get_version("dep-b"), Some("2.3.4"));
169 }
170
171 #[test]
172 fn to_resolved_map() {
173 let lockfile = Lockfile {
174 version: 1,
175 packages: vec![LockedPackage {
176 name: "x".into(),
177 version: "1.0.0".into(),
178 source: None,
179 checksum: None,
180 dependencies: BTreeMap::new(),
181 }],
182 };
183
184 let resolved = lockfile.to_resolved().unwrap();
185 assert_eq!(resolved["x"], Version::new(1, 0, 0));
186 }
187
188 #[test]
189 fn file_roundtrip() {
190 let dir = tempfile::tempdir().unwrap();
191 let path = dir.path().join("bock.lock");
192
193 let mut resolved = BTreeMap::new();
194 resolved.insert("pkg-a".to_string(), Version::new(3, 1, 4));
195
196 let lockfile = Lockfile::from_resolved(&resolved);
197 lockfile.write_to_file(&path).unwrap();
198
199 let loaded = Lockfile::from_file(&path).unwrap();
200 assert_eq!(loaded.get_version("pkg-a"), Some("3.1.4"));
201 }
202}