use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use crate::traits::PluginResult;
pub trait PluginStorage {
fn storage_path(&self) -> &Path;
fn get(&self, key: &str) -> PluginResult<Option<serde_json::Value>>;
fn set(&mut self, key: &str, value: &serde_json::Value) -> PluginResult<()>;
fn delete(&mut self, key: &str) -> PluginResult<bool>;
fn keys(&self) -> PluginResult<Vec<String>>;
fn clear(&mut self) -> PluginResult<()>;
}
#[derive(Debug, Clone)]
pub struct JsonFileStorage {
storage_dir: PathBuf,
state: HashMap<String, serde_json::Value>,
dirty: bool,
}
impl JsonFileStorage {
pub fn new(repo_root: &Path, plugin_name: &str) -> Self {
let storage_dir = repo_root
.join(".progit")
.join("plugins")
.join(plugin_name);
let mut storage = Self {
storage_dir,
state: HashMap::new(),
dirty: false,
};
let _ = storage.load();
storage
}
fn load(&mut self) -> PluginResult<()> {
let state_file = self.storage_dir.join("state.json");
if state_file.exists() {
let content = fs::read_to_string(&state_file)?;
self.state = serde_json::from_str(&content)?;
}
self.dirty = false;
Ok(())
}
pub fn save(&mut self) -> PluginResult<()> {
if !self.dirty {
return Ok(());
}
fs::create_dir_all(&self.storage_dir)?;
let state_file = self.storage_dir.join("state.json");
let content = serde_json::to_string_pretty(&self.state)?;
fs::write(&state_file, content)?;
self.dirty = false;
Ok(())
}
}
impl PluginStorage for JsonFileStorage {
fn storage_path(&self) -> &Path {
&self.storage_dir
}
fn get(&self, key: &str) -> PluginResult<Option<serde_json::Value>> {
Ok(self.state.get(key).cloned())
}
fn set(&mut self, key: &str, value: &serde_json::Value) -> PluginResult<()> {
self.state.insert(key.to_string(), value.clone());
self.dirty = true;
self.save()
}
fn delete(&mut self, key: &str) -> PluginResult<bool> {
let existed = self.state.remove(key).is_some();
if existed {
self.dirty = true;
self.save()?;
}
Ok(existed)
}
fn keys(&self) -> PluginResult<Vec<String>> {
Ok(self.state.keys().cloned().collect())
}
fn clear(&mut self) -> PluginResult<()> {
self.state.clear();
self.dirty = true;
self.save()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncState {
pub last_sync: Option<String>,
pub id_mappings: HashMap<String, String>,
pub cursor: Option<String>,
pub error_count: u32,
pub metadata: HashMap<String, serde_json::Value>,
}
impl Default for SyncState {
fn default() -> Self {
Self {
last_sync: None,
id_mappings: HashMap::new(),
cursor: None,
error_count: 0,
metadata: HashMap::new(),
}
}
}
impl SyncState {
pub fn load(storage: &dyn PluginStorage) -> PluginResult<Self> {
match storage.get("sync_state")? {
Some(value) => Ok(serde_json::from_value(value)?),
None => Ok(Self::default()),
}
}
pub fn save(&self, storage: &mut dyn PluginStorage) -> PluginResult<()> {
let value = serde_json::to_value(self)?;
storage.set("sync_state", &value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_json_storage() {
let temp_dir = env::temp_dir().join("progit-test-storage");
let _ = fs::remove_dir_all(&temp_dir);
let mut storage = JsonFileStorage::new(&temp_dir, "test-plugin");
let value = serde_json::json!({"foo": "bar"});
storage.set("key1", &value).unwrap();
let retrieved = storage.get("key1").unwrap();
assert_eq!(retrieved, Some(value));
let keys = storage.keys().unwrap();
assert_eq!(keys, vec!["key1".to_string()]);
let deleted = storage.delete("key1").unwrap();
assert!(deleted);
assert!(storage.get("key1").unwrap().is_none());
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_sync_state() {
let temp_dir = env::temp_dir().join("progit-test-sync-state");
let _ = fs::remove_dir_all(&temp_dir);
let mut storage = JsonFileStorage::new(&temp_dir, "test-plugin");
let mut state = SyncState::default();
state.last_sync = Some("2025-01-14T10:00:00Z".to_string());
state.id_mappings.insert("local-1".to_string(), "external-1".to_string());
state.save(&mut storage).unwrap();
let loaded = SyncState::load(&storage).unwrap();
assert_eq!(loaded.last_sync, Some("2025-01-14T10:00:00Z".to_string()));
assert_eq!(loaded.id_mappings.get("local-1"), Some(&"external-1".to_string()));
let _ = fs::remove_dir_all(&temp_dir);
}
}