1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::Path;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10pub struct Lockfile {
11 pub version: u32,
13
14 pub packages: HashMap<String, LockfileEntry>,
16}
17
18impl Lockfile {
19 pub fn new() -> Self {
20 Self {
21 version: 1,
22 packages: HashMap::new(),
23 }
24 }
25
26 pub fn load(path: &Path) -> Result<Self, LockfileError> {
28 let content = fs::read_to_string(path).map_err(|e| LockfileError::Io(e.to_string()))?;
29
30 serde_json::from_str(&content).map_err(|e| LockfileError::Parse(e.to_string()))
31 }
32
33 pub fn save(&self, path: &Path) -> Result<(), LockfileError> {
35 let content =
36 serde_json::to_string_pretty(self).map_err(|e| LockfileError::Parse(e.to_string()))?;
37
38 fs::write(path, content).map_err(|e| LockfileError::Io(e.to_string()))
39 }
40
41 pub fn is_locked(&self, name: &str, version: &str) -> bool {
43 self.packages
44 .get(name)
45 .is_some_and(|entry| entry.version == version)
46 }
47
48 pub fn get_version(&self, name: &str) -> Option<&str> {
50 self.packages.get(name).map(|e| e.version.as_str())
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct LockfileEntry {
57 pub version: String,
59
60 pub resolved: String,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub integrity: Option<String>,
66
67 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
69 pub dependencies: HashMap<String, String>,
70}
71
72#[derive(Debug, thiserror::Error)]
73pub enum LockfileError {
74 #[error("IO error: {0}")]
75 Io(String),
76
77 #[error("Parse error: {0}")]
78 Parse(String),
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn test_lockfile_new() {
87 let lockfile = Lockfile::new();
88 assert_eq!(lockfile.version, 1);
89 assert!(lockfile.packages.is_empty());
90 }
91
92 #[test]
93 fn test_lockfile_serialize() {
94 let mut lockfile = Lockfile::new();
95 lockfile.packages.insert(
96 "lodash".to_string(),
97 LockfileEntry {
98 version: "4.17.21".to_string(),
99 resolved: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz".to_string(),
100 integrity: Some("sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==".to_string()),
101 dependencies: HashMap::new(),
102 },
103 );
104
105 let json = serde_json::to_string_pretty(&lockfile).unwrap();
106 assert!(json.contains("lodash"));
107 assert!(json.contains("4.17.21"));
108 }
109
110 #[test]
111 fn test_lockfile_deserialize() {
112 let json = r#"{
113 "version": 1,
114 "packages": {
115 "lodash": {
116 "version": "4.17.21",
117 "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
118 }
119 }
120 }"#;
121
122 let lockfile: Lockfile = serde_json::from_str(json).unwrap();
123 assert_eq!(lockfile.version, 1);
124 assert!(lockfile.packages.contains_key("lodash"));
125 assert_eq!(lockfile.packages["lodash"].version, "4.17.21");
126 }
127
128 #[test]
129 fn test_is_locked() {
130 let mut lockfile = Lockfile::new();
131 lockfile.packages.insert(
132 "lodash".to_string(),
133 LockfileEntry {
134 version: "4.17.21".to_string(),
135 resolved: "https://example.com".to_string(),
136 integrity: None,
137 dependencies: HashMap::new(),
138 },
139 );
140
141 assert!(lockfile.is_locked("lodash", "4.17.21"));
142 assert!(!lockfile.is_locked("lodash", "4.17.20"));
143 assert!(!lockfile.is_locked("underscore", "1.0.0"));
144 }
145}