use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
const LOCK_FILE_NAME: &str = "jgpackage.lock";
const LOCK_HEADER: &str = "# Auto-generated by juglans. Do not edit manually.\n\n";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LockedPackage {
pub name: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checksum: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LockFile {
#[serde(default, rename = "package")]
pub packages: Vec<LockedPackage>,
}
impl LockFile {
pub fn load(project_dir: &Path) -> Result<Self> {
let path = project_dir.join(LOCK_FILE_NAME);
if !path.exists() {
return Ok(Self::default());
}
let content = fs::read_to_string(&path)
.with_context(|| format!("Failed to read {}", path.display()))?;
toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))
}
pub fn save(&self, project_dir: &Path) -> Result<PathBuf> {
let path = project_dir.join(LOCK_FILE_NAME);
let toml_str = toml::to_string_pretty(self).context("Failed to serialize lock file")?;
let content = format!("{}{}", LOCK_HEADER, toml_str);
fs::write(&path, &content)
.with_context(|| format!("Failed to write {}", path.display()))?;
Ok(path)
}
pub fn _get(&self, name: &str) -> Option<&LockedPackage> {
self.packages.iter().find(|p| p.name == name)
}
pub fn upsert(&mut self, pkg: LockedPackage) {
if let Some(existing) = self.packages.iter_mut().find(|p| p.name == pkg.name) {
*existing = pkg;
} else {
self.packages.push(pkg);
}
self.packages.sort_by(|a, b| a.name.cmp(&b.name));
}
pub fn remove(&mut self, name: &str) {
self.packages.retain(|p| p.name != name);
}
pub fn to_map(&self) -> BTreeMap<String, String> {
self.packages
.iter()
.map(|p| (p.name.clone(), p.version.clone()))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lock_file_roundtrip() {
let mut lock = LockFile::default();
lock.upsert(LockedPackage {
name: "sqlite-tools".to_string(),
version: "1.2.0".to_string(),
checksum: Some("sha256:abc123".to_string()),
dependencies: vec![],
});
lock.upsert(LockedPackage {
name: "http-client".to_string(),
version: "2.0.1".to_string(),
checksum: None,
dependencies: vec!["json-utils@^1.0".to_string()],
});
let dir = std::env::temp_dir().join("juglans_test_lock");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
lock.save(&dir).unwrap();
let loaded = LockFile::load(&dir).unwrap();
assert_eq!(loaded.packages.len(), 2);
assert_eq!(loaded.packages[0].name, "http-client");
assert_eq!(loaded.packages[1].name, "sqlite-tools");
assert_eq!(loaded.packages[1].version, "1.2.0");
assert_eq!(loaded.packages[0].dependencies, vec!["json-utils@^1.0"]);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_lock_file_empty() {
let dir = std::env::temp_dir().join("juglans_test_lock_empty");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lock = LockFile::load(&dir).unwrap();
assert!(lock.packages.is_empty());
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_lock_upsert_and_remove() {
let mut lock = LockFile::default();
lock.upsert(LockedPackage {
name: "pkg-a".to_string(),
version: "1.0.0".to_string(),
checksum: None,
dependencies: vec![],
});
assert_eq!(lock.packages.len(), 1);
lock.upsert(LockedPackage {
name: "pkg-a".to_string(),
version: "2.0.0".to_string(),
checksum: None,
dependencies: vec![],
});
assert_eq!(lock.packages.len(), 1);
assert_eq!(lock.packages[0].version, "2.0.0");
lock.remove("pkg-a");
assert!(lock.packages.is_empty());
}
#[test]
fn test_lock_to_map() {
let mut lock = LockFile::default();
lock.upsert(LockedPackage {
name: "a".to_string(),
version: "1.0.0".to_string(),
checksum: None,
dependencies: vec![],
});
lock.upsert(LockedPackage {
name: "b".to_string(),
version: "2.0.0".to_string(),
checksum: None,
dependencies: vec![],
});
let map = lock.to_map();
assert_eq!(map.get("a").unwrap(), "1.0.0");
assert_eq!(map.get("b").unwrap(), "2.0.0");
}
}