devalang_core/core/plugin/
loader.rs

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