1use crate::{LoaderResult, PluginLoaderError, PluginSource};
7use mockforge_plugin_core::PluginId;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use tokio::fs;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct PluginMetadata {
16 pub plugin_id: PluginId,
18 pub source: PluginSource,
20 pub installed_at: u64,
22 pub updated_at: Option<u64>,
24 pub version: String,
26}
27
28impl PluginMetadata {
29 pub fn new(plugin_id: PluginId, source: PluginSource, version: String) -> Self {
31 let now = std::time::SystemTime::now()
32 .duration_since(std::time::UNIX_EPOCH)
33 .unwrap()
34 .as_secs();
35
36 Self {
37 plugin_id,
38 source,
39 installed_at: now,
40 updated_at: None,
41 version,
42 }
43 }
44
45 pub fn mark_updated(&mut self, new_version: String) {
47 let now = std::time::SystemTime::now()
48 .duration_since(std::time::UNIX_EPOCH)
49 .unwrap()
50 .as_secs();
51
52 self.updated_at = Some(now);
53 self.version = new_version;
54 }
55}
56
57pub struct MetadataStore {
59 metadata_dir: PathBuf,
61 cache: HashMap<PluginId, PluginMetadata>,
63}
64
65impl MetadataStore {
66 pub fn new(metadata_dir: PathBuf) -> Self {
68 Self {
69 metadata_dir,
70 cache: HashMap::new(),
71 }
72 }
73
74 pub async fn init(&self) -> LoaderResult<()> {
76 if !self.metadata_dir.exists() {
77 fs::create_dir_all(&self.metadata_dir).await.map_err(|e| {
78 PluginLoaderError::fs(format!("Failed to create metadata directory: {}", e))
79 })?;
80 }
81 Ok(())
82 }
83
84 pub async fn load(&mut self) -> LoaderResult<()> {
86 self.init().await?;
87
88 let mut entries = fs::read_dir(&self.metadata_dir).await.map_err(|e| {
89 PluginLoaderError::fs(format!("Failed to read metadata directory: {}", e))
90 })?;
91
92 while let Ok(Some(entry)) = entries.next_entry().await {
93 let path = entry.path();
94
95 if path.extension().and_then(|s| s.to_str()) != Some("json") {
97 continue;
98 }
99
100 match self.load_metadata_file(&path).await {
101 Ok(metadata) => {
102 self.cache.insert(metadata.plugin_id.clone(), metadata);
103 }
104 Err(e) => {
105 tracing::warn!("Failed to load metadata file {}: {}", path.display(), e);
106 }
107 }
108 }
109
110 tracing::info!("Loaded {} plugin metadata entries", self.cache.len());
111 Ok(())
112 }
113
114 async fn load_metadata_file(&self, path: &Path) -> LoaderResult<PluginMetadata> {
116 let content = fs::read_to_string(path)
117 .await
118 .map_err(|e| PluginLoaderError::fs(format!("Failed to read metadata file: {}", e)))?;
119
120 let metadata: PluginMetadata = serde_json::from_str(&content).map_err(|e| {
121 PluginLoaderError::load(format!("Failed to parse metadata JSON: {}", e))
122 })?;
123
124 Ok(metadata)
125 }
126
127 pub async fn save(&mut self, metadata: PluginMetadata) -> LoaderResult<()> {
129 self.init().await?;
130
131 let file_path = self.metadata_file_path(&metadata.plugin_id);
132 let json = serde_json::to_string_pretty(&metadata)
133 .map_err(|e| PluginLoaderError::load(format!("Failed to serialize metadata: {}", e)))?;
134
135 fs::write(&file_path, json)
136 .await
137 .map_err(|e| PluginLoaderError::fs(format!("Failed to write metadata file: {}", e)))?;
138
139 self.cache.insert(metadata.plugin_id.clone(), metadata);
141
142 Ok(())
143 }
144
145 pub fn get(&self, plugin_id: &PluginId) -> Option<&PluginMetadata> {
147 self.cache.get(plugin_id)
148 }
149
150 pub fn get_mut(&mut self, plugin_id: &PluginId) -> Option<&mut PluginMetadata> {
152 self.cache.get_mut(plugin_id)
153 }
154
155 pub async fn remove(&mut self, plugin_id: &PluginId) -> LoaderResult<()> {
157 let file_path = self.metadata_file_path(plugin_id);
158
159 if file_path.exists() {
160 fs::remove_file(&file_path).await.map_err(|e| {
161 PluginLoaderError::fs(format!("Failed to remove metadata file: {}", e))
162 })?;
163 }
164
165 self.cache.remove(plugin_id);
166
167 Ok(())
168 }
169
170 pub fn list(&self) -> Vec<PluginId> {
172 self.cache.keys().cloned().collect()
173 }
174
175 pub fn has(&self, plugin_id: &PluginId) -> bool {
177 self.cache.contains_key(plugin_id)
178 }
179
180 fn metadata_file_path(&self, plugin_id: &PluginId) -> PathBuf {
182 self.metadata_dir.join(format!("{}.json", plugin_id.as_str()))
183 }
184}
185
186impl Serialize for PluginSource {
188 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
189 where
190 S: serde::Serializer,
191 {
192 use serde::ser::SerializeStruct;
193
194 match self {
195 PluginSource::Local(path) => {
196 let mut state = serializer.serialize_struct("PluginSource", 2)?;
197 state.serialize_field("type", "local")?;
198 state.serialize_field("path", &path.display().to_string())?;
199 state.end()
200 }
201 PluginSource::Url { url, checksum } => {
202 let mut state = serializer.serialize_struct("PluginSource", 3)?;
203 state.serialize_field("type", "url")?;
204 state.serialize_field("url", url)?;
205 state.serialize_field("checksum", checksum)?;
206 state.end()
207 }
208 PluginSource::Git(git_source) => {
209 let mut state = serializer.serialize_struct("PluginSource", 2)?;
210 state.serialize_field("type", "git")?;
211 state.serialize_field("source", &git_source.to_string())?;
212 state.end()
213 }
214 PluginSource::Registry { name, version } => {
215 let mut state = serializer.serialize_struct("PluginSource", 3)?;
216 state.serialize_field("type", "registry")?;
217 state.serialize_field("name", name)?;
218 state.serialize_field("version", version)?;
219 state.end()
220 }
221 }
222 }
223}
224
225impl<'de> Deserialize<'de> for PluginSource {
227 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
228 where
229 D: serde::Deserializer<'de>,
230 {
231 use serde::de::{self, MapAccess, Visitor};
232 use std::fmt;
233
234 struct PluginSourceVisitor;
235
236 impl<'de> Visitor<'de> for PluginSourceVisitor {
237 type Value = PluginSource;
238
239 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
240 formatter.write_str("a plugin source")
241 }
242
243 fn visit_map<M>(self, mut map: M) -> Result<PluginSource, M::Error>
244 where
245 M: MapAccess<'de>,
246 {
247 let mut source_type: Option<String> = None;
248 let mut path: Option<String> = None;
249 let mut url: Option<String> = None;
250 let mut checksum: Option<Option<String>> = None;
251 let mut source: Option<String> = None;
252 let mut name: Option<String> = None;
253 let mut version: Option<Option<String>> = None;
254
255 while let Some(key) = map.next_key::<String>()? {
256 match key.as_str() {
257 "type" => source_type = Some(map.next_value()?),
258 "path" => path = Some(map.next_value()?),
259 "url" => url = Some(map.next_value()?),
260 "checksum" => checksum = Some(map.next_value()?),
261 "source" => source = Some(map.next_value()?),
262 "name" => name = Some(map.next_value()?),
263 "version" => version = Some(map.next_value()?),
264 _ => {
265 let _: serde::de::IgnoredAny = map.next_value()?;
266 }
267 }
268 }
269
270 let source_type = source_type.ok_or_else(|| de::Error::missing_field("type"))?;
271
272 match source_type.as_str() {
273 "local" => {
274 let path = path.ok_or_else(|| de::Error::missing_field("path"))?;
275 Ok(PluginSource::Local(PathBuf::from(path)))
276 }
277 "url" => {
278 let url = url.ok_or_else(|| de::Error::missing_field("url"))?;
279 let checksum =
280 checksum.ok_or_else(|| de::Error::missing_field("checksum"))?;
281 Ok(PluginSource::Url { url, checksum })
282 }
283 "git" => {
284 let source_str =
285 source.ok_or_else(|| de::Error::missing_field("source"))?;
286 let git_source = crate::git::GitPluginSource::parse(&source_str)
287 .map_err(|e| de::Error::custom(format!("Invalid git source: {}", e)))?;
288 Ok(PluginSource::Git(git_source))
289 }
290 "registry" => {
291 let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
292 let version = version.ok_or_else(|| de::Error::missing_field("version"))?;
293 Ok(PluginSource::Registry { name, version })
294 }
295 _ => Err(de::Error::custom(format!("Unknown source type: {}", source_type))),
296 }
297 }
298 }
299
300 deserializer.deserialize_struct(
301 "PluginSource",
302 &[
303 "type", "path", "url", "checksum", "source", "name", "version",
304 ],
305 PluginSourceVisitor,
306 )
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use tempfile::TempDir;
314
315 #[tokio::test]
316 async fn test_metadata_store_creation() {
317 let temp_dir = TempDir::new().unwrap();
318 let store = MetadataStore::new(temp_dir.path().to_path_buf());
319 store.init().await.unwrap();
320
321 assert!(temp_dir.path().exists());
322 }
323
324 #[tokio::test]
325 async fn test_save_and_load_metadata() {
326 let temp_dir = TempDir::new().unwrap();
327 let mut store = MetadataStore::new(temp_dir.path().to_path_buf());
328
329 let plugin_id = PluginId::new("test-plugin");
330 let source = PluginSource::Url {
331 url: "https://example.com/plugin.zip".to_string(),
332 checksum: None,
333 };
334 let metadata = PluginMetadata::new(plugin_id.clone(), source, "1.0.0".to_string());
335
336 store.save(metadata.clone()).await.unwrap();
337 assert!(store.has(&plugin_id));
338
339 let mut new_store = MetadataStore::new(temp_dir.path().to_path_buf());
341 new_store.load().await.unwrap();
342
343 let loaded = new_store.get(&plugin_id).unwrap();
344 assert_eq!(loaded.plugin_id, plugin_id);
345 assert_eq!(loaded.version, "1.0.0");
346 }
347
348 #[tokio::test]
349 async fn test_remove_metadata() {
350 let temp_dir = TempDir::new().unwrap();
351 let mut store = MetadataStore::new(temp_dir.path().to_path_buf());
352
353 let plugin_id = PluginId::new("test-plugin");
354 let source = PluginSource::Local(PathBuf::from("/tmp/test"));
355 let metadata = PluginMetadata::new(plugin_id.clone(), source, "1.0.0".to_string());
356
357 store.save(metadata).await.unwrap();
358 assert!(store.has(&plugin_id));
359
360 store.remove(&plugin_id).await.unwrap();
361 assert!(!store.has(&plugin_id));
362 }
363
364 #[tokio::test]
365 async fn test_mark_updated() {
366 let plugin_id = PluginId::new("test-plugin");
367 let source = PluginSource::Local(PathBuf::from("/tmp/test"));
368 let mut metadata = PluginMetadata::new(plugin_id, source, "1.0.0".to_string());
369
370 assert!(metadata.updated_at.is_none());
371
372 metadata.mark_updated("1.1.0".to_string());
373
374 assert!(metadata.updated_at.is_some());
375 assert_eq!(metadata.version, "1.1.0");
376 }
377}