zoey_core/
plugin.rs

1//! Plugin loading and management utilities
2
3use crate::types::Plugin;
4use crate::{ZoeyError, Result};
5use std::collections::{HashMap, HashSet};
6use std::sync::Arc;
7
8/// Validate a plugin's structure
9///
10/// # Arguments
11/// * `plugin` - The plugin to validate
12///
13/// # Returns
14/// A result with validation errors if any
15pub fn validate_plugin(plugin: &Arc<dyn Plugin>) -> Result<()> {
16    let mut errors = Vec::new();
17
18    // Check name
19    if plugin.name().is_empty() {
20        errors.push("Plugin must have a name".to_string());
21    }
22
23    // Check description
24    if plugin.description().is_empty() {
25        errors.push("Plugin should have a description".to_string());
26    }
27
28    if !errors.is_empty() {
29        return Err(ZoeyError::Validation(format!(
30            "Plugin validation failed: {}",
31            errors.join(", ")
32        )));
33    }
34
35    Ok(())
36}
37
38/// Resolve plugin dependencies with circular dependency detection
39/// Performs topological sorting of plugins to ensure dependencies are loaded in the correct order
40///
41/// # Arguments
42/// * `plugins` - Map of plugin names to plugin instances
43/// * `is_test_mode` - Whether to include test dependencies
44///
45/// # Returns
46/// A vector of plugins in dependency order
47pub fn resolve_plugin_dependencies(
48    plugins: HashMap<String, Arc<dyn Plugin>>,
49    is_test_mode: bool,
50) -> Result<Vec<Arc<dyn Plugin>>> {
51    let mut resolution_order: Vec<String> = Vec::new();
52    let mut visited: HashSet<String> = HashSet::new();
53    let mut visiting: HashSet<String> = HashSet::new();
54
55    fn visit(
56        plugin_name: &str,
57        plugins: &HashMap<String, Arc<dyn Plugin>>,
58        is_test_mode: bool,
59        visited: &mut HashSet<String>,
60        visiting: &mut HashSet<String>,
61        resolution_order: &mut Vec<String>,
62    ) -> Result<()> {
63        if !plugins.contains_key(plugin_name) {
64            return Err(ZoeyError::NotFound(format!(
65                "Plugin dependency '{}' not found",
66                plugin_name
67            )));
68        }
69
70        if visited.contains(plugin_name) {
71            return Ok(());
72        }
73
74        if visiting.contains(plugin_name) {
75            return Err(ZoeyError::Validation(format!(
76                "Circular dependency detected involving plugin: {}",
77                plugin_name
78            )));
79        }
80
81        visiting.insert(plugin_name.to_string());
82
83        if let Some(plugin) = plugins.get(plugin_name) {
84            // Visit regular dependencies
85            for dep in plugin.dependencies() {
86                visit(
87                    &dep,
88                    plugins,
89                    is_test_mode,
90                    visited,
91                    visiting,
92                    resolution_order,
93                )?;
94            }
95
96            // Visit test dependencies if in test mode
97            if is_test_mode {
98                for dep in plugin.test_dependencies() {
99                    visit(
100                        &dep,
101                        plugins,
102                        is_test_mode,
103                        visited,
104                        visiting,
105                        resolution_order,
106                    )?;
107                }
108            }
109        }
110
111        visiting.remove(plugin_name);
112        visited.insert(plugin_name.to_string());
113        resolution_order.push(plugin_name.to_string());
114
115        Ok(())
116    }
117
118    // Visit all plugins
119    for name in plugins.keys() {
120        if !visited.contains(name) {
121            visit(
122                name,
123                &plugins,
124                is_test_mode,
125                &mut visited,
126                &mut visiting,
127                &mut resolution_order,
128            )?;
129        }
130    }
131
132    // Return plugins in resolution order
133    let final_plugins: Vec<Arc<dyn Plugin>> = resolution_order
134        .iter()
135        .filter_map(|name| plugins.get(name).cloned())
136        .collect();
137
138    Ok(final_plugins)
139}
140
141/// Load and initialize plugins
142///
143/// # Arguments
144/// * `plugins` - List of plugins to load
145/// * `is_test_mode` - Whether to include test dependencies
146///
147/// # Returns
148/// A vector of plugins in dependency order
149pub async fn load_plugins(
150    plugins: Vec<Arc<dyn Plugin>>,
151    is_test_mode: bool,
152) -> Result<Vec<Arc<dyn Plugin>>> {
153    // Validate all plugins
154    for plugin in &plugins {
155        validate_plugin(plugin)?;
156    }
157
158    // Create plugin map
159    let mut plugin_map: HashMap<String, Arc<dyn Plugin>> = HashMap::new();
160    for plugin in plugins {
161        plugin_map.insert(plugin.name().to_string(), plugin);
162    }
163
164    // Resolve dependencies
165    resolve_plugin_dependencies(plugin_map, is_test_mode)
166}
167
168/// Initialize plugins with runtime
169///
170/// # Arguments
171/// * `plugins` - List of plugins to initialize
172/// * `config` - Configuration map
173/// * `runtime` - Runtime instance (type-erased)
174///
175/// # Returns
176/// Result indicating success or failure
177pub async fn initialize_plugins(
178    plugins: &[Arc<dyn Plugin>],
179    config: HashMap<String, String>,
180    runtime: Arc<dyn std::any::Any + Send + Sync>,
181) -> Result<()> {
182    for plugin in plugins {
183        plugin.init(config.clone(), runtime.clone()).await?;
184    }
185
186    Ok(())
187}
188
189/// Get all actions from plugins
190///
191/// # Arguments
192/// * `plugins` - List of plugins
193///
194/// # Returns
195/// A vector of all actions from all plugins
196pub fn get_plugin_actions(plugins: &[Arc<dyn Plugin>]) -> Vec<Arc<dyn crate::types::Action>> {
197    plugins.iter().flat_map(|plugin| plugin.actions()).collect()
198}
199
200/// Get all providers from plugins
201///
202/// # Arguments
203/// * `plugins` - List of plugins
204///
205/// # Returns
206/// A vector of all providers from all plugins
207pub fn get_plugin_providers(plugins: &[Arc<dyn Plugin>]) -> Vec<Arc<dyn crate::types::Provider>> {
208    plugins
209        .iter()
210        .flat_map(|plugin| plugin.providers())
211        .collect()
212}
213
214/// Get all evaluators from plugins
215///
216/// # Arguments
217/// * `plugins` - List of plugins
218///
219/// # Returns
220/// A vector of all evaluators from all plugins
221pub fn get_plugin_evaluators(plugins: &[Arc<dyn Plugin>]) -> Vec<Arc<dyn crate::types::Evaluator>> {
222    plugins
223        .iter()
224        .flat_map(|plugin| plugin.evaluators())
225        .collect()
226}
227
228/// Get all services from plugins
229///
230/// # Arguments
231/// * `plugins` - List of plugins
232///
233/// # Returns
234/// A vector of all services from all plugins
235pub fn get_plugin_services(plugins: &[Arc<dyn Plugin>]) -> Vec<Arc<dyn crate::types::Service>> {
236    plugins
237        .iter()
238        .flat_map(|plugin| plugin.services())
239        .collect()
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use async_trait::async_trait;
246
247    struct MockPlugin {
248        name: String,
249        dependencies: Vec<String>,
250    }
251
252    #[async_trait]
253    impl Plugin for MockPlugin {
254        fn name(&self) -> &str {
255            &self.name
256        }
257
258        fn description(&self) -> &str {
259            "Mock plugin"
260        }
261
262        fn dependencies(&self) -> Vec<String> {
263            self.dependencies.clone()
264        }
265    }
266
267    #[tokio::test]
268    async fn test_validate_plugin() {
269        let plugin: Arc<dyn Plugin> = Arc::new(MockPlugin {
270            name: "test-plugin".to_string(),
271            dependencies: vec![],
272        });
273
274        assert!(validate_plugin(&plugin).is_ok());
275    }
276
277    #[tokio::test]
278    async fn test_empty_name_validation() {
279        let plugin: Arc<dyn Plugin> = Arc::new(MockPlugin {
280            name: "".to_string(),
281            dependencies: vec![],
282        });
283
284        assert!(validate_plugin(&plugin).is_err());
285    }
286
287    #[test]
288    fn test_resolve_dependencies() {
289        let plugin_a: Arc<dyn Plugin> = Arc::new(MockPlugin {
290            name: "plugin-a".to_string(),
291            dependencies: vec![],
292        });
293
294        let plugin_b: Arc<dyn Plugin> = Arc::new(MockPlugin {
295            name: "plugin-b".to_string(),
296            dependencies: vec!["plugin-a".to_string()],
297        });
298
299        let mut plugins = HashMap::new();
300        plugins.insert("plugin-a".to_string(), plugin_a);
301        plugins.insert("plugin-b".to_string(), plugin_b);
302
303        let result = resolve_plugin_dependencies(plugins, false);
304        assert!(result.is_ok());
305
306        let resolved = result.unwrap();
307        assert_eq!(resolved.len(), 2);
308        assert_eq!(resolved[0].name(), "plugin-a");
309        assert_eq!(resolved[1].name(), "plugin-b");
310    }
311
312    #[test]
313    fn test_circular_dependency_detection() {
314        let plugin_a: Arc<dyn Plugin> = Arc::new(MockPlugin {
315            name: "plugin-a".to_string(),
316            dependencies: vec!["plugin-b".to_string()],
317        });
318
319        let plugin_b: Arc<dyn Plugin> = Arc::new(MockPlugin {
320            name: "plugin-b".to_string(),
321            dependencies: vec!["plugin-a".to_string()],
322        });
323
324        let mut plugins = HashMap::new();
325        plugins.insert("plugin-a".to_string(), plugin_a);
326        plugins.insert("plugin-b".to_string(), plugin_b);
327
328        let result = resolve_plugin_dependencies(plugins, false);
329        assert!(result.is_err());
330    }
331}