use indexmap::IndexMap;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PluginError {
#[error("Plugin initialization failed: {0}")]
InitFailed(String),
#[error("Component creation failed: {0}")]
ComponentFailed(String),
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PluginValue {
#[default]
Null,
Bool(bool),
Integer(i64),
Float(f64),
String(String),
Array(Vec<PluginValue>),
Object(IndexMap<String, PluginValue>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
pub id: String,
pub name: String,
pub version: Version,
pub description: String,
pub author: Option<String>,
pub capabilities: Vec<Capability>,
pub permissions: PluginPermissions,
}
impl PluginManifest {
pub fn new(id: impl Into<String>, name: impl Into<String>, version: Version) -> Self {
Self {
id: id.into(),
name: name.into(),
version,
description: String::new(),
author: None,
capabilities: Vec::new(),
permissions: PluginPermissions::default(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn with_capability(mut self, capability: Capability) -> Self {
self.capabilities.push(capability);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Capability {
Component(String),
DataSource(String),
Transform(String),
Action(String),
EventHandler(String),
Settings(String),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginPermissions {
pub network: bool,
pub filesystem: bool,
pub subprocess: bool,
pub data_paths: Vec<String>,
pub event_types: Vec<String>,
}
pub trait PluginRegistrar {
fn register_component(&mut self, name: &str, schema: ComponentSchema);
fn register_data_source(&mut self, name: &str, schema: DataSourceSchema);
fn register_transform(&mut self, name: &str, schema: TransformSchema);
fn register_action(&mut self, name: &str, schema: ActionSchema);
fn register_settings_page(&mut self, name: &str, page: PluginValue);
fn register_template(&mut self, name: &str, template: PluginValue);
fn context(&self) -> &dyn PluginContext;
fn context_arc(&self) -> std::sync::Arc<dyn PluginContext>;
}
pub trait PluginContext: Send + Sync {
fn get_data(&self, path: &str) -> Option<PluginValue>;
fn set_data(&self, path: &str, value: PluginValue) -> Result<(), PluginError>;
fn emit_event(&self, event_type: &str, payload: PluginValue);
fn get_config(&self, path: &str) -> Option<PluginValue>;
fn log(&self, level: LogLevel, message: &str);
fn get_component_property(&self, component_id: &str, property: &str) -> Option<PluginValue>;
fn set_component_property(
&self,
component_id: &str,
property: &str,
value: PluginValue,
) -> Result<(), PluginError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ComponentSchema {
pub name: String,
pub description: String,
pub properties: HashMap<String, PropertySchema>,
pub required: Vec<String>,
}
impl ComponentSchema {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn with_property(mut self, name: impl Into<String>, schema: PropertySchema) -> Self {
self.properties.insert(name.into(), schema);
self
}
pub fn require(mut self, name: impl Into<String>) -> Self {
self.required.push(name.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PropertySchema {
pub property_type: PropertyType,
pub description: Option<String>,
pub default: Option<PluginValue>,
}
impl PropertySchema {
pub fn string() -> Self {
Self {
property_type: PropertyType::String,
description: None,
default: None,
}
}
pub fn boolean() -> Self {
Self {
property_type: PropertyType::Boolean,
description: None,
default: None,
}
}
pub fn integer() -> Self {
Self {
property_type: PropertyType::Integer,
description: None,
default: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_default(mut self, default: PluginValue) -> Self {
self.default = Some(default);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PropertyType {
String,
Boolean,
Integer,
Float,
Array,
Object,
Any,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DataSourceSchema {
pub name: String,
pub description: String,
pub supports_polling: bool,
pub supports_streaming: bool,
pub properties: HashMap<String, PropertySchema>,
}
impl DataSourceSchema {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TransformSchema {
pub name: String,
pub description: String,
pub properties: HashMap<String, PropertySchema>,
}
impl TransformSchema {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ActionSchema {
pub name: String,
pub description: String,
pub is_async: bool,
pub properties: HashMap<String, PropertySchema>,
}
impl ActionSchema {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
}
#[allow(improper_ctypes_definitions)]
pub type PluginEntryFn = unsafe extern "C" fn(&mut dyn PluginRegistrar);
#[macro_export]
macro_rules! declare_plugin {
($manifest:expr, $init:expr) => {
#[no_mangle]
pub extern "C" fn nemo_plugin_manifest() -> $crate::PluginManifest {
$manifest
}
#[no_mangle]
pub extern "C" fn nemo_plugin_entry(registrar: &mut dyn $crate::PluginRegistrar) {
$init(registrar)
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_manifest() {
let manifest = PluginManifest::new("test-plugin", "Test Plugin", Version::new(1, 0, 0))
.with_description("A test plugin")
.with_capability(Capability::Component("my-component".into()));
assert_eq!(manifest.id, "test-plugin");
assert_eq!(manifest.capabilities.len(), 1);
}
#[test]
fn test_component_schema() {
let schema = ComponentSchema::new("button")
.with_description("A button component")
.with_property("label", PropertySchema::string())
.require("label");
assert!(schema.required.contains(&"label".to_string()));
}
#[test]
fn test_plugin_value() {
let value = PluginValue::Object(IndexMap::from([
("name".to_string(), PluginValue::String("test".to_string())),
("count".to_string(), PluginValue::Integer(42)),
]));
if let PluginValue::Object(obj) = value {
assert!(obj.contains_key("name"));
}
}
}