mockforge_plugin_registry/
storage.rs

1//! Plugin storage backend
2
3use crate::{RegistryEntry, Result, VersionEntry};
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use tokio::fs;
7
8/// Storage backend for plugin registry
9pub struct RegistryStorage {
10    /// Base directory for storage
11    base_dir: PathBuf,
12
13    /// In-memory index cache
14    index: HashMap<String, RegistryEntry>,
15}
16
17impl RegistryStorage {
18    /// Create a new storage backend
19    pub async fn new<P: AsRef<Path>>(base_dir: P) -> Result<Self> {
20        let base_dir = base_dir.as_ref().to_path_buf();
21        fs::create_dir_all(&base_dir).await?;
22
23        let mut storage = Self {
24            base_dir,
25            index: HashMap::new(),
26        };
27
28        storage.load_index().await?;
29        Ok(storage)
30    }
31
32    /// Get plugin entry by name
33    pub fn get(&self, name: &str) -> Option<&RegistryEntry> {
34        self.index.get(name)
35    }
36
37    /// Get plugin entry with specific version
38    pub fn get_version(&self, name: &str, version: &str) -> Option<&VersionEntry> {
39        self.index.get(name)?.versions.iter().find(|v| v.version == version)
40    }
41
42    /// Add or update plugin entry
43    pub async fn put(&mut self, entry: RegistryEntry) -> Result<()> {
44        let name = entry.name.clone();
45        self.index.insert(name.clone(), entry.clone());
46        self.save_entry(&entry).await?;
47        self.save_index().await?;
48        Ok(())
49    }
50
51    /// Remove plugin entry
52    pub async fn remove(&mut self, name: &str) -> Result<()> {
53        self.index.remove(name);
54        let path = self.entry_path(name);
55        if path.exists() {
56            fs::remove_file(path).await?;
57        }
58        self.save_index().await?;
59        Ok(())
60    }
61
62    /// Search plugins
63    pub fn search(&self, query: Option<&str>, tags: &[String]) -> Vec<&RegistryEntry> {
64        self.index
65            .values()
66            .filter(|entry| {
67                // Filter by query
68                if let Some(q) = query {
69                    let q = q.to_lowercase();
70                    if !entry.name.to_lowercase().contains(&q)
71                        && !entry.description.to_lowercase().contains(&q)
72                    {
73                        return false;
74                    }
75                }
76
77                // Filter by tags
78                if !tags.is_empty() && !tags.iter().any(|tag| entry.tags.contains(tag)) {
79                    return false;
80                }
81
82                true
83            })
84            .collect()
85    }
86
87    /// List all plugins
88    pub fn list(&self) -> Vec<&RegistryEntry> {
89        self.index.values().collect()
90    }
91
92    /// Get index file path
93    fn index_path(&self) -> PathBuf {
94        self.base_dir.join("index.json")
95    }
96
97    /// Get entry file path
98    fn entry_path(&self, name: &str) -> PathBuf {
99        self.base_dir.join(format!("{}.json", name))
100    }
101
102    /// Load index from disk
103    async fn load_index(&mut self) -> Result<()> {
104        let path = self.index_path();
105        if !path.exists() {
106            return Ok(());
107        }
108
109        let contents = fs::read_to_string(path).await?;
110        let names: Vec<String> = serde_json::from_str(&contents)?;
111
112        for name in names {
113            if let Ok(entry) = self.load_entry(&name).await {
114                self.index.insert(name, entry);
115            }
116        }
117
118        Ok(())
119    }
120
121    /// Save index to disk
122    async fn save_index(&self) -> Result<()> {
123        let names: Vec<String> = self.index.keys().cloned().collect();
124        let contents = serde_json::to_string_pretty(&names)?;
125        fs::write(self.index_path(), contents).await?;
126        Ok(())
127    }
128
129    /// Load entry from disk
130    async fn load_entry(&self, name: &str) -> Result<RegistryEntry> {
131        let path = self.entry_path(name);
132        let contents = fs::read_to_string(path).await?;
133        let entry = serde_json::from_str(&contents)?;
134        Ok(entry)
135    }
136
137    /// Save entry to disk
138    async fn save_entry(&self, entry: &RegistryEntry) -> Result<()> {
139        let path = self.entry_path(&entry.name);
140        let contents = serde_json::to_string_pretty(entry)?;
141        fs::write(path, contents).await?;
142        Ok(())
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::{AuthorInfo, PluginCategory};
150    use tempfile::tempdir;
151
152    #[tokio::test]
153    async fn test_storage_crud() {
154        let dir = tempdir().unwrap();
155        let mut storage = RegistryStorage::new(dir.path()).await.unwrap();
156
157        let entry = RegistryEntry {
158            name: "test-plugin".to_string(),
159            description: "Test".to_string(),
160            version: "1.0.0".to_string(),
161            versions: vec![],
162            author: AuthorInfo {
163                name: "Test".to_string(),
164                email: None,
165                url: None,
166            },
167            tags: vec!["test".to_string()],
168            category: PluginCategory::Auth,
169            downloads: 0,
170            rating: 0.0,
171            reviews_count: 0,
172            repository: None,
173            homepage: None,
174            license: "MIT".to_string(),
175            created_at: "2025-01-01T00:00:00Z".to_string(),
176            updated_at: "2025-01-01T00:00:00Z".to_string(),
177        };
178
179        // Create
180        storage.put(entry.clone()).await.unwrap();
181
182        // Read
183        let retrieved = storage.get("test-plugin").unwrap();
184        assert_eq!(retrieved.name, "test-plugin");
185
186        // Search
187        let results = storage.search(Some("test"), &[]);
188        assert_eq!(results.len(), 1);
189
190        // Delete
191        storage.remove("test-plugin").await.unwrap();
192        assert!(storage.get("test-plugin").is_none());
193    }
194}