devalang_core/core/plugin/
loader.rs1use devalang_types::{plugin::PluginExport, plugin::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 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(rename = "exports", default)]
19 pub exports: 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 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 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 !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 let mut exports: Vec<PluginExport> = Vec::new();
81 for e in plugin_file.exports.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
103pub 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 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}