1use super::*;
7use mockforge_plugin_core::{PluginHealth, PluginId, PluginInstance, PluginVersion};
8use std::collections::HashMap;
9use std::collections::HashSet;
10
11pub struct PluginRegistry {
13 plugins: HashMap<PluginId, PluginInstance>,
15 load_order: Vec<PluginId>,
17 stats: RegistryStats,
19}
20
21unsafe impl Send for PluginRegistry {}
23unsafe impl Sync for PluginRegistry {}
24
25impl Default for PluginRegistry {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl PluginRegistry {
32 pub fn new() -> Self {
34 Self {
35 plugins: HashMap::new(),
36 load_order: Vec::new(),
37 stats: RegistryStats::default(),
38 }
39 }
40
41 pub fn add_plugin(&mut self, plugin: PluginInstance) -> LoaderResult<()> {
43 let plugin_id = plugin.id.clone();
44
45 if self.plugins.contains_key(&plugin_id) {
47 return Err(PluginLoaderError::already_loaded(plugin_id));
48 }
49
50 self.validate_dependencies(&plugin)?;
52
53 self.plugins.insert(plugin_id.clone(), plugin);
55 self.load_order.push(plugin_id);
56
57 self.stats.total_plugins += 1;
59 self.stats.last_updated = chrono::Utc::now();
60
61 Ok(())
62 }
63
64 pub fn remove_plugin(&mut self, plugin_id: &PluginId) -> LoaderResult<PluginInstance> {
66 if !self.plugins.contains_key(plugin_id) {
68 return Err(PluginLoaderError::not_found(plugin_id.clone()));
69 }
70
71 self.check_reverse_dependencies(plugin_id)?;
73
74 self.load_order.retain(|id| id != plugin_id);
76
77 let plugin = self.plugins.remove(plugin_id).unwrap();
79
80 self.stats.total_plugins -= 1;
82 self.stats.last_updated = chrono::Utc::now();
83
84 Ok(plugin)
85 }
86
87 pub fn get_plugin(&self, plugin_id: &PluginId) -> Option<&PluginInstance> {
89 self.plugins.get(plugin_id)
90 }
91
92 pub fn get_plugin_mut(&mut self, plugin_id: &PluginId) -> Option<&mut PluginInstance> {
94 self.plugins.get_mut(plugin_id)
95 }
96
97 pub fn has_plugin(&self, plugin_id: &PluginId) -> bool {
99 self.plugins.contains_key(plugin_id)
100 }
101
102 pub fn list_plugins(&self) -> Vec<PluginId> {
104 self.plugins.keys().cloned().collect()
105 }
106
107 pub fn get_plugin_health(&self, plugin_id: &PluginId) -> LoaderResult<PluginHealth> {
109 let plugin = self
110 .get_plugin(plugin_id)
111 .ok_or_else(|| PluginLoaderError::not_found(plugin_id.clone()))?;
112
113 Ok(plugin.health.clone())
114 }
115
116 pub fn get_stats(&self) -> &RegistryStats {
118 &self.stats
119 }
120
121 pub fn is_version_compatible(&self, requirement: &str, version: &PluginVersion) -> bool {
123 if requirement.starts_with('^') {
126 let req_version = requirement.strip_prefix('^').unwrap();
128 let req_parts: Vec<&str> = req_version.split('.').collect();
129 let ver_parts: Vec<u32> = vec![version.major, version.minor, version.patch];
130
131 if !req_parts.is_empty() && req_parts[0].parse::<u32>().unwrap_or(0) == ver_parts[0] {
132 return true;
133 }
134 } else {
135 return requirement == version.to_string();
137 }
138 false
139 }
140
141 pub fn find_plugins_by_capability(&self, capability: &str) -> Vec<&PluginInstance> {
143 self.plugins
144 .values()
145 .filter(|plugin| plugin.manifest.capabilities.contains(&capability.to_string()))
146 .collect()
147 }
148
149 pub fn get_plugins_in_dependency_order(&self) -> Vec<&PluginInstance> {
151 self.load_order.iter().filter_map(|id| self.plugins.get(id)).collect()
152 }
153
154 fn validate_dependencies(&self, plugin: &PluginInstance) -> LoaderResult<()> {
156 for dep_id in plugin.manifest.dependencies.keys() {
157 if !self.has_plugin(dep_id) {
159 return Err(PluginLoaderError::dependency(format!(
160 "Required dependency {} not found",
161 dep_id.0
162 )));
163 }
164
165 if let Some(_loaded_plugin) = self.get_plugin(dep_id) {
167 }
170 }
171
172 Ok(())
173 }
174
175 fn check_reverse_dependencies(&self, plugin_id: &PluginId) -> LoaderResult<()> {
177 for (id, plugin) in &self.plugins {
178 if id == plugin_id {
179 continue; }
181
182 if plugin.manifest.dependencies.contains_key(plugin_id) {
183 return Err(PluginLoaderError::dependency(format!(
184 "Cannot remove plugin {}: required by plugin {}",
185 plugin_id.0, id.0
186 )));
187 }
188 }
189
190 Ok(())
191 }
192
193 pub fn get_dependency_graph(&self) -> HashMap<PluginId, Vec<PluginId>> {
195 let mut graph = HashMap::new();
196
197 for (plugin_id, plugin) in &self.plugins {
198 let mut deps = Vec::new();
199 for dep_id in plugin.manifest.dependencies.keys() {
200 if self.has_plugin(dep_id) {
201 deps.push(dep_id.clone());
202 }
203 }
204 graph.insert(plugin_id.clone(), deps);
205 }
206
207 graph
208 }
209
210 pub fn get_initialization_order(&self) -> LoaderResult<Vec<PluginId>> {
212 let graph = self.get_dependency_graph();
213 let mut visited = HashSet::new();
214 let mut visiting = HashSet::new();
215 let mut order = Vec::new();
216
217 fn visit(
218 plugin_id: &PluginId,
219 graph: &HashMap<PluginId, Vec<PluginId>>,
220 visited: &mut HashSet<PluginId>,
221 visiting: &mut HashSet<PluginId>,
222 order: &mut Vec<PluginId>,
223 ) -> LoaderResult<()> {
224 if visited.contains(plugin_id) {
225 return Ok(());
226 }
227
228 if visiting.contains(plugin_id) {
229 return Err(PluginLoaderError::dependency(format!(
230 "Circular dependency detected involving plugin {}",
231 plugin_id
232 )));
233 }
234
235 visiting.insert(plugin_id.clone());
236
237 if let Some(deps) = graph.get(plugin_id) {
238 for dep in deps {
239 visit(dep, graph, visited, visiting, order)?;
240 }
241 }
242
243 visiting.remove(plugin_id);
244 visited.insert(plugin_id.clone());
245 order.push(plugin_id.clone());
246
247 Ok(())
248 }
249
250 for plugin_id in self.plugins.keys() {
251 if !visited.contains(plugin_id) {
252 visit(plugin_id, &graph, &mut visited, &mut visiting, &mut order)?;
253 }
254 }
255
256 Ok(order)
257 }
258
259 pub fn clear(&mut self) {
261 self.plugins.clear();
262 self.load_order.clear();
263 self.stats = RegistryStats::default();
264 }
265
266 pub fn health_status(&self) -> RegistryHealth {
268 let mut healthy_plugins = 0;
269 let mut unhealthy_plugins = 0;
270
271 for plugin in self.plugins.values() {
272 if plugin.health.healthy {
273 healthy_plugins += 1;
274 } else {
275 unhealthy_plugins += 1;
276 }
277 }
278
279 RegistryHealth {
280 total_plugins: self.plugins.len(),
281 healthy_plugins,
282 unhealthy_plugins,
283 last_updated: self.stats.last_updated,
284 }
285 }
286}
287
288#[derive(Debug, Clone, Default)]
290pub struct RegistryStats {
291 pub total_plugins: usize,
293 pub last_updated: chrono::DateTime<chrono::Utc>,
295 pub total_loads: u64,
297 pub total_unloads: u64,
299}
300
301#[derive(Debug, Clone)]
303pub struct RegistryHealth {
304 pub total_plugins: usize,
306 pub healthy_plugins: usize,
308 pub unhealthy_plugins: usize,
310 pub last_updated: chrono::DateTime<chrono::Utc>,
312}
313
314impl RegistryHealth {
315 pub fn is_healthy(&self) -> bool {
317 self.unhealthy_plugins == 0
318 }
319
320 pub fn health_percentage(&self) -> f64 {
322 if self.total_plugins == 0 {
323 100.0
324 } else {
325 (self.healthy_plugins as f64 / self.total_plugins as f64) * 100.0
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use mockforge_plugin_core::{PluginMetrics, PluginState};
334
335 #[test]
336 fn test_registry_creation() {
337 let registry = PluginRegistry::new();
338 assert_eq!(registry.list_plugins().len(), 0);
339 assert_eq!(registry.get_stats().total_plugins, 0);
340 }
341
342 #[test]
343 fn test_registry_health() {
344 let health = RegistryHealth {
345 total_plugins: 10,
346 healthy_plugins: 8,
347 unhealthy_plugins: 2,
348 last_updated: chrono::Utc::now(),
349 };
350
351 assert!(!health.is_healthy());
352 assert_eq!(health.health_percentage(), 80.0);
353 }
354
355 #[test]
356 fn test_empty_registry_health() {
357 let health = RegistryHealth {
358 total_plugins: 0,
359 healthy_plugins: 0,
360 unhealthy_plugins: 0,
361 last_updated: chrono::Utc::now(),
362 };
363
364 assert!(health.is_healthy());
365 assert_eq!(health.health_percentage(), 100.0);
366 }
367
368 #[test]
369 fn test_version_compatibility() {
370 let registry = PluginRegistry::new();
371
372 let v1 = PluginVersion::new(1, 0, 0);
374 assert!(registry.is_version_compatible("1.0.0", &v1));
375
376 assert!(registry.is_version_compatible("^1.0.0", &v1));
378
379 assert!(!registry.is_version_compatible("2.0.0", &v1));
381 }
382
383 #[tokio::test]
384 async fn test_registry_operations() {
385 let mut registry = PluginRegistry::new();
386
387 let plugin_id = PluginId::new("test-plugin");
389 let plugin_info = PluginInfo::new(
390 plugin_id.clone(),
391 PluginVersion::new(1, 0, 0),
392 "Test Plugin",
393 "A test plugin",
394 PluginAuthor::new("Test Author"),
395 );
396 let manifest = PluginManifest::new(plugin_info);
397
398 let plugin = PluginInstance {
399 id: plugin_id.clone(),
400 manifest,
401 state: PluginState::Ready,
402 health: PluginHealth::healthy("Test plugin".to_string(), PluginMetrics::default()),
403 };
404
405 registry.add_plugin(plugin).unwrap();
407 assert_eq!(registry.list_plugins().len(), 1);
408 assert!(registry.has_plugin(&plugin_id));
409
410 assert!(registry.get_plugin(&plugin_id).is_some());
412
413 let removed = registry.remove_plugin(&plugin_id).unwrap();
415 assert_eq!(removed.id, plugin_id);
416 assert_eq!(registry.list_plugins().len(), 0);
417 assert!(!registry.has_plugin(&plugin_id));
418 }
419
420 #[tokio::test]
421 async fn test_duplicate_plugin() {
422 let mut registry = PluginRegistry::new();
423
424 let plugin_id = PluginId::new("test-plugin");
425 let plugin_info = PluginInfo::new(
426 plugin_id.clone(),
427 PluginVersion::new(1, 0, 0),
428 "Test Plugin",
429 "A test plugin",
430 PluginAuthor::new("Test Author"),
431 );
432 let manifest = PluginManifest::new(plugin_info.clone());
433
434 let plugin1 = PluginInstance {
435 id: plugin_id.clone(),
436 manifest: manifest.clone(),
437 state: PluginState::Ready,
438 health: PluginHealth::healthy("Test plugin".to_string(), PluginMetrics::default()),
439 };
440
441 let plugin2 = PluginInstance {
442 id: plugin_id.clone(),
443 manifest,
444 state: PluginState::Ready,
445 health: PluginHealth::healthy("Test plugin".to_string(), PluginMetrics::default()),
446 };
447
448 registry.add_plugin(plugin1).unwrap();
450
451 let result = registry.add_plugin(plugin2);
453 assert!(result.is_err());
454 assert!(matches!(result.unwrap_err(), PluginLoaderError::AlreadyLoaded { .. }));
455 }
456}