use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Lockfile {
pub version: u32,
pub packages: HashMap<String, LockfileEntry>,
}
impl Lockfile {
pub fn new() -> Self {
Self {
version: 1,
packages: HashMap::new(),
}
}
pub fn load(path: &Path) -> Result<Self, LockfileError> {
let content = fs::read_to_string(path).map_err(|e| LockfileError::Io(e.to_string()))?;
serde_json::from_str(&content).map_err(|e| LockfileError::Parse(e.to_string()))
}
pub fn save(&self, path: &Path) -> Result<(), LockfileError> {
let content =
serde_json::to_string_pretty(self).map_err(|e| LockfileError::Parse(e.to_string()))?;
fs::write(path, content).map_err(|e| LockfileError::Io(e.to_string()))
}
pub fn is_locked(&self, name: &str, version: &str) -> bool {
self.packages
.get(name)
.is_some_and(|entry| entry.version == version)
}
pub fn get_version(&self, name: &str) -> Option<&str> {
self.packages.get(name).map(|e| e.version.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockfileEntry {
pub version: String,
pub resolved: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub integrity: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub dependencies: HashMap<String, String>,
}
#[derive(Debug, thiserror::Error)]
pub enum LockfileError {
#[error("IO error: {0}")]
Io(String),
#[error("Parse error: {0}")]
Parse(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lockfile_new() {
let lockfile = Lockfile::new();
assert_eq!(lockfile.version, 1);
assert!(lockfile.packages.is_empty());
}
#[test]
fn test_lockfile_serialize() {
let mut lockfile = Lockfile::new();
lockfile.packages.insert(
"lodash".to_string(),
LockfileEntry {
version: "4.17.21".to_string(),
resolved: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz".to_string(),
integrity: Some("sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==".to_string()),
dependencies: HashMap::new(),
},
);
let json = serde_json::to_string_pretty(&lockfile).unwrap();
assert!(json.contains("lodash"));
assert!(json.contains("4.17.21"));
}
#[test]
fn test_lockfile_deserialize() {
let json = r#"{
"version": 1,
"packages": {
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
}
}
}"#;
let lockfile: Lockfile = serde_json::from_str(json).unwrap();
assert_eq!(lockfile.version, 1);
assert!(lockfile.packages.contains_key("lodash"));
assert_eq!(lockfile.packages["lodash"].version, "4.17.21");
}
#[test]
fn test_is_locked() {
let mut lockfile = Lockfile::new();
lockfile.packages.insert(
"lodash".to_string(),
LockfileEntry {
version: "4.17.21".to_string(),
resolved: "https://example.com".to_string(),
integrity: None,
dependencies: HashMap::new(),
},
);
assert!(lockfile.is_locked("lodash", "4.17.21"));
assert!(!lockfile.is_locked("lodash", "4.17.20"));
assert!(!lockfile.is_locked("underscore", "1.0.0"));
}
}