use serde::{Deserialize, Serialize};
use crate::BufFile;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PluginManifest {
#[serde(rename = "type")]
pub plugin_type: String,
pub name: String,
pub version: String,
#[serde(default)]
pub description: String,
pub js: String,
pub wasm: String,
#[serde(default)]
pub data_file: Option<String>,
#[serde(default)]
pub canvas_id: Option<String>,
#[serde(default)]
pub entry_point: Option<String>,
#[serde(default)]
pub storage_key: Option<String>,
#[serde(default)]
pub ldt_file: Option<String>,
#[serde(default)]
pub storage_keys: Option<StorageKeys>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct StorageKeys {
#[serde(default)]
pub ldt: Option<String>,
#[serde(default)]
pub config: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Plugin {
pub manifest: PluginManifest,
pub plugin_dir: String,
pub js_content: Vec<u8>,
pub wasm_content: Vec<u8>,
pub data_content: Option<Vec<u8>>,
pub ldt_content: Option<Vec<u8>>,
}
impl Plugin {
pub fn js_as_string(&self) -> Result<String, std::string::FromUtf8Error> {
String::from_utf8(self.js_content.clone())
}
pub fn has_data(&self) -> bool {
self.data_content.is_some()
}
pub fn data_as_string(&self) -> Option<Result<String, std::string::FromUtf8Error>> {
self.data_content
.as_ref()
.map(|d| String::from_utf8(d.clone()))
}
}
pub struct PluginManager;
impl PluginManager {
pub fn discover_plugins(files: &[BufFile]) -> Vec<Plugin> {
let mut plugins = Vec::new();
let manifests: Vec<_> = files
.iter()
.filter(|f| {
f.name
.as_ref()
.map(|n| n.starts_with("other/viewer/") && n.ends_with("/manifest.json"))
.unwrap_or(false)
})
.collect();
for manifest_file in manifests {
let manifest_path = match &manifest_file.name {
Some(p) => p,
None => continue,
};
let manifest_content = match &manifest_file.content {
Some(c) => c,
None => continue,
};
let manifest: PluginManifest = match serde_json::from_slice(manifest_content) {
Ok(m) => m,
Err(_e) => {
#[cfg(debug_assertions)]
eprintln!(
"[Plugin] Failed to parse manifest {}: {}",
manifest_path, _e
);
continue;
}
};
let plugin_dir = manifest_path
.strip_suffix("/manifest.json")
.unwrap_or(manifest_path)
.to_string();
let js_path = format!("{}/{}", plugin_dir, manifest.js);
let js_content = files
.iter()
.find(|f| f.name.as_ref() == Some(&js_path))
.and_then(|f| f.content.clone());
let wasm_path = format!("{}/{}", plugin_dir, manifest.wasm);
let wasm_content = files
.iter()
.find(|f| f.name.as_ref() == Some(&wasm_path))
.and_then(|f| f.content.clone());
let data_content = manifest.data_file.as_ref().and_then(|data_path| {
files
.iter()
.find(|f| f.name.as_ref() == Some(data_path))
.and_then(|f| f.content.clone())
});
let ldt_content = manifest.ldt_file.as_ref().and_then(|ldt_path| {
files
.iter()
.find(|f| f.name.as_ref() == Some(ldt_path))
.and_then(|f| f.content.clone())
});
if let (Some(js), Some(wasm)) = (js_content, wasm_content) {
plugins.push(Plugin {
manifest,
plugin_dir,
js_content: js,
wasm_content: wasm,
data_content,
ldt_content,
});
} else {
#[cfg(debug_assertions)]
eprintln!("[Plugin] {} missing JS or WASM files", manifest.name);
}
}
plugins
}
pub fn has_plugins(files: &[BufFile]) -> bool {
files.iter().any(|f| {
f.name
.as_ref()
.map(|n| n.starts_with("other/viewer/") && n.ends_with("/manifest.json"))
.unwrap_or(false)
})
}
pub fn get_plugin_types(files: &[BufFile]) -> Vec<String> {
Self::discover_plugins(files)
.iter()
.map(|p| p.manifest.plugin_type.clone())
.collect()
}
pub fn find_plugin(files: &[BufFile], plugin_type: &str) -> Option<Plugin> {
Self::discover_plugins(files)
.into_iter()
.find(|p| p.manifest.plugin_type == plugin_type)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_files() -> Vec<BufFile> {
let manifest = r#"{
"type": "test-plugin",
"name": "Test Plugin",
"version": "1.0.0",
"description": "A test plugin",
"js": "test.js",
"wasm": "test_bg.wasm",
"data_file": "other/test_data.json"
}"#;
vec![
BufFile {
name: Some("other/viewer/test-plugin/manifest.json".to_string()),
content: Some(manifest.as_bytes().to_vec()),
file_id: None,
path: None,
},
BufFile {
name: Some("other/viewer/test-plugin/test.js".to_string()),
content: Some(b"// test JS".to_vec()),
file_id: None,
path: None,
},
BufFile {
name: Some("other/viewer/test-plugin/test_bg.wasm".to_string()),
content: Some(b"\0asm\x01\0\0\0".to_vec()), file_id: None,
path: None,
},
BufFile {
name: Some("other/test_data.json".to_string()),
content: Some(b"{\"test\": true}".to_vec()),
file_id: None,
path: None,
},
]
}
#[test]
fn test_discover_plugins() {
let files = create_test_files();
let plugins = PluginManager::discover_plugins(&files);
assert_eq!(plugins.len(), 1);
let plugin = &plugins[0];
assert_eq!(plugin.manifest.plugin_type, "test-plugin");
assert_eq!(plugin.manifest.name, "Test Plugin");
assert!(plugin.has_data());
}
#[test]
fn test_has_plugins() {
let files = create_test_files();
assert!(PluginManager::has_plugins(&files));
let empty_files: Vec<BufFile> = vec![];
assert!(!PluginManager::has_plugins(&empty_files));
}
#[test]
fn test_find_plugin() {
let files = create_test_files();
let found = PluginManager::find_plugin(&files, "test-plugin");
assert!(found.is_some());
let not_found = PluginManager::find_plugin(&files, "nonexistent");
assert!(not_found.is_none());
}
}