yosh_plugin_manager/
lockfile.rs1use std::io::Write;
2use std::path::Path;
3
4use serde::{Deserialize, Serialize};
5use tempfile::NamedTempFile;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct LockFile {
9 #[serde(default)]
10 pub plugin: Vec<LockEntry>,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub struct LockEntry {
15 pub name: String,
16 pub path: String,
17 #[serde(default = "default_true")]
18 pub enabled: bool,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub capabilities: Option<Vec<String>>,
21 pub sha256: String,
22 pub source: String,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub version: Option<String>,
25}
26
27fn default_true() -> bool {
28 true
29}
30
31pub fn load_lockfile(path: &Path) -> Result<LockFile, String> {
32 if !path.exists() {
33 return Ok(LockFile { plugin: Vec::new() });
34 }
35 let content =
36 std::fs::read_to_string(path).map_err(|e| format!("{}: {}", path.display(), e))?;
37 toml::from_str(&content).map_err(|e| format!("{}: {}", path.display(), e))
38}
39
40pub fn save_lockfile(path: &Path, lockfile: &LockFile) -> Result<(), String> {
41 let content =
42 toml::to_string_pretty(lockfile).map_err(|e| format!("serialize lock file: {}", e))?;
43 let parent = path
44 .parent()
45 .ok_or_else(|| format!("{}: no parent directory", path.display()))?;
46 std::fs::create_dir_all(parent).map_err(|e| format!("{}: {}", parent.display(), e))?;
47 let mut tmp =
48 NamedTempFile::new_in(parent).map_err(|e| format!("{}: {}", path.display(), e))?;
49 tmp.write_all(content.as_bytes())
50 .map_err(|e| format!("{}: {}", path.display(), e))?;
51 tmp.persist(path)
52 .map_err(|e| format!("{}: {}", path.display(), e))?;
53 Ok(())
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 fn sample_entry() -> LockEntry {
61 LockEntry {
62 name: "git-status".into(),
63 path: "~/.yosh/plugins/git-status/libgit_status.dylib".into(),
64 enabled: true,
65 capabilities: Some(vec!["variables:read".into(), "io".into()]),
66 sha256: "abc123".into(),
67 source: "github:user/repo".into(),
68 version: Some("1.2.3".into()),
69 }
70 }
71
72 #[test]
73 fn round_trip() {
74 let dir = tempfile::tempdir().unwrap();
75 let lock_path = dir.path().join("plugins.lock");
76 let original = LockFile {
77 plugin: vec![sample_entry()],
78 };
79 save_lockfile(&lock_path, &original).unwrap();
80 let loaded = load_lockfile(&lock_path).unwrap();
81 assert_eq!(original, loaded);
82 }
83
84 #[test]
85 fn load_nonexistent_returns_empty() {
86 let lf = load_lockfile(Path::new("/nonexistent/plugins.lock")).unwrap();
87 assert!(lf.plugin.is_empty());
88 }
89
90 #[test]
91 fn save_creates_parent_dirs() {
92 let dir = tempfile::tempdir().unwrap();
93 let lock_path = dir.path().join("sub/dir/plugins.lock");
94 let lf = LockFile {
95 plugin: vec![sample_entry()],
96 };
97 save_lockfile(&lock_path, &lf).unwrap();
98 assert!(lock_path.exists());
99 }
100
101 #[test]
102 fn local_entry_without_version() {
103 let entry = LockEntry {
104 name: "local-tool".into(),
105 path: "~/.yosh/plugins/liblocal.dylib".into(),
106 enabled: true,
107 capabilities: Some(vec!["io".into()]),
108 sha256: "def456".into(),
109 source: "local:~/.yosh/plugins/liblocal.dylib".into(),
110 version: None,
111 };
112 let dir = tempfile::tempdir().unwrap();
113 let lock_path = dir.path().join("plugins.lock");
114 let original = LockFile {
115 plugin: vec![entry],
116 };
117 save_lockfile(&lock_path, &original).unwrap();
118 let loaded = load_lockfile(&lock_path).unwrap();
119 assert_eq!(original, loaded);
120 assert!(loaded.plugin[0].version.is_none());
121 }
122
123 #[test]
124 fn partial_write_only_successful_entries() {
125 let dir = tempfile::tempdir().unwrap();
126 let lock_path = dir.path().join("plugins.lock");
127 let lf = LockFile {
128 plugin: vec![sample_entry()],
129 };
130 save_lockfile(&lock_path, &lf).unwrap();
131 let loaded = load_lockfile(&lock_path).unwrap();
132 assert_eq!(loaded.plugin.len(), 1);
133 }
134
135 #[test]
136 fn empty_lockfile() {
137 let dir = tempfile::tempdir().unwrap();
138 let lock_path = dir.path().join("plugins.lock");
139 let lf = LockFile { plugin: vec![] };
140 save_lockfile(&lock_path, &lf).unwrap();
141 let loaded = load_lockfile(&lock_path).unwrap();
142 assert!(loaded.plugin.is_empty());
143 }
144}