devalang_core/core/plugin/
loader.rs

1use serde::Deserialize;
2use std::path::Path;
3use toml::Value as TomlValue;
4
5#[derive(Debug, Deserialize, Clone)]
6pub struct PluginInfo {
7    pub name: String,
8    pub version: Option<String>,
9    pub description: Option<String>,
10    pub author: Option<String>,
11    #[serde(skip)]
12    pub exports: Vec<ExportEntry>,
13}
14
15#[derive(Debug, Deserialize, Clone)]
16pub struct ExportEntry {
17    pub name: String,
18    #[serde(rename = "type")]
19    pub kind: String,
20    #[serde(default)]
21    pub default: Option<TomlValue>,
22}
23
24#[derive(Debug, Deserialize, Clone)]
25pub struct PluginFile {
26    pub plugin: PluginInfo,
27    #[serde(default)]
28    pub export: Vec<ExportEntry>,
29}
30
31/// Load a plugin from local .deva directory given author and name
32pub fn load_plugin(author: &str, name: &str) -> Result<(PluginInfo, Vec<u8>), String> {
33    // Align with other loaders (banks) that use relative ./.deva paths
34    let root = Path::new("./.deva");
35    // Preferred layout: ./.deva/plugin/<author>.<name>/
36    let plugin_dir_preferred = root.join("plugin").join(format!("{}.{}", author, name));
37    let toml_path_preferred = plugin_dir_preferred.join("plugin.toml");
38    let wasm_path_preferred_bg = plugin_dir_preferred.join(format!("{}_bg.wasm", name));
39    let wasm_path_preferred_plain = plugin_dir_preferred.join(format!("{}.wasm", name));
40
41    // Legacy layout (fallback): ./.deva/plugin/<author>/<name>/
42    let plugin_dir_fallback = root.join("plugin").join(author).join(name);
43    let toml_path_fallback = plugin_dir_fallback.join("plugin.toml");
44    let wasm_path_fallback_bg = plugin_dir_fallback.join(format!("{}_bg.wasm", name));
45    let wasm_path_fallback_plain = plugin_dir_fallback.join(format!("{}.wasm", name));
46
47    // Resolve actual paths to use
48    let (toml_path, wasm_path) = if toml_path_preferred.exists() && wasm_path_preferred_bg.exists()
49    {
50        (toml_path_preferred, wasm_path_preferred_bg)
51    } else if toml_path_preferred.exists() && wasm_path_preferred_plain.exists() {
52        (toml_path_preferred, wasm_path_preferred_plain)
53    } else if toml_path_fallback.exists() && wasm_path_fallback_bg.exists() {
54        (toml_path_fallback, wasm_path_fallback_bg)
55    } else if toml_path_fallback.exists() && wasm_path_fallback_plain.exists() {
56        (toml_path_fallback, wasm_path_fallback_plain)
57    } else {
58        // If either file is missing in both layouts, produce specific errors for missing files in preferred layout
59        if !toml_path_preferred.exists() {
60            return Err(format!(
61                "❌ Plugin file not found: {}",
62                toml_path_preferred.display()
63            ));
64        }
65        if !wasm_path_preferred_bg.exists() && !wasm_path_preferred_plain.exists() {
66            return Err(format!(
67                "❌ Plugin wasm not found: '{}' or '{}'",
68                wasm_path_preferred_bg.display(),
69                wasm_path_preferred_plain.display()
70            ));
71        }
72        unreachable!();
73    };
74
75    let toml_content = std::fs::read_to_string(&toml_path)
76        .map_err(|e| format!("Failed to read '{}': {}", toml_path.display(), e))?;
77    let plugin_file: PluginFile = toml::from_str(&toml_content)
78        .map_err(|e| format!("Failed to parse '{}': {}", toml_path.display(), e))?;
79
80    let wasm_bytes = std::fs::read(&wasm_path)
81        .map_err(|e| format!("Failed to read '{}': {}", wasm_path.display(), e))?;
82
83    let mut info = plugin_file.plugin.clone();
84    info.exports = plugin_file.export.clone();
85
86    Ok((info, wasm_bytes))
87}
88
89/// Load a plugin from dot notation: "author.name"
90pub fn load_plugin_from_dot(dot: &str) -> Result<(PluginInfo, Vec<u8>), String> {
91    let mut parts = dot.split('.');
92    let author = parts
93        .next()
94        .ok_or_else(|| "Invalid plugin name, missing author".to_string())?;
95    let name = parts
96        .next()
97        .ok_or_else(|| "Invalid plugin name, missing name".to_string())?;
98    if parts.next().is_some() {
99        return Err("Invalid plugin name format, expected <author>.<name>".into());
100    }
101    load_plugin(author, name)
102}
103
104pub fn load_plugin_from_uri(uri: &str) -> Result<(PluginInfo, Vec<u8>), String> {
105    if !uri.starts_with("devalang://plugin/") {
106        return Err("Invalid plugin URI".into());
107    }
108
109    // Expect format: devalang://plugin/author.name
110    let payload = uri.trim_start_matches("devalang://plugin/");
111    let mut parts = payload.split('.');
112    let author = parts
113        .next()
114        .ok_or_else(|| "Invalid plugin URI, missing author".to_string())?;
115    let name = parts
116        .next()
117        .ok_or_else(|| "Invalid plugin URI, missing name".to_string())?;
118    if parts.next().is_some() {
119        return Err("Invalid plugin URI format, expected devalang://plugin/<author>.<name>".into());
120    }
121
122    load_plugin(author, name)
123}