mockforge_plugin_registry/
storage.rs1use crate::{RegistryEntry, Result, VersionEntry};
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use tokio::fs;
7
8pub struct RegistryStorage {
10 base_dir: PathBuf,
12
13 index: HashMap<String, RegistryEntry>,
15}
16
17impl RegistryStorage {
18 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 pub fn get(&self, name: &str) -> Option<&RegistryEntry> {
34 self.index.get(name)
35 }
36
37 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 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 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 pub fn search(&self, query: Option<&str>, tags: &[String]) -> Vec<&RegistryEntry> {
64 self.index
65 .values()
66 .filter(|entry| {
67 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 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 pub fn list(&self) -> Vec<&RegistryEntry> {
89 self.index.values().collect()
90 }
91
92 fn index_path(&self) -> PathBuf {
94 self.base_dir.join("index.json")
95 }
96
97 fn entry_path(&self, name: &str) -> PathBuf {
99 self.base_dir.join(format!("{}.json", name))
100 }
101
102 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 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 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 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 storage.put(entry.clone()).await.unwrap();
181
182 let retrieved = storage.get("test-plugin").unwrap();
184 assert_eq!(retrieved.name, "test-plugin");
185
186 let results = storage.search(Some("test"), &[]);
188 assert_eq!(results.len(), 1);
189
190 storage.remove("test-plugin").await.unwrap();
192 assert!(storage.get("test-plugin").is_none());
193 }
194}