Skip to main content

strixonomy_plugin/
discovery.rs

1use crate::manifest::{parse_manifest, DiscoveredPlugin};
2use std::fs;
3use std::path::{Path, PathBuf};
4use thiserror::Error;
5
6/// Relative directory scanned for plugin manifests inside a workspace (primary).
7pub const PLUGIN_DIR: &str = ".strixonomy/plugins";
8
9/// Legacy OntoCore plugins directory.
10#[allow(dead_code)] // public for callers / tests; discovery uses `plugin_search_dirs`
11pub const LEGACY_PLUGIN_DIR: &str = ".ontocore/plugins";
12
13#[derive(Debug, Error)]
14pub enum PluginDiscoveryError {
15    #[error("IO error reading {path}: {source}")]
16    Io { path: PathBuf, source: std::io::Error },
17    #[error("invalid manifest {path}: {message}")]
18    InvalidManifest { path: PathBuf, message: String },
19}
20
21/// Discover plugin manifests under `.strixonomy/plugins/` and legacy `.ontocore/plugins/`.
22pub fn discover_plugins(workspace: &Path) -> Result<Vec<DiscoveredPlugin>, PluginDiscoveryError> {
23    let mut discovered = Vec::new();
24    let mut seen_ids = std::collections::HashSet::new();
25    for plugins_dir in strixonomy_core::plugin_search_dirs(workspace) {
26        if !plugins_dir.is_dir() {
27            continue;
28        }
29        for entry in fs::read_dir(&plugins_dir)
30            .map_err(|source| PluginDiscoveryError::Io { path: plugins_dir.clone(), source })?
31        {
32            let entry = entry
33                .map_err(|source| PluginDiscoveryError::Io { path: plugins_dir.clone(), source })?;
34            let path = entry.path();
35            if path.extension().and_then(|e| e.to_str()) != Some("toml") {
36                continue;
37            }
38            let text = fs::read_to_string(&path)
39                .map_err(|source| PluginDiscoveryError::Io { path: path.clone(), source })?;
40            let manifest = parse_manifest(&text).map_err(|e| {
41                PluginDiscoveryError::InvalidManifest { path: path.clone(), message: e.to_string() }
42            })?;
43            if !seen_ids.insert(manifest.id.clone()) {
44                // Primary (.strixonomy) wins when both dirs define the same id.
45                continue;
46            }
47            discovered.push(DiscoveredPlugin { manifest, manifest_path: path });
48        }
49    }
50    Ok(discovered)
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn discovers_legacy_plugins_dir() {
59        let dir = tempfile::tempdir().unwrap();
60        let plugins = dir.path().join(LEGACY_PLUGIN_DIR);
61        fs::create_dir_all(&plugins).unwrap();
62        fs::write(
63            plugins.join("demo.toml"),
64            r#"
65[plugin]
66id = "demo"
67name = "Demo"
68version = "0.1.0"
69kind = "validator"
70entry = "./demo"
71"#,
72        )
73        .unwrap();
74        let found = discover_plugins(dir.path()).unwrap();
75        assert_eq!(found.len(), 1);
76        assert_eq!(found[0].manifest.id.as_deref(), Some("demo"));
77    }
78}