rgen_core/
lockfile.rs

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// use crate::cache::RpackManifest;
9
10/// Lockfile manager for rgen.lock
11#[derive(Debug, Clone)]
12pub struct LockfileManager {
13    lockfile_path: PathBuf,
14}
15
16/// Lockfile structure
17#[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/// Individual lock entry for a pack
25#[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    /// Create a new lockfile manager
36    pub fn new(project_dir: &Path) -> Self {
37        let lockfile_path = project_dir.join("rgen.lock");
38        Self { lockfile_path }
39    }
40
41    /// Create a lockfile manager with custom path
42    pub fn with_path(lockfile_path: PathBuf) -> Self {
43        Self { lockfile_path }
44    }
45
46    /// Get the lockfile path
47    pub fn lockfile_path(&self) -> &Path {
48        &self.lockfile_path
49    }
50
51    /// Load the lockfile if it exists
52    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    /// Create a new lockfile
65    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    /// Save the lockfile
74    pub fn save(&self, lockfile: &Lockfile) -> Result<()> {
75        // Create parent directory if it doesn't exist
76        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    /// Add or update a pack in the lockfile
88    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        // Remove existing entry if present
92        lockfile.packs.retain(|entry| entry.id != pack_id);
93
94        // Resolve dependencies for this pack
95        let dependencies = self.resolve_dependencies(pack_id, version, source)?;
96
97        // Add new entry
98        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        // Sort by pack ID for consistency
107        lockfile.packs.sort_by(|a, b| a.id.cmp(&b.id));
108
109        self.save(&lockfile)
110    }
111
112    /// Resolve dependencies for a pack with caching
113    fn resolve_dependencies(
114        &self, pack_id: &str, version: &str, source: &str,
115    ) -> Result<Option<Vec<String>>> {
116        // Check if we have a cached dependency resolution
117        let _cache_key = format!("{}@{}", pack_id, version);
118
119        // Try to load the pack manifest to get its dependencies
120        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                // Resolve dependencies in parallel for better performance
125                let dep_futures: Vec<_> = manifest
126                    .dependencies
127                    .iter()
128                    .map(|(dep_id, dep_version)| {
129                        // Format as "id@version" for consistency
130                        format!("{}@{}", dep_id, dep_version)
131                    })
132                    .collect();
133
134                resolved_deps.extend(dep_futures);
135
136                // Sort for deterministic output
137                resolved_deps.sort();
138
139                return Ok(Some(resolved_deps));
140            }
141        }
142
143        // If we can't load the manifest or there are no dependencies, return None
144        Ok(None)
145    }
146
147    /// Load pack manifest from cache or source
148    fn load_pack_manifest(
149        &self, pack_id: &str, version: &str, _source: &str,
150    ) -> Result<crate::rpack::RpackManifest> {
151        // First try to load from cache
152        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        // If not in cache, try to load from source (this is a simplified approach)
161        // In a real implementation, you might want to download and parse the manifest
162        Err(anyhow::anyhow!(
163            "Could not load manifest for pack {}@{}",
164            pack_id,
165            version
166        ))
167    }
168
169    /// Remove a pack from the lockfile
170    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    /// Get a specific pack entry
188    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    /// List all installed packs
198    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    /// Check if a pack is installed
208    pub fn is_installed(&self, pack_id: &str) -> Result<bool> {
209        Ok(self.get(pack_id)?.is_some())
210    }
211
212    /// Get installed packs as a map for quick lookup
213    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    /// Update the generated timestamp
227    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    /// Get lockfile statistics
235    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/// Lockfile statistics
256#[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        // Upsert a pack
302        manager
303            .upsert("io.rgen.test", "1.0.0", "abc123", "https://example.com")
304            .unwrap();
305
306        // Get the pack
307        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        // Add a pack
320        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        // Remove the pack
326        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        // Empty lockfile
337        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        // Add a pack
343        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}