progit_plugin_sdk/
storage.rs1use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fs;
12use std::path::{Path, PathBuf};
13
14use crate::traits::PluginResult;
15
16pub trait PluginStorage {
20 fn storage_path(&self) -> &Path;
22
23 fn get(&self, key: &str) -> PluginResult<Option<serde_json::Value>>;
25
26 fn set(&mut self, key: &str, value: &serde_json::Value) -> PluginResult<()>;
28
29 fn delete(&mut self, key: &str) -> PluginResult<bool>;
31
32 fn keys(&self) -> PluginResult<Vec<String>>;
34
35 fn clear(&mut self) -> PluginResult<()>;
37}
38
39#[derive(Debug, Clone)]
41pub struct JsonFileStorage {
42 storage_dir: PathBuf,
44 state: HashMap<String, serde_json::Value>,
46 dirty: bool,
48}
49
50impl JsonFileStorage {
51 pub fn new(repo_root: &Path, plugin_name: &str) -> Self {
57 let storage_dir = repo_root
58 .join(".progit")
59 .join("plugins")
60 .join(plugin_name);
61
62 let mut storage = Self {
63 storage_dir,
64 state: HashMap::new(),
65 dirty: false,
66 };
67
68 let _ = storage.load();
70
71 storage
72 }
73
74 fn load(&mut self) -> PluginResult<()> {
76 let state_file = self.storage_dir.join("state.json");
77
78 if state_file.exists() {
79 let content = fs::read_to_string(&state_file)?;
80 self.state = serde_json::from_str(&content)?;
81 }
82
83 self.dirty = false;
84 Ok(())
85 }
86
87 pub fn save(&mut self) -> PluginResult<()> {
89 if !self.dirty {
90 return Ok(());
91 }
92
93 fs::create_dir_all(&self.storage_dir)?;
95
96 let state_file = self.storage_dir.join("state.json");
97 let content = serde_json::to_string_pretty(&self.state)?;
98 fs::write(&state_file, content)?;
99
100 self.dirty = false;
101 Ok(())
102 }
103}
104
105impl PluginStorage for JsonFileStorage {
106 fn storage_path(&self) -> &Path {
107 &self.storage_dir
108 }
109
110 fn get(&self, key: &str) -> PluginResult<Option<serde_json::Value>> {
111 Ok(self.state.get(key).cloned())
112 }
113
114 fn set(&mut self, key: &str, value: &serde_json::Value) -> PluginResult<()> {
115 self.state.insert(key.to_string(), value.clone());
116 self.dirty = true;
117 self.save()
119 }
120
121 fn delete(&mut self, key: &str) -> PluginResult<bool> {
122 let existed = self.state.remove(key).is_some();
123 if existed {
124 self.dirty = true;
125 self.save()?;
126 }
127 Ok(existed)
128 }
129
130 fn keys(&self) -> PluginResult<Vec<String>> {
131 Ok(self.state.keys().cloned().collect())
132 }
133
134 fn clear(&mut self) -> PluginResult<()> {
135 self.state.clear();
136 self.dirty = true;
137 self.save()
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct SyncState {
144 pub last_sync: Option<String>,
146 pub id_mappings: HashMap<String, String>,
148 pub cursor: Option<String>,
150 pub error_count: u32,
152 pub metadata: HashMap<String, serde_json::Value>,
154}
155
156impl Default for SyncState {
157 fn default() -> Self {
158 Self {
159 last_sync: None,
160 id_mappings: HashMap::new(),
161 cursor: None,
162 error_count: 0,
163 metadata: HashMap::new(),
164 }
165 }
166}
167
168impl SyncState {
169 pub fn load(storage: &dyn PluginStorage) -> PluginResult<Self> {
171 match storage.get("sync_state")? {
172 Some(value) => Ok(serde_json::from_value(value)?),
173 None => Ok(Self::default()),
174 }
175 }
176
177 pub fn save(&self, storage: &mut dyn PluginStorage) -> PluginResult<()> {
179 let value = serde_json::to_value(self)?;
180 storage.set("sync_state", &value)
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use std::env;
188
189 #[test]
190 fn test_json_storage() {
191 let temp_dir = env::temp_dir().join("progit-test-storage");
192 let _ = fs::remove_dir_all(&temp_dir);
193
194 let mut storage = JsonFileStorage::new(&temp_dir, "test-plugin");
195
196 let value = serde_json::json!({"foo": "bar"});
198 storage.set("key1", &value).unwrap();
199
200 let retrieved = storage.get("key1").unwrap();
201 assert_eq!(retrieved, Some(value));
202
203 let keys = storage.keys().unwrap();
205 assert_eq!(keys, vec!["key1".to_string()]);
206
207 let deleted = storage.delete("key1").unwrap();
209 assert!(deleted);
210 assert!(storage.get("key1").unwrap().is_none());
211
212 let _ = fs::remove_dir_all(&temp_dir);
214 }
215
216 #[test]
217 fn test_sync_state() {
218 let temp_dir = env::temp_dir().join("progit-test-sync-state");
219 let _ = fs::remove_dir_all(&temp_dir);
220
221 let mut storage = JsonFileStorage::new(&temp_dir, "test-plugin");
222
223 let mut state = SyncState::default();
225 state.last_sync = Some("2025-01-14T10:00:00Z".to_string());
226 state.id_mappings.insert("local-1".to_string(), "external-1".to_string());
227 state.save(&mut storage).unwrap();
228
229 let loaded = SyncState::load(&storage).unwrap();
231 assert_eq!(loaded.last_sync, Some("2025-01-14T10:00:00Z".to_string()));
232 assert_eq!(loaded.id_mappings.get("local-1"), Some(&"external-1".to_string()));
233
234 let _ = fs::remove_dir_all(&temp_dir);
236 }
237}