fusabi_plugin_runtime/
registry.rs1use std::collections::HashMap;
4use std::sync::Arc;
5
6use dashmap::DashMap;
7
8use crate::error::{Error, Result};
9use crate::lifecycle::{LifecycleHooks, LifecycleState};
10use crate::plugin::{Plugin, PluginHandle, PluginInfo};
11
12#[derive(Debug, Clone)]
14pub struct RegistryConfig {
15 pub max_plugins: usize,
17 pub allow_overwrite: bool,
19 pub auto_unload_stopped: bool,
21}
22
23impl Default for RegistryConfig {
24 fn default() -> Self {
25 Self {
26 max_plugins: 100,
27 allow_overwrite: false,
28 auto_unload_stopped: false,
29 }
30 }
31}
32
33impl RegistryConfig {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn with_max_plugins(mut self, max: usize) -> Self {
41 self.max_plugins = max;
42 self
43 }
44
45 pub fn with_allow_overwrite(mut self, allow: bool) -> Self {
47 self.allow_overwrite = allow;
48 self
49 }
50
51 pub fn with_auto_unload_stopped(mut self, auto: bool) -> Self {
53 self.auto_unload_stopped = auto;
54 self
55 }
56}
57
58#[derive(Debug, Clone, Default)]
60pub struct RegistryStats {
61 pub total: usize,
63 pub running: usize,
65 pub stopped: usize,
67 pub error: usize,
69 pub unloaded: usize,
71}
72
73pub struct PluginRegistry {
75 config: RegistryConfig,
76 plugins: DashMap<String, PluginHandle>,
77 hooks: Arc<LifecycleHooks>,
78}
79
80impl PluginRegistry {
81 pub fn new(config: RegistryConfig) -> Self {
83 Self {
84 config,
85 plugins: DashMap::new(),
86 hooks: Arc::new(LifecycleHooks::new()),
87 }
88 }
89
90 pub fn default_config() -> Self {
92 Self::new(RegistryConfig::default())
93 }
94
95 pub fn config(&self) -> &RegistryConfig {
97 &self.config
98 }
99
100 pub fn register(&self, plugin: PluginHandle) -> Result<()> {
102 let name = plugin.name();
103
104 if self.plugins.len() >= self.config.max_plugins {
106 return Err(Error::Registry(format!(
107 "registry full: max {} plugins",
108 self.config.max_plugins
109 )));
110 }
111
112 if self.plugins.contains_key(&name) {
114 if !self.config.allow_overwrite {
115 return Err(Error::PluginAlreadyLoaded(name));
116 }
117
118 if let Some((_, existing)) = self.plugins.remove(&name) {
120 let _ = existing.inner().unload();
121 }
122 }
123
124 self.plugins.insert(name.clone(), plugin);
125 self.hooks.emit_created(&name);
126
127 Ok(())
128 }
129
130 pub fn unregister(&self, name: &str) -> Result<PluginHandle> {
132 let (_, plugin) = self
133 .plugins
134 .remove(name)
135 .ok_or_else(|| Error::plugin_not_found(name))?;
136
137 let _ = plugin.inner().unload();
139 self.hooks.emit_unloaded(name);
140
141 Ok(plugin)
142 }
143
144 pub fn get(&self, name: &str) -> Option<PluginHandle> {
146 self.plugins.get(name).map(|r| r.clone())
147 }
148
149 pub fn contains(&self, name: &str) -> bool {
151 self.plugins.contains_key(name)
152 }
153
154 pub fn names(&self) -> Vec<String> {
156 self.plugins.iter().map(|r| r.key().clone()).collect()
157 }
158
159 pub fn all(&self) -> Vec<PluginHandle> {
161 self.plugins.iter().map(|r| r.value().clone()).collect()
162 }
163
164 pub fn by_state(&self, state: LifecycleState) -> Vec<PluginHandle> {
166 self.plugins
167 .iter()
168 .filter(|r| r.state() == state)
169 .map(|r| r.value().clone())
170 .collect()
171 }
172
173 pub fn running(&self) -> Vec<PluginHandle> {
175 self.by_state(LifecycleState::Running)
176 }
177
178 pub fn len(&self) -> usize {
180 self.plugins.len()
181 }
182
183 pub fn is_empty(&self) -> bool {
185 self.plugins.is_empty()
186 }
187
188 pub fn stats(&self) -> RegistryStats {
190 let mut stats = RegistryStats::default();
191 stats.total = self.plugins.len();
192
193 for entry in self.plugins.iter() {
194 match entry.state() {
195 LifecycleState::Running => stats.running += 1,
196 LifecycleState::Stopped => stats.stopped += 1,
197 LifecycleState::Error => stats.error += 1,
198 LifecycleState::Unloaded => stats.unloaded += 1,
199 _ => {}
200 }
201 }
202
203 stats
204 }
205
206 pub fn info(&self) -> Vec<PluginInfo> {
208 self.plugins.iter().map(|r| r.info()).collect()
209 }
210
211 pub fn start_all(&self) -> Vec<Result<()>> {
213 self.plugins
214 .iter()
215 .filter(|r| r.state() == LifecycleState::Initialized)
216 .map(|r| {
217 let plugin = r.value();
218 plugin.inner().start()
219 })
220 .collect()
221 }
222
223 pub fn stop_all(&self) -> Vec<Result<()>> {
225 self.plugins
226 .iter()
227 .filter(|r| r.state() == LifecycleState::Running)
228 .map(|r| {
229 let plugin = r.value();
230 plugin.inner().stop()
231 })
232 .collect()
233 }
234
235 pub fn unload_all(&self) {
237 for entry in self.plugins.iter() {
238 let _ = entry.value().inner().unload();
239 }
240 self.plugins.clear();
241 }
242
243 pub fn reload(&self, name: &str) -> Result<()> {
245 let plugin = self
246 .get(name)
247 .ok_or_else(|| Error::plugin_not_found(name))?;
248
249 plugin.inner().reload()?;
250
251 let info = plugin.info();
252 self.hooks.emit_reloaded(name, info.reload_count);
253
254 Ok(())
255 }
256
257 pub fn reload_all(&self) -> Vec<Result<()>> {
259 self.plugins
260 .iter()
261 .map(|r| {
262 let name = r.key().clone();
263 self.reload(&name)
264 })
265 .collect()
266 }
267
268 pub fn find_by_tag(&self, tag: &str) -> Vec<PluginHandle> {
270 self.plugins
271 .iter()
272 .filter(|r| {
273 r.value()
274 .inner()
275 .manifest()
276 .tags
277 .contains(&tag.to_string())
278 })
279 .map(|r| r.value().clone())
280 .collect()
281 }
282
283 pub fn find_by_capability(&self, cap: &str) -> Vec<PluginHandle> {
285 self.plugins
286 .iter()
287 .filter(|r| r.value().inner().requires_capability(cap))
288 .map(|r| r.value().clone())
289 .collect()
290 }
291
292 pub fn cleanup(&self) -> usize {
294 let to_remove: Vec<String> = self
295 .plugins
296 .iter()
297 .filter(|r| {
298 let state = r.state();
299 state == LifecycleState::Unloaded
300 || (self.config.auto_unload_stopped && state == LifecycleState::Stopped)
301 })
302 .map(|r| r.key().clone())
303 .collect();
304
305 let count = to_remove.len();
306 for name in to_remove {
307 self.plugins.remove(&name);
308 }
309
310 count
311 }
312}
313
314impl std::fmt::Debug for PluginRegistry {
315 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316 f.debug_struct("PluginRegistry")
317 .field("config", &self.config)
318 .field("plugin_count", &self.plugins.len())
319 .finish()
320 }
321}
322
323impl Drop for PluginRegistry {
324 fn drop(&mut self) {
325 for entry in self.plugins.iter() {
327 let _ = entry.value().inner().unload();
328 }
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::manifest::ManifestBuilder;
336
337 fn create_test_plugin(name: &str) -> PluginHandle {
338 let manifest = ManifestBuilder::new(name, "1.0.0")
339 .source("test.fsx")
340 .build_unchecked();
341 PluginHandle::new(Plugin::new(manifest))
342 }
343
344 #[test]
345 fn test_registry_creation() {
346 let registry = PluginRegistry::default_config();
347 assert!(registry.is_empty());
348 assert_eq!(registry.len(), 0);
349 }
350
351 #[test]
352 fn test_register_plugin() {
353 let registry = PluginRegistry::default_config();
354 let plugin = create_test_plugin("test-plugin");
355
356 registry.register(plugin).unwrap();
357
358 assert!(registry.contains("test-plugin"));
359 assert_eq!(registry.len(), 1);
360 }
361
362 #[test]
363 fn test_register_duplicate() {
364 let registry = PluginRegistry::default_config();
365
366 let plugin1 = create_test_plugin("test-plugin");
367 let plugin2 = create_test_plugin("test-plugin");
368
369 registry.register(plugin1).unwrap();
370 let result = registry.register(plugin2);
371
372 assert!(matches!(result, Err(Error::PluginAlreadyLoaded(_))));
373 }
374
375 #[test]
376 fn test_register_duplicate_with_overwrite() {
377 let config = RegistryConfig::new().with_allow_overwrite(true);
378 let registry = PluginRegistry::new(config);
379
380 let plugin1 = create_test_plugin("test-plugin");
381 let id1 = plugin1.id();
382
383 let plugin2 = create_test_plugin("test-plugin");
384 let id2 = plugin2.id();
385
386 registry.register(plugin1).unwrap();
387 registry.register(plugin2).unwrap();
388
389 let plugin = registry.get("test-plugin").unwrap();
390 assert_eq!(plugin.id(), id2);
391 assert_ne!(plugin.id(), id1);
392 }
393
394 #[test]
395 fn test_unregister_plugin() {
396 let registry = PluginRegistry::default_config();
397 let plugin = create_test_plugin("test-plugin");
398
399 registry.register(plugin).unwrap();
400 assert!(registry.contains("test-plugin"));
401
402 registry.unregister("test-plugin").unwrap();
403 assert!(!registry.contains("test-plugin"));
404 }
405
406 #[test]
407 fn test_unregister_nonexistent() {
408 let registry = PluginRegistry::default_config();
409 let result = registry.unregister("nonexistent");
410 assert!(matches!(result, Err(Error::PluginNotFound(_))));
411 }
412
413 #[test]
414 fn test_get_all_plugins() {
415 let registry = PluginRegistry::default_config();
416
417 registry.register(create_test_plugin("plugin-1")).unwrap();
418 registry.register(create_test_plugin("plugin-2")).unwrap();
419 registry.register(create_test_plugin("plugin-3")).unwrap();
420
421 let all = registry.all();
422 assert_eq!(all.len(), 3);
423
424 let names = registry.names();
425 assert!(names.contains(&"plugin-1".to_string()));
426 assert!(names.contains(&"plugin-2".to_string()));
427 assert!(names.contains(&"plugin-3".to_string()));
428 }
429
430 #[test]
431 fn test_registry_stats() {
432 let registry = PluginRegistry::default_config();
433
434 registry.register(create_test_plugin("plugin-1")).unwrap();
435 registry.register(create_test_plugin("plugin-2")).unwrap();
436
437 let stats = registry.stats();
438 assert_eq!(stats.total, 2);
439 }
440
441 #[test]
442 fn test_max_plugins() {
443 let config = RegistryConfig::new().with_max_plugins(2);
444 let registry = PluginRegistry::new(config);
445
446 registry.register(create_test_plugin("plugin-1")).unwrap();
447 registry.register(create_test_plugin("plugin-2")).unwrap();
448
449 let result = registry.register(create_test_plugin("plugin-3"));
450 assert!(matches!(result, Err(Error::Registry(_))));
451 }
452}