1use anyhow::{Context, Result};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone)]
12pub struct LockfileManager {
13 lockfile_path: PathBuf,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Lockfile {
19 pub version: String,
20 pub generated: DateTime<Utc>,
21 pub packs: Vec<LockEntry>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct LockEntry {
27 pub id: String,
28 pub version: String,
29 pub sha256: String,
30 pub source: String,
31 pub dependencies: Option<Vec<String>>,
32}
33
34impl LockfileManager {
35 pub fn new(project_dir: &Path) -> Self {
37 let lockfile_path = project_dir.join("rgen.lock");
38 Self { lockfile_path }
39 }
40
41 pub fn with_path(lockfile_path: PathBuf) -> Self {
43 Self { lockfile_path }
44 }
45
46 pub fn lockfile_path(&self) -> &Path {
48 &self.lockfile_path
49 }
50
51 pub fn load(&self) -> Result<Option<Lockfile>> {
53 if !self.lockfile_path.exists() {
54 return Ok(None);
55 }
56
57 let content = fs::read_to_string(&self.lockfile_path).context("Failed to read lockfile")?;
58
59 let lockfile: Lockfile = toml::from_str(&content).context("Failed to parse lockfile")?;
60
61 Ok(Some(lockfile))
62 }
63
64 pub fn create(&self) -> Result<Lockfile> {
66 Ok(Lockfile {
67 version: "1.0".to_string(),
68 generated: Utc::now(),
69 packs: Vec::new(),
70 })
71 }
72
73 pub fn save(&self, lockfile: &Lockfile) -> Result<()> {
75 if let Some(parent) = self.lockfile_path.parent() {
77 fs::create_dir_all(parent).context("Failed to create lockfile directory")?;
78 }
79
80 let content = toml::to_string_pretty(lockfile).context("Failed to serialize lockfile")?;
81
82 fs::write(&self.lockfile_path, content).context("Failed to write lockfile")?;
83
84 Ok(())
85 }
86
87 pub fn upsert(&self, pack_id: &str, version: &str, sha256: &str, source: &str) -> Result<()> {
89 let mut lockfile = self.load()?.unwrap_or_else(|| self.create().unwrap());
90
91 lockfile.packs.retain(|entry| entry.id != pack_id);
93
94 let dependencies = self.resolve_dependencies(pack_id, version, source)?;
96
97 lockfile.packs.push(LockEntry {
99 id: pack_id.to_string(),
100 version: version.to_string(),
101 sha256: sha256.to_string(),
102 source: source.to_string(),
103 dependencies,
104 });
105
106 lockfile.packs.sort_by(|a, b| a.id.cmp(&b.id));
108
109 self.save(&lockfile)
110 }
111
112 fn resolve_dependencies(
114 &self, pack_id: &str, version: &str, source: &str,
115 ) -> Result<Option<Vec<String>>> {
116 let _cache_key = format!("{}@{}", pack_id, version);
118
119 if let Ok(manifest) = self.load_pack_manifest(pack_id, version, source) {
121 if !manifest.dependencies.is_empty() {
122 let mut resolved_deps = Vec::with_capacity(manifest.dependencies.len());
123
124 let dep_futures: Vec<_> = manifest
126 .dependencies
127 .iter()
128 .map(|(dep_id, dep_version)| {
129 format!("{}@{}", dep_id, dep_version)
131 })
132 .collect();
133
134 resolved_deps.extend(dep_futures);
135
136 resolved_deps.sort();
138
139 return Ok(Some(resolved_deps));
140 }
141 }
142
143 Ok(None)
145 }
146
147 fn load_pack_manifest(
149 &self, pack_id: &str, version: &str, _source: &str,
150 ) -> Result<crate::rpack::RpackManifest> {
151 if let Ok(cache_manager) = crate::cache::CacheManager::new() {
153 if let Ok(cached_pack) = cache_manager.load_cached(pack_id, version) {
154 if let Some(manifest) = cached_pack.manifest {
155 return Ok(manifest);
156 }
157 }
158 }
159
160 Err(anyhow::anyhow!(
163 "Could not load manifest for pack {}@{}",
164 pack_id,
165 version
166 ))
167 }
168
169 pub fn remove(&self, pack_id: &str) -> Result<bool> {
171 let mut lockfile = match self.load()? {
172 Some(lockfile) => lockfile,
173 None => return Ok(false),
174 };
175
176 let original_len = lockfile.packs.len();
177 lockfile.packs.retain(|entry| entry.id != pack_id);
178
179 if lockfile.packs.len() < original_len {
180 self.save(&lockfile)?;
181 Ok(true)
182 } else {
183 Ok(false)
184 }
185 }
186
187 pub fn get(&self, pack_id: &str) -> Result<Option<LockEntry>> {
189 let lockfile = match self.load()? {
190 Some(lockfile) => lockfile,
191 None => return Ok(None),
192 };
193
194 Ok(lockfile.packs.into_iter().find(|entry| entry.id == pack_id))
195 }
196
197 pub fn list(&self) -> Result<Vec<LockEntry>> {
199 let lockfile = match self.load()? {
200 Some(lockfile) => lockfile,
201 None => return Ok(Vec::new()),
202 };
203
204 Ok(lockfile.packs)
205 }
206
207 pub fn is_installed(&self, pack_id: &str) -> Result<bool> {
209 Ok(self.get(pack_id)?.is_some())
210 }
211
212 pub fn installed_packs(&self) -> Result<HashMap<String, LockEntry>> {
214 let lockfile = match self.load()? {
215 Some(lockfile) => lockfile,
216 None => return Ok(HashMap::new()),
217 };
218
219 Ok(lockfile
220 .packs
221 .into_iter()
222 .map(|entry| (entry.id.clone(), entry))
223 .collect())
224 }
225
226 pub fn touch(&self) -> Result<()> {
228 let mut lockfile = self.load()?.unwrap_or_else(|| self.create().unwrap());
229
230 lockfile.generated = Utc::now();
231 self.save(&lockfile)
232 }
233
234 pub fn stats(&self) -> Result<LockfileStats> {
236 let lockfile = match self.load()? {
237 Some(lockfile) => lockfile,
238 None => {
239 return Ok(LockfileStats {
240 total_packs: 0,
241 generated: None,
242 version: None,
243 })
244 }
245 };
246
247 Ok(LockfileStats {
248 total_packs: lockfile.packs.len(),
249 generated: Some(lockfile.generated),
250 version: Some(lockfile.version),
251 })
252 }
253}
254
255#[derive(Debug, Clone)]
257pub struct LockfileStats {
258 pub total_packs: usize,
259 pub generated: Option<DateTime<Utc>>,
260 pub version: Option<String>,
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use tempfile::TempDir;
267
268 #[test]
269 fn test_lockfile_manager_creation() {
270 let temp_dir = TempDir::new().unwrap();
271 let manager = LockfileManager::new(temp_dir.path());
272
273 assert_eq!(manager.lockfile_path(), temp_dir.path().join("rgen.lock"));
274 }
275
276 #[test]
277 fn test_lockfile_create_and_save() {
278 let temp_dir = TempDir::new().unwrap();
279 let manager = LockfileManager::new(temp_dir.path());
280
281 let lockfile = manager.create().unwrap();
282 manager.save(&lockfile).unwrap();
283
284 assert!(manager.lockfile_path().exists());
285 }
286
287 #[test]
288 fn test_lockfile_load_nonexistent() {
289 let temp_dir = TempDir::new().unwrap();
290 let manager = LockfileManager::new(temp_dir.path());
291
292 let loaded = manager.load().unwrap();
293 assert!(loaded.is_none());
294 }
295
296 #[test]
297 fn test_lockfile_upsert_and_get() {
298 let temp_dir = TempDir::new().unwrap();
299 let manager = LockfileManager::new(temp_dir.path());
300
301 manager
303 .upsert("io.rgen.test", "1.0.0", "abc123", "https://example.com")
304 .unwrap();
305
306 let entry = manager.get("io.rgen.test").unwrap().unwrap();
308 assert_eq!(entry.id, "io.rgen.test");
309 assert_eq!(entry.version, "1.0.0");
310 assert_eq!(entry.sha256, "abc123");
311 assert_eq!(entry.source, "https://example.com");
312 }
313
314 #[test]
315 fn test_lockfile_remove() {
316 let temp_dir = TempDir::new().unwrap();
317 let manager = LockfileManager::new(temp_dir.path());
318
319 manager
321 .upsert("io.rgen.test", "1.0.0", "abc123", "https://example.com")
322 .unwrap();
323 assert!(manager.is_installed("io.rgen.test").unwrap());
324
325 let removed = manager.remove("io.rgen.test").unwrap();
327 assert!(removed);
328 assert!(!manager.is_installed("io.rgen.test").unwrap());
329 }
330
331 #[test]
332 fn test_lockfile_stats() {
333 let temp_dir = TempDir::new().unwrap();
334 let manager = LockfileManager::new(temp_dir.path());
335
336 let stats = manager.stats().unwrap();
338 assert_eq!(stats.total_packs, 0);
339 assert!(stats.generated.is_none());
340 assert!(stats.version.is_none());
341
342 manager
344 .upsert("io.rgen.test", "1.0.0", "abc123", "https://example.com")
345 .unwrap();
346
347 let stats = manager.stats().unwrap();
348 assert_eq!(stats.total_packs, 1);
349 assert!(stats.generated.is_some());
350 assert_eq!(stats.version, Some("1.0".to_string()));
351 }
352}