use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct LockfileManager {
lockfile_path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Lockfile {
pub version: String,
pub generated: DateTime<Utc>,
pub packs: Vec<LockEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockEntry {
pub id: String,
pub version: String,
pub sha256: String,
pub source: String,
pub dependencies: Option<Vec<String>>,
}
impl LockfileManager {
pub fn new(project_dir: &Path) -> Self {
let lockfile_path = project_dir.join("rgen.lock");
Self { lockfile_path }
}
pub fn with_path(lockfile_path: PathBuf) -> Self {
Self { lockfile_path }
}
pub fn lockfile_path(&self) -> &Path {
&self.lockfile_path
}
pub fn load(&self) -> Result<Option<Lockfile>> {
if !self.lockfile_path.exists() {
return Ok(None);
}
let content = fs::read_to_string(&self.lockfile_path).context("Failed to read lockfile")?;
let lockfile: Lockfile = toml::from_str(&content).context("Failed to parse lockfile")?;
Ok(Some(lockfile))
}
pub fn create(&self) -> Result<Lockfile> {
Ok(Lockfile {
version: "1.0".to_string(),
generated: Utc::now(),
packs: Vec::new(),
})
}
pub fn save(&self, lockfile: &Lockfile) -> Result<()> {
if let Some(parent) = self.lockfile_path.parent() {
fs::create_dir_all(parent).context("Failed to create lockfile directory")?;
}
let content = toml::to_string_pretty(lockfile).context("Failed to serialize lockfile")?;
fs::write(&self.lockfile_path, content).context("Failed to write lockfile")?;
Ok(())
}
pub fn upsert(&self, pack_id: &str, version: &str, sha256: &str, source: &str) -> Result<()> {
let mut lockfile = self.load()?.unwrap_or_else(|| self.create().unwrap());
lockfile.packs.retain(|entry| entry.id != pack_id);
let dependencies = self.resolve_dependencies(pack_id, version, source)?;
lockfile.packs.push(LockEntry {
id: pack_id.to_string(),
version: version.to_string(),
sha256: sha256.to_string(),
source: source.to_string(),
dependencies,
});
lockfile.packs.sort_by(|a, b| a.id.cmp(&b.id));
self.save(&lockfile)
}
fn resolve_dependencies(
&self, pack_id: &str, version: &str, source: &str,
) -> Result<Option<Vec<String>>> {
let _cache_key = format!("{}@{}", pack_id, version);
if let Ok(manifest) = self.load_pack_manifest(pack_id, version, source) {
if !manifest.dependencies.is_empty() {
let mut resolved_deps = Vec::with_capacity(manifest.dependencies.len());
let dep_futures: Vec<_> = manifest
.dependencies
.iter()
.map(|(dep_id, dep_version)| {
format!("{}@{}", dep_id, dep_version)
})
.collect();
resolved_deps.extend(dep_futures);
resolved_deps.sort();
return Ok(Some(resolved_deps));
}
}
Ok(None)
}
fn load_pack_manifest(
&self, pack_id: &str, version: &str, _source: &str,
) -> Result<crate::rpack::RpackManifest> {
if let Ok(cache_manager) = crate::cache::CacheManager::new() {
if let Ok(cached_pack) = cache_manager.load_cached(pack_id, version) {
if let Some(manifest) = cached_pack.manifest {
return Ok(manifest);
}
}
}
Err(anyhow::anyhow!(
"Could not load manifest for pack {}@{}",
pack_id,
version
))
}
pub fn remove(&self, pack_id: &str) -> Result<bool> {
let mut lockfile = match self.load()? {
Some(lockfile) => lockfile,
None => return Ok(false),
};
let original_len = lockfile.packs.len();
lockfile.packs.retain(|entry| entry.id != pack_id);
if lockfile.packs.len() < original_len {
self.save(&lockfile)?;
Ok(true)
} else {
Ok(false)
}
}
pub fn get(&self, pack_id: &str) -> Result<Option<LockEntry>> {
let lockfile = match self.load()? {
Some(lockfile) => lockfile,
None => return Ok(None),
};
Ok(lockfile.packs.into_iter().find(|entry| entry.id == pack_id))
}
pub fn list(&self) -> Result<Vec<LockEntry>> {
let lockfile = match self.load()? {
Some(lockfile) => lockfile,
None => return Ok(Vec::new()),
};
Ok(lockfile.packs)
}
pub fn is_installed(&self, pack_id: &str) -> Result<bool> {
Ok(self.get(pack_id)?.is_some())
}
pub fn installed_packs(&self) -> Result<HashMap<String, LockEntry>> {
let lockfile = match self.load()? {
Some(lockfile) => lockfile,
None => return Ok(HashMap::new()),
};
Ok(lockfile
.packs
.into_iter()
.map(|entry| (entry.id.clone(), entry))
.collect())
}
pub fn touch(&self) -> Result<()> {
let mut lockfile = self.load()?.unwrap_or_else(|| self.create().unwrap());
lockfile.generated = Utc::now();
self.save(&lockfile)
}
pub fn stats(&self) -> Result<LockfileStats> {
let lockfile = match self.load()? {
Some(lockfile) => lockfile,
None => {
return Ok(LockfileStats {
total_packs: 0,
generated: None,
version: None,
})
}
};
Ok(LockfileStats {
total_packs: lockfile.packs.len(),
generated: Some(lockfile.generated),
version: Some(lockfile.version),
})
}
}
#[derive(Debug, Clone)]
pub struct LockfileStats {
pub total_packs: usize,
pub generated: Option<DateTime<Utc>>,
pub version: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_lockfile_manager_creation() {
let temp_dir = TempDir::new().unwrap();
let manager = LockfileManager::new(temp_dir.path());
assert_eq!(manager.lockfile_path(), temp_dir.path().join("rgen.lock"));
}
#[test]
fn test_lockfile_create_and_save() {
let temp_dir = TempDir::new().unwrap();
let manager = LockfileManager::new(temp_dir.path());
let lockfile = manager.create().unwrap();
manager.save(&lockfile).unwrap();
assert!(manager.lockfile_path().exists());
}
#[test]
fn test_lockfile_load_nonexistent() {
let temp_dir = TempDir::new().unwrap();
let manager = LockfileManager::new(temp_dir.path());
let loaded = manager.load().unwrap();
assert!(loaded.is_none());
}
#[test]
fn test_lockfile_upsert_and_get() {
let temp_dir = TempDir::new().unwrap();
let manager = LockfileManager::new(temp_dir.path());
manager
.upsert("io.rgen.test", "1.0.0", "abc123", "https://example.com")
.unwrap();
let entry = manager.get("io.rgen.test").unwrap().unwrap();
assert_eq!(entry.id, "io.rgen.test");
assert_eq!(entry.version, "1.0.0");
assert_eq!(entry.sha256, "abc123");
assert_eq!(entry.source, "https://example.com");
}
#[test]
fn test_lockfile_remove() {
let temp_dir = TempDir::new().unwrap();
let manager = LockfileManager::new(temp_dir.path());
manager
.upsert("io.rgen.test", "1.0.0", "abc123", "https://example.com")
.unwrap();
assert!(manager.is_installed("io.rgen.test").unwrap());
let removed = manager.remove("io.rgen.test").unwrap();
assert!(removed);
assert!(!manager.is_installed("io.rgen.test").unwrap());
}
#[test]
fn test_lockfile_stats() {
let temp_dir = TempDir::new().unwrap();
let manager = LockfileManager::new(temp_dir.path());
let stats = manager.stats().unwrap();
assert_eq!(stats.total_packs, 0);
assert!(stats.generated.is_none());
assert!(stats.version.is_none());
manager
.upsert("io.rgen.test", "1.0.0", "abc123", "https://example.com")
.unwrap();
let stats = manager.stats().unwrap();
assert_eq!(stats.total_packs, 1);
assert!(stats.generated.is_some());
assert_eq!(stats.version, Some("1.0".to_string()));
}
}