Skip to main content

roboticus_plugin_sdk/
loader.rs

1use std::path::{Path, PathBuf};
2
3use tracing::{debug, warn};
4
5use roboticus_core::Result;
6
7use crate::manifest::PluginManifest;
8
9#[derive(Debug, Clone)]
10pub struct DiscoveredPlugin {
11    pub manifest: PluginManifest,
12    pub dir: PathBuf,
13}
14
15pub fn discover_plugins(plugins_dir: &Path) -> Result<Vec<DiscoveredPlugin>> {
16    if !plugins_dir.exists() {
17        debug!(dir = %plugins_dir.display(), "plugins directory does not exist");
18        return Ok(Vec::new());
19    }
20
21    let mut discovered = Vec::new();
22
23    let entries = std::fs::read_dir(plugins_dir)?;
24
25    for entry in entries {
26        let entry = entry?;
27        let path = entry.path();
28
29        if !path.is_dir() {
30            continue;
31        }
32
33        let manifest_path = path.join("plugin.toml");
34        if !manifest_path.exists() {
35            debug!(dir = %path.display(), "skipping directory without plugin.toml");
36            continue;
37        }
38
39        match PluginManifest::from_file(&manifest_path) {
40            Ok(manifest) => {
41                debug!(name = %manifest.name, version = %manifest.version, "discovered plugin");
42                discovered.push(DiscoveredPlugin {
43                    manifest,
44                    dir: path,
45                });
46            }
47            Err(e) => {
48                warn!(path = %manifest_path.display(), error = %e, "failed to parse plugin manifest");
49            }
50        }
51    }
52
53    discovered.sort_by(|a, b| a.manifest.name.cmp(&b.manifest.name));
54    Ok(discovered)
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn discover_missing_dir() {
63        let result = discover_plugins(Path::new("/nonexistent/plugins"));
64        assert!(result.is_ok());
65        assert!(result.unwrap().is_empty());
66    }
67
68    #[test]
69    fn discover_empty_dir() {
70        let dir = tempfile::tempdir().unwrap();
71        let result = discover_plugins(dir.path()).unwrap();
72        assert!(result.is_empty());
73    }
74
75    #[test]
76    fn discover_valid_plugin() {
77        let dir = tempfile::tempdir().unwrap();
78        let plugin_dir = dir.path().join("my-plugin");
79        std::fs::create_dir(&plugin_dir).unwrap();
80        std::fs::write(
81            plugin_dir.join("plugin.toml"),
82            r#"
83name = "my-plugin"
84version = "1.0.0"
85description = "Test plugin"
86"#,
87        )
88        .unwrap();
89
90        let result = discover_plugins(dir.path()).unwrap();
91        assert_eq!(result.len(), 1);
92        assert_eq!(result[0].manifest.name, "my-plugin");
93        assert_eq!(result[0].dir, plugin_dir);
94    }
95
96    #[test]
97    fn discover_skips_files() {
98        let dir = tempfile::tempdir().unwrap();
99        std::fs::write(dir.path().join("not-a-dir.txt"), "hello").unwrap();
100        let result = discover_plugins(dir.path()).unwrap();
101        assert!(result.is_empty());
102    }
103
104    #[test]
105    fn discover_skips_dir_without_manifest() {
106        let dir = tempfile::tempdir().unwrap();
107        std::fs::create_dir(dir.path().join("no-manifest")).unwrap();
108        let result = discover_plugins(dir.path()).unwrap();
109        assert!(result.is_empty());
110    }
111
112    #[test]
113    fn discover_skips_invalid_manifest() {
114        let dir = tempfile::tempdir().unwrap();
115        let plugin_dir = dir.path().join("bad");
116        std::fs::create_dir(&plugin_dir).unwrap();
117        std::fs::write(plugin_dir.join("plugin.toml"), "[[[[invalid").unwrap();
118        let result = discover_plugins(dir.path()).unwrap();
119        assert!(result.is_empty());
120    }
121
122    #[test]
123    fn discover_sorted_by_name() {
124        let dir = tempfile::tempdir().unwrap();
125        for name in ["charlie", "alpha", "bravo"] {
126            let p = dir.path().join(name);
127            std::fs::create_dir(&p).unwrap();
128            std::fs::write(
129                p.join("plugin.toml"),
130                format!("name = \"{name}\"\nversion = \"1.0.0\"\n"),
131            )
132            .unwrap();
133        }
134        let result = discover_plugins(dir.path()).unwrap();
135        let names: Vec<_> = result.iter().map(|p| p.manifest.name.as_str()).collect();
136        assert_eq!(names, vec!["alpha", "bravo", "charlie"]);
137    }
138}