fusabi_plugin_runtime/
loader.rs1use std::path::{Path, PathBuf};
4
5use fusabi_host::{
6 compile_source, compile_file, validate_bytecode, CompileOptions,
7 EngineConfig,
8};
9
10use crate::error::{Error, Result};
11use crate::manifest::{ApiVersion, Manifest};
12use crate::plugin::{Plugin, PluginHandle};
13
14#[derive(Debug, Clone)]
16pub struct LoaderConfig {
17 pub engine_config: EngineConfig,
19 pub compile_options: CompileOptions,
21 pub host_api_version: ApiVersion,
23 pub base_path: Option<PathBuf>,
25 pub auto_start: bool,
27 pub strict_validation: bool,
29}
30
31impl Default for LoaderConfig {
32 fn default() -> Self {
33 Self {
34 engine_config: EngineConfig::default(),
35 compile_options: CompileOptions::default(),
36 host_api_version: ApiVersion::default(),
37 base_path: None,
38 auto_start: true,
39 strict_validation: true,
40 }
41 }
42}
43
44impl LoaderConfig {
45 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn with_engine_config(mut self, config: EngineConfig) -> Self {
52 self.engine_config = config;
53 self
54 }
55
56 pub fn with_compile_options(mut self, options: CompileOptions) -> Self {
58 self.compile_options = options;
59 self
60 }
61
62 pub fn with_host_api_version(mut self, version: ApiVersion) -> Self {
64 self.host_api_version = version;
65 self
66 }
67
68 pub fn with_base_path(mut self, path: impl Into<PathBuf>) -> Self {
70 self.base_path = Some(path.into());
71 self
72 }
73
74 pub fn with_auto_start(mut self, auto_start: bool) -> Self {
76 self.auto_start = auto_start;
77 self
78 }
79
80 pub fn with_strict_validation(mut self, strict: bool) -> Self {
82 self.strict_validation = strict;
83 self
84 }
85
86 pub fn strict() -> Self {
88 Self {
89 engine_config: EngineConfig::strict(),
90 compile_options: CompileOptions::production(),
91 host_api_version: ApiVersion::default(),
92 base_path: None,
93 auto_start: false,
94 strict_validation: true,
95 }
96 }
97}
98
99pub struct PluginLoader {
101 config: LoaderConfig,
102}
103
104impl PluginLoader {
105 pub fn new(config: LoaderConfig) -> Result<Self> {
107 Ok(Self { config })
108 }
109
110 pub fn config(&self) -> &LoaderConfig {
112 &self.config
113 }
114
115 #[cfg(feature = "serde")]
117 pub fn load_from_manifest(&self, manifest_path: impl AsRef<Path>) -> Result<PluginHandle> {
118 let manifest_path = self.resolve_path(manifest_path.as_ref());
119 let manifest = Manifest::from_file(&manifest_path)?;
120
121 self.load_manifest(manifest, Some(manifest_path))
122 }
123
124 pub fn load_manifest(
126 &self,
127 manifest: Manifest,
128 manifest_path: Option<PathBuf>,
129 ) -> Result<PluginHandle> {
130 if self.config.strict_validation {
132 manifest.validate()?;
133 }
134
135 if !manifest.is_compatible_with_host(&self.config.host_api_version) {
137 return Err(Error::api_version_mismatch(
138 manifest.api_version.to_string(),
139 self.config.host_api_version.to_string(),
140 ));
141 }
142
143 let plugin = Plugin::new(manifest.clone());
145
146 let entry_path = manifest.entry_point().map(|p| {
148 if let Some(ref manifest_path) = manifest_path {
149 manifest_path.parent().unwrap_or(Path::new(".")).join(p)
150 } else {
151 self.resolve_path(Path::new(p))
152 }
153 });
154
155 if let Some(ref entry_path) = entry_path {
157 if manifest.uses_source() {
158 self.compile_and_load(&plugin, entry_path)?;
159 } else {
160 self.load_bytecode(&plugin, entry_path)?;
161 }
162 }
163
164 let engine_config = self.build_engine_config(&manifest)?;
166
167 plugin.initialize(engine_config)?;
169
170 if self.config.auto_start {
172 plugin.start()?;
173 }
174
175 Ok(PluginHandle::new(plugin))
176 }
177
178 pub fn load_source(&self, source_path: impl AsRef<Path>) -> Result<PluginHandle> {
180 let source_path = self.resolve_path(source_path.as_ref());
181
182 let source = std::fs::read_to_string(&source_path)?;
184
185 let name = source_path
187 .file_stem()
188 .and_then(|s| s.to_str())
189 .unwrap_or("unnamed")
190 .to_string();
191
192 let manifest = Manifest::new(name, "0.0.0");
193
194 let plugin = Plugin::new(manifest);
196
197 let compile_result = compile_source(&source, &self.config.compile_options)?;
199 plugin.set_bytecode(compile_result.bytecode);
200
201 plugin.initialize(self.config.engine_config.clone())?;
203
204 if self.config.auto_start {
206 plugin.start()?;
207 }
208
209 Ok(PluginHandle::new(plugin))
210 }
211
212 pub fn load_bytecode_file(&self, bytecode_path: impl AsRef<Path>) -> Result<PluginHandle> {
214 let bytecode_path = self.resolve_path(bytecode_path.as_ref());
215
216 let bytecode = std::fs::read(&bytecode_path)?;
218
219 let metadata = validate_bytecode(&bytecode)?;
221
222 let name = bytecode_path
224 .file_stem()
225 .and_then(|s| s.to_str())
226 .unwrap_or("unnamed")
227 .to_string();
228
229 let manifest = Manifest::new(name, metadata.compiler_version.clone());
230
231 let plugin = Plugin::new(manifest);
233 plugin.set_bytecode(bytecode);
234
235 plugin.initialize(self.config.engine_config.clone())?;
237
238 if self.config.auto_start {
240 plugin.start()?;
241 }
242
243 Ok(PluginHandle::new(plugin))
244 }
245
246 pub fn reload(&self, plugin: &PluginHandle) -> Result<()> {
248 plugin.inner().reload()
249 }
250
251 fn resolve_path(&self, path: &Path) -> PathBuf {
254 if path.is_absolute() {
255 path.to_path_buf()
256 } else if let Some(ref base) = self.config.base_path {
257 base.join(path)
258 } else {
259 path.to_path_buf()
260 }
261 }
262
263 fn compile_and_load(&self, plugin: &Plugin, source_path: &Path) -> Result<()> {
264 let compile_result = compile_file(source_path, &self.config.compile_options)
265 .map_err(|e: fusabi_host::Error| Error::Compilation(e.to_string()))?;
266
267 plugin.set_bytecode(compile_result.bytecode);
268
269 for warning in &compile_result.warnings {
271 tracing::warn!("Plugin {}: {}", plugin.name(), warning.message);
272 }
273
274 Ok(())
275 }
276
277 fn load_bytecode(&self, plugin: &Plugin, bytecode_path: &Path) -> Result<()> {
278 let bytecode = std::fs::read(bytecode_path)?;
279
280 validate_bytecode(&bytecode)?;
282
283 plugin.set_bytecode(bytecode);
284 Ok(())
285 }
286
287 fn build_engine_config(&self, manifest: &Manifest) -> Result<EngineConfig> {
288 let mut config = self.config.engine_config.clone();
290
291 let mut caps = config.capabilities.clone();
293 for cap_name in &manifest.capabilities {
294 let cap = fusabi_host::Capability::from_name(cap_name)
295 .ok_or_else(|| Error::invalid_manifest(format!("unknown capability: {}", cap_name)))?;
296 caps.grant(cap);
297 }
298 config.capabilities = caps;
299
300 Ok(config)
301 }
302}
303
304impl std::fmt::Debug for PluginLoader {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 f.debug_struct("PluginLoader")
307 .field("config", &self.config)
308 .finish()
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::manifest::ManifestBuilder;
316
317 #[test]
318 fn test_loader_config_builder() {
319 let config = LoaderConfig::new()
320 .with_auto_start(false)
321 .with_strict_validation(true);
322
323 assert!(!config.auto_start);
324 assert!(config.strict_validation);
325 }
326
327 #[test]
328 fn test_loader_creation() {
329 let loader = PluginLoader::new(LoaderConfig::default()).unwrap();
330 assert!(loader.config().auto_start);
331 }
332
333 #[test]
334 fn test_load_manifest() {
335 let loader = PluginLoader::new(
336 LoaderConfig::new().with_auto_start(false),
337 )
338 .unwrap();
339
340 let manifest = ManifestBuilder::new("test-plugin", "1.0.0")
341 .source("test.fsx")
342 .build_unchecked();
343
344 let result = loader.load_manifest(manifest, None);
347
348 assert!(result.is_err());
350 }
351
352 #[test]
353 fn test_api_version_check() {
354 let loader = PluginLoader::new(
355 LoaderConfig::new()
356 .with_host_api_version(ApiVersion::new(0, 18, 0))
357 .with_auto_start(false),
358 )
359 .unwrap();
360
361 let manifest = ManifestBuilder::new("test", "1.0.0")
363 .api_version(ApiVersion::new(1, 0, 0))
364 .source("test.fsx")
365 .build_unchecked();
366
367 let result = loader.load_manifest(manifest, None);
368 assert!(matches!(result, Err(Error::ApiVersionMismatch { .. })));
369 }
370}