Skip to main content

nemo_plugin_api/
lib.rs

1//! Nemo Plugin API - Shared interface for native plugins.
2//!
3//! This crate defines the stable API boundary between the Nemo host and native plugins.
4//! Plugins link against this crate to register their capabilities.
5//!
6//! # Writing a Plugin
7//!
8//! A Nemo plugin is a dynamic library (`cdylib`) that exports two symbols:
9//! - `nemo_plugin_manifest` - returns a [`PluginManifest`] describing the plugin
10//! - `nemo_plugin_entry` - called with a [`PluginRegistrar`] to register components,
11//!   data sources, transforms, actions, and templates
12//!
13//! Use the [`declare_plugin!`] macro to generate both exports.
14//!
15//! ## Minimal Example
16//!
17//! ```rust,no_run
18//! use nemo_plugin_api::*;
19//! use semver::Version;
20//!
21//! fn init(registrar: &mut dyn PluginRegistrar) {
22//!     // Register a custom component
23//!     registrar.register_component(
24//!         "my_counter",
25//!         ComponentSchema::new("my_counter")
26//!             .with_description("A counter component")
27//!             .with_property("initial", PropertySchema::integer())
28//!             .require("initial"),
29//!     );
30//!
31//!     // Access host data
32//!     if let Some(value) = registrar.context().get_data("app.settings.theme") {
33//!         registrar.context().log(LogLevel::Info, &format!("Theme: {:?}", value));
34//!     }
35//! }
36//!
37//! declare_plugin!(
38//!     PluginManifest::new("my-plugin", "My Plugin", Version::new(0, 1, 0))
39//!         .with_description("Example Nemo plugin")
40//!         .with_capability(Capability::Component("my_counter".into())),
41//!     init
42//! );
43//! ```
44//!
45//! ## Registering a Data Source
46//!
47//! ```rust,no_run
48//! # use nemo_plugin_api::*;
49//! fn init(registrar: &mut dyn PluginRegistrar) {
50//!     let mut schema = DataSourceSchema::new("my_feed");
51//!     schema.description = "Streams data from a custom feed".into();
52//!     schema.supports_streaming = true;
53//!     schema.properties.insert(
54//!         "url".into(),
55//!         PropertySchema::string().with_description("Feed URL"),
56//!     );
57//!     registrar.register_data_source("my_feed", schema);
58//! }
59//! ```
60//!
61//! ## Plugin Permissions
62//!
63//! Plugins declare required permissions in their manifest via [`PluginPermissions`].
64//! The host checks these before granting access to network, filesystem, or
65//! subprocess operations.
66
67use indexmap::IndexMap;
68use semver::Version;
69use serde::{Deserialize, Serialize};
70use std::collections::HashMap;
71use thiserror::Error;
72
73/// Error from plugin operations.
74#[derive(Debug, Error)]
75pub enum PluginError {
76    /// Plugin initialization failed.
77    #[error("Plugin initialization failed: {0}")]
78    InitFailed(String),
79
80    /// Component creation failed.
81    #[error("Component creation failed: {0}")]
82    ComponentFailed(String),
83
84    /// Invalid configuration.
85    #[error("Invalid configuration: {0}")]
86    InvalidConfig(String),
87
88    /// Permission denied.
89    #[error("Permission denied: {0}")]
90    PermissionDenied(String),
91}
92
93/// A configuration value (simplified for FFI safety).
94#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
95#[serde(untagged)]
96pub enum PluginValue {
97    /// Null value.
98    #[default]
99    Null,
100    /// Boolean value.
101    Bool(bool),
102    /// Integer value.
103    Integer(i64),
104    /// Float value.
105    Float(f64),
106    /// String value.
107    String(String),
108    /// Array of values.
109    Array(Vec<PluginValue>),
110    /// Object (map) of values, preserving insertion order.
111    Object(IndexMap<String, PluginValue>),
112}
113
114/// Plugin manifest describing capabilities.
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct PluginManifest {
117    /// Unique plugin identifier.
118    pub id: String,
119    /// Display name.
120    pub name: String,
121    /// Plugin version.
122    pub version: Version,
123    /// Description.
124    pub description: String,
125    /// Author information.
126    pub author: Option<String>,
127    /// Capabilities provided.
128    pub capabilities: Vec<Capability>,
129    /// Required permissions.
130    pub permissions: PluginPermissions,
131}
132
133impl PluginManifest {
134    /// Creates a new plugin manifest.
135    pub fn new(id: impl Into<String>, name: impl Into<String>, version: Version) -> Self {
136        Self {
137            id: id.into(),
138            name: name.into(),
139            version,
140            description: String::new(),
141            author: None,
142            capabilities: Vec::new(),
143            permissions: PluginPermissions::default(),
144        }
145    }
146
147    /// Sets the description.
148    pub fn with_description(mut self, description: impl Into<String>) -> Self {
149        self.description = description.into();
150        self
151    }
152
153    /// Adds a capability.
154    pub fn with_capability(mut self, capability: Capability) -> Self {
155        self.capabilities.push(capability);
156        self
157    }
158}
159
160/// Plugin capability type.
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub enum Capability {
163    /// Provides a UI component.
164    Component(String),
165    /// Provides a data source.
166    DataSource(String),
167    /// Provides a transform.
168    Transform(String),
169    /// Provides an action.
170    Action(String),
171    /// Provides an event handler.
172    EventHandler(String),
173}
174
175/// Permissions requested by a plugin.
176#[derive(Debug, Clone, Default, Serialize, Deserialize)]
177pub struct PluginPermissions {
178    /// Can make network requests.
179    pub network: bool,
180    /// Can access filesystem.
181    pub filesystem: bool,
182    /// Can spawn subprocesses.
183    pub subprocess: bool,
184    /// Allowed data paths.
185    pub data_paths: Vec<String>,
186    /// Allowed event types.
187    pub event_types: Vec<String>,
188}
189
190/// Trait for plugin registration.
191///
192/// Passed to the plugin entry point function. Plugins use this to register
193/// their capabilities (components, data sources, transforms, actions, and
194/// templates) with the Nemo host.
195///
196/// The registrar is only valid during the plugin entry call and must not be
197/// stored or used after the entry function returns.
198///
199/// # Example
200///
201/// ```rust,no_run
202/// # use nemo_plugin_api::*;
203/// fn init(registrar: &mut dyn PluginRegistrar) {
204///     registrar.register_component(
205///         "widget",
206///         ComponentSchema::new("widget")
207///             .with_property("title", PropertySchema::string()),
208///     );
209///     registrar.register_action(
210///         "refresh_widget",
211///         ActionSchema::new("refresh_widget"),
212///     );
213/// }
214/// ```
215pub trait PluginRegistrar {
216    /// Registers a component factory with the given name and schema.
217    fn register_component(&mut self, name: &str, schema: ComponentSchema);
218
219    /// Registers a data source factory with the given name and schema.
220    fn register_data_source(&mut self, name: &str, schema: DataSourceSchema);
221
222    /// Registers a transform with the given name and schema.
223    fn register_transform(&mut self, name: &str, schema: TransformSchema);
224
225    /// Registers an action with the given name and schema.
226    fn register_action(&mut self, name: &str, schema: ActionSchema);
227
228    /// Registers a UI template that can be referenced in HCL layout configs.
229    ///
230    /// Templates registered by plugins are merged with HCL-defined templates
231    /// during layout expansion. HCL-defined templates take precedence if there
232    /// is a name collision.
233    fn register_template(&mut self, name: &str, template: PluginValue);
234
235    /// Gets the plugin context for API access during initialization.
236    fn context(&self) -> &dyn PluginContext;
237
238    /// Gets the plugin context as an `Arc` for use in background threads.
239    ///
240    /// The returned `Arc<dyn PluginContext>` is `Send + Sync` and can safely
241    /// be moved to spawned tasks.
242    fn context_arc(&self) -> std::sync::Arc<dyn PluginContext>;
243}
244
245/// Context providing API access to plugins at runtime.
246///
247/// This trait is `Send + Sync`, allowing plugins to use it from background
248/// threads and async tasks. Obtain an `Arc<dyn PluginContext>` via
249/// [`PluginRegistrar::context_arc`] for shared ownership.
250///
251/// # Data Paths
252///
253/// Data paths use dot-separated notation: `"data.my_source.items"`.
254/// Config paths follow the same convention: `"app.settings.theme"`.
255///
256/// # Example
257///
258/// ```rust,no_run
259/// # use nemo_plugin_api::*;
260/// fn read_and_write(ctx: &dyn PluginContext) {
261///     // Read a value
262///     if let Some(PluginValue::Integer(count)) = ctx.get_data("metrics.request_count") {
263///         ctx.log(LogLevel::Info, &format!("Requests: {}", count));
264///     }
265///
266///     // Write a value
267///     ctx.set_data("metrics.last_check", PluginValue::String("now".into())).ok();
268///
269///     // Emit an event for other plugins/components to observe
270///     ctx.emit_event("plugin:refresh", PluginValue::Null);
271/// }
272/// ```
273pub trait PluginContext: Send + Sync {
274    /// Gets data by dot-separated path (e.g. `"data.source.field"`).
275    fn get_data(&self, path: &str) -> Option<PluginValue>;
276
277    /// Sets data at a dot-separated path.
278    ///
279    /// Returns `Err` if the path is invalid or permission is denied.
280    fn set_data(&self, path: &str, value: PluginValue) -> Result<(), PluginError>;
281
282    /// Emits a named event with an arbitrary payload.
283    ///
284    /// Events are delivered asynchronously via the host's event bus.
285    fn emit_event(&self, event_type: &str, payload: PluginValue);
286
287    /// Gets a configuration value by dot-separated path.
288    fn get_config(&self, path: &str) -> Option<PluginValue>;
289
290    /// Logs a message at the given severity level.
291    fn log(&self, level: LogLevel, message: &str);
292
293    /// Gets a component property by component ID and property name.
294    ///
295    /// Returns `None` if the component or property does not exist.
296    fn get_component_property(&self, component_id: &str, property: &str) -> Option<PluginValue>;
297
298    /// Sets a component property by component ID and property name.
299    ///
300    /// Returns `Err` if the component does not exist or the property is
301    /// read-only.
302    fn set_component_property(
303        &self,
304        component_id: &str,
305        property: &str,
306        value: PluginValue,
307    ) -> Result<(), PluginError>;
308}
309
310/// Log level.
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum LogLevel {
313    /// Debug level.
314    Debug,
315    /// Info level.
316    Info,
317    /// Warning level.
318    Warn,
319    /// Error level.
320    Error,
321}
322
323/// Schema for a component.
324#[derive(Debug, Clone, Default, Serialize, Deserialize)]
325pub struct ComponentSchema {
326    /// Component name.
327    pub name: String,
328    /// Description.
329    pub description: String,
330    /// Configuration properties.
331    pub properties: HashMap<String, PropertySchema>,
332    /// Required properties.
333    pub required: Vec<String>,
334}
335
336impl ComponentSchema {
337    /// Creates a new component schema.
338    pub fn new(name: impl Into<String>) -> Self {
339        Self {
340            name: name.into(),
341            ..Default::default()
342        }
343    }
344
345    /// Sets the description.
346    pub fn with_description(mut self, description: impl Into<String>) -> Self {
347        self.description = description.into();
348        self
349    }
350
351    /// Adds a property.
352    pub fn with_property(mut self, name: impl Into<String>, schema: PropertySchema) -> Self {
353        self.properties.insert(name.into(), schema);
354        self
355    }
356
357    /// Marks a property as required.
358    pub fn require(mut self, name: impl Into<String>) -> Self {
359        self.required.push(name.into());
360        self
361    }
362}
363
364/// Schema for a property.
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct PropertySchema {
367    /// Property type.
368    pub property_type: PropertyType,
369    /// Description.
370    pub description: Option<String>,
371    /// Default value.
372    pub default: Option<PluginValue>,
373}
374
375impl PropertySchema {
376    /// Creates a string property schema.
377    pub fn string() -> Self {
378        Self {
379            property_type: PropertyType::String,
380            description: None,
381            default: None,
382        }
383    }
384
385    /// Creates a boolean property schema.
386    pub fn boolean() -> Self {
387        Self {
388            property_type: PropertyType::Boolean,
389            description: None,
390            default: None,
391        }
392    }
393
394    /// Creates an integer property schema.
395    pub fn integer() -> Self {
396        Self {
397            property_type: PropertyType::Integer,
398            description: None,
399            default: None,
400        }
401    }
402
403    /// Sets the description.
404    pub fn with_description(mut self, description: impl Into<String>) -> Self {
405        self.description = Some(description.into());
406        self
407    }
408
409    /// Sets the default value.
410    pub fn with_default(mut self, default: PluginValue) -> Self {
411        self.default = Some(default);
412        self
413    }
414}
415
416/// Property type.
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
418pub enum PropertyType {
419    /// String type.
420    String,
421    /// Boolean type.
422    Boolean,
423    /// Integer type.
424    Integer,
425    /// Float type.
426    Float,
427    /// Array type.
428    Array,
429    /// Object type.
430    Object,
431    /// Any type.
432    Any,
433}
434
435/// Schema for a data source.
436#[derive(Debug, Clone, Default, Serialize, Deserialize)]
437pub struct DataSourceSchema {
438    /// Data source name.
439    pub name: String,
440    /// Description.
441    pub description: String,
442    /// Supports polling.
443    pub supports_polling: bool,
444    /// Supports streaming.
445    pub supports_streaming: bool,
446    /// Configuration properties.
447    pub properties: HashMap<String, PropertySchema>,
448}
449
450impl DataSourceSchema {
451    /// Creates a new data source schema.
452    pub fn new(name: impl Into<String>) -> Self {
453        Self {
454            name: name.into(),
455            ..Default::default()
456        }
457    }
458}
459
460/// Schema for a transform.
461#[derive(Debug, Clone, Default, Serialize, Deserialize)]
462pub struct TransformSchema {
463    /// Transform name.
464    pub name: String,
465    /// Description.
466    pub description: String,
467    /// Configuration properties.
468    pub properties: HashMap<String, PropertySchema>,
469}
470
471impl TransformSchema {
472    /// Creates a new transform schema.
473    pub fn new(name: impl Into<String>) -> Self {
474        Self {
475            name: name.into(),
476            ..Default::default()
477        }
478    }
479}
480
481/// Schema for an action.
482#[derive(Debug, Clone, Default, Serialize, Deserialize)]
483pub struct ActionSchema {
484    /// Action name.
485    pub name: String,
486    /// Description.
487    pub description: String,
488    /// Whether action is async.
489    pub is_async: bool,
490    /// Configuration properties.
491    pub properties: HashMap<String, PropertySchema>,
492}
493
494impl ActionSchema {
495    /// Creates a new action schema.
496    pub fn new(name: impl Into<String>) -> Self {
497        Self {
498            name: name.into(),
499            ..Default::default()
500        }
501    }
502}
503
504/// Plugin entry point function type.
505///
506/// # Safety
507///
508/// This type is the signature for native plugin entry points loaded via
509/// `dlopen`/`LoadLibrary`. The following invariants must hold:
510///
511/// - **ABI compatibility**: The plugin must be compiled with the same Rust
512///   compiler version and the same `nemo-plugin-api` crate version as the
513///   host. Mismatched versions cause undefined behaviour due to differing
514///   type layouts.
515/// - **Single-threaded call**: The host calls this function on the main
516///   thread. The `PluginRegistrar` reference is valid only for the duration
517///   of the call and must not be stored or sent to other threads.
518/// - **No unwinding**: The entry function must not panic. A panic across
519///   the FFI boundary is undefined behaviour. Use `catch_unwind` internally
520///   if necessary.
521/// - **Library lifetime**: The dynamic library must remain loaded for as
522///   long as any symbols (vtables, function pointers) obtained through the
523///   registrar are in use.
524/// - **No re-entrancy**: The entry function must not call back into the
525///   host's plugin loading machinery.
526#[allow(improper_ctypes_definitions)]
527pub type PluginEntryFn = unsafe extern "C" fn(&mut dyn PluginRegistrar);
528
529/// Macro to declare a plugin entry point.
530///
531/// Generates two `extern "C"` functions:
532/// - `nemo_plugin_manifest() -> PluginManifest` — returns the plugin descriptor
533/// - `nemo_plugin_entry(registrar: &mut dyn PluginRegistrar)` — called to
534///   register capabilities
535///
536/// # Example
537///
538/// ```rust,no_run
539/// # use nemo_plugin_api::*;
540/// # use semver::Version;
541/// declare_plugin!(
542///     PluginManifest::new("hello", "Hello Plugin", Version::new(1, 0, 0))
543///         .with_description("Greets the user"),
544///     |registrar: &mut dyn PluginRegistrar| {
545///         registrar.context().log(LogLevel::Info, "Hello from plugin!");
546///     }
547/// );
548/// ```
549#[macro_export]
550macro_rules! declare_plugin {
551    ($manifest:expr, $init:expr) => {
552        #[no_mangle]
553        pub extern "C" fn nemo_plugin_manifest() -> $crate::PluginManifest {
554            $manifest
555        }
556
557        #[no_mangle]
558        pub extern "C" fn nemo_plugin_entry(registrar: &mut dyn $crate::PluginRegistrar) {
559            $init(registrar)
560        }
561    };
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567
568    #[test]
569    fn test_plugin_manifest() {
570        let manifest = PluginManifest::new("test-plugin", "Test Plugin", Version::new(1, 0, 0))
571            .with_description("A test plugin")
572            .with_capability(Capability::Component("my-component".into()));
573
574        assert_eq!(manifest.id, "test-plugin");
575        assert_eq!(manifest.capabilities.len(), 1);
576    }
577
578    #[test]
579    fn test_component_schema() {
580        let schema = ComponentSchema::new("button")
581            .with_description("A button component")
582            .with_property("label", PropertySchema::string())
583            .require("label");
584
585        assert!(schema.required.contains(&"label".to_string()));
586    }
587
588    #[test]
589    fn test_plugin_value() {
590        let value = PluginValue::Object(IndexMap::from([
591            ("name".to_string(), PluginValue::String("test".to_string())),
592            ("count".to_string(), PluginValue::Integer(42)),
593        ]));
594
595        if let PluginValue::Object(obj) = value {
596            assert!(obj.contains_key("name"));
597        }
598    }
599}