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