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    /// Provides a settings page.
174    Settings(String),
175}
176
177/// Permissions requested by a plugin.
178#[derive(Debug, Clone, Default, Serialize, Deserialize)]
179pub struct PluginPermissions {
180    /// Can make network requests.
181    pub network: bool,
182    /// Can access filesystem.
183    pub filesystem: bool,
184    /// Can spawn subprocesses.
185    pub subprocess: bool,
186    /// Allowed data paths.
187    pub data_paths: Vec<String>,
188    /// Allowed event types.
189    pub event_types: Vec<String>,
190}
191
192/// Trait for plugin registration.
193///
194/// Passed to the plugin entry point function. Plugins use this to register
195/// their capabilities (components, data sources, transforms, actions, and
196/// templates) with the Nemo host.
197///
198/// The registrar is only valid during the plugin entry call and must not be
199/// stored or used after the entry function returns.
200///
201/// # Example
202///
203/// ```rust,no_run
204/// # use nemo_plugin_api::*;
205/// fn init(registrar: &mut dyn PluginRegistrar) {
206///     registrar.register_component(
207///         "widget",
208///         ComponentSchema::new("widget")
209///             .with_property("title", PropertySchema::string()),
210///     );
211///     registrar.register_action(
212///         "refresh_widget",
213///         ActionSchema::new("refresh_widget"),
214///     );
215/// }
216/// ```
217pub trait PluginRegistrar {
218    /// Registers a component factory with the given name and schema.
219    fn register_component(&mut self, name: &str, schema: ComponentSchema);
220
221    /// Registers a data source factory with the given name and schema.
222    fn register_data_source(&mut self, name: &str, schema: DataSourceSchema);
223
224    /// Registers a transform with the given name and schema.
225    fn register_transform(&mut self, name: &str, schema: TransformSchema);
226
227    /// Registers an action with the given name and schema.
228    fn register_action(&mut self, name: &str, schema: ActionSchema);
229
230    /// Registers a settings page with the given display name and UI definition.
231    ///
232    /// The `page` value is a `PluginValue::Object` describing the settings UI
233    /// using a declarative layout (e.g. `stack`, `label`, `switch`, `input`).
234    fn register_settings_page(&mut self, name: &str, page: PluginValue);
235
236    /// Registers a UI template that can be referenced in HCL layout configs.
237    ///
238    /// Templates registered by plugins are merged with HCL-defined templates
239    /// during layout expansion. HCL-defined templates take precedence if there
240    /// is a name collision.
241    fn register_template(&mut self, name: &str, template: PluginValue);
242
243    /// Gets the plugin context for API access during initialization.
244    fn context(&self) -> &dyn PluginContext;
245
246    /// Gets the plugin context as an `Arc` for use in background threads.
247    ///
248    /// The returned `Arc<dyn PluginContext>` is `Send + Sync` and can safely
249    /// be moved to spawned tasks.
250    fn context_arc(&self) -> std::sync::Arc<dyn PluginContext>;
251}
252
253/// Context providing API access to plugins at runtime.
254///
255/// This trait is `Send + Sync`, allowing plugins to use it from background
256/// threads and async tasks. Obtain an `Arc<dyn PluginContext>` via
257/// [`PluginRegistrar::context_arc`] for shared ownership.
258///
259/// # Data Paths
260///
261/// Data paths use dot-separated notation: `"data.my_source.items"`.
262/// Config paths follow the same convention: `"app.settings.theme"`.
263///
264/// # Example
265///
266/// ```rust,no_run
267/// # use nemo_plugin_api::*;
268/// fn read_and_write(ctx: &dyn PluginContext) {
269///     // Read a value
270///     if let Some(PluginValue::Integer(count)) = ctx.get_data("metrics.request_count") {
271///         ctx.log(LogLevel::Info, &format!("Requests: {}", count));
272///     }
273///
274///     // Write a value
275///     ctx.set_data("metrics.last_check", PluginValue::String("now".into())).ok();
276///
277///     // Emit an event for other plugins/components to observe
278///     ctx.emit_event("plugin:refresh", PluginValue::Null);
279/// }
280/// ```
281pub trait PluginContext: Send + Sync {
282    /// Gets data by dot-separated path (e.g. `"data.source.field"`).
283    fn get_data(&self, path: &str) -> Option<PluginValue>;
284
285    /// Sets data at a dot-separated path.
286    ///
287    /// Returns `Err` if the path is invalid or permission is denied.
288    fn set_data(&self, path: &str, value: PluginValue) -> Result<(), PluginError>;
289
290    /// Emits a named event with an arbitrary payload.
291    ///
292    /// Events are delivered asynchronously via the host's event bus.
293    fn emit_event(&self, event_type: &str, payload: PluginValue);
294
295    /// Gets a configuration value by dot-separated path.
296    fn get_config(&self, path: &str) -> Option<PluginValue>;
297
298    /// Logs a message at the given severity level.
299    fn log(&self, level: LogLevel, message: &str);
300
301    /// Gets a component property by component ID and property name.
302    ///
303    /// Returns `None` if the component or property does not exist.
304    fn get_component_property(&self, component_id: &str, property: &str) -> Option<PluginValue>;
305
306    /// Sets a component property by component ID and property name.
307    ///
308    /// Returns `Err` if the component does not exist or the property is
309    /// read-only.
310    fn set_component_property(
311        &self,
312        component_id: &str,
313        property: &str,
314        value: PluginValue,
315    ) -> Result<(), PluginError>;
316}
317
318/// Log level.
319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub enum LogLevel {
321    /// Debug level.
322    Debug,
323    /// Info level.
324    Info,
325    /// Warning level.
326    Warn,
327    /// Error level.
328    Error,
329}
330
331/// Schema for a component.
332#[derive(Debug, Clone, Default, Serialize, Deserialize)]
333pub struct ComponentSchema {
334    /// Component name.
335    pub name: String,
336    /// Description.
337    pub description: String,
338    /// Configuration properties.
339    pub properties: HashMap<String, PropertySchema>,
340    /// Required properties.
341    pub required: Vec<String>,
342}
343
344impl ComponentSchema {
345    /// Creates a new component schema.
346    pub fn new(name: impl Into<String>) -> Self {
347        Self {
348            name: name.into(),
349            ..Default::default()
350        }
351    }
352
353    /// Sets the description.
354    pub fn with_description(mut self, description: impl Into<String>) -> Self {
355        self.description = description.into();
356        self
357    }
358
359    /// Adds a property.
360    pub fn with_property(mut self, name: impl Into<String>, schema: PropertySchema) -> Self {
361        self.properties.insert(name.into(), schema);
362        self
363    }
364
365    /// Marks a property as required.
366    pub fn require(mut self, name: impl Into<String>) -> Self {
367        self.required.push(name.into());
368        self
369    }
370}
371
372/// Schema for a property.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct PropertySchema {
375    /// Property type.
376    pub property_type: PropertyType,
377    /// Description.
378    pub description: Option<String>,
379    /// Default value.
380    pub default: Option<PluginValue>,
381}
382
383impl PropertySchema {
384    /// Creates a string property schema.
385    pub fn string() -> Self {
386        Self {
387            property_type: PropertyType::String,
388            description: None,
389            default: None,
390        }
391    }
392
393    /// Creates a boolean property schema.
394    pub fn boolean() -> Self {
395        Self {
396            property_type: PropertyType::Boolean,
397            description: None,
398            default: None,
399        }
400    }
401
402    /// Creates an integer property schema.
403    pub fn integer() -> Self {
404        Self {
405            property_type: PropertyType::Integer,
406            description: None,
407            default: None,
408        }
409    }
410
411    /// Sets the description.
412    pub fn with_description(mut self, description: impl Into<String>) -> Self {
413        self.description = Some(description.into());
414        self
415    }
416
417    /// Sets the default value.
418    pub fn with_default(mut self, default: PluginValue) -> Self {
419        self.default = Some(default);
420        self
421    }
422}
423
424/// Property type.
425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
426pub enum PropertyType {
427    /// String type.
428    String,
429    /// Boolean type.
430    Boolean,
431    /// Integer type.
432    Integer,
433    /// Float type.
434    Float,
435    /// Array type.
436    Array,
437    /// Object type.
438    Object,
439    /// Any type.
440    Any,
441}
442
443/// Schema for a data source.
444#[derive(Debug, Clone, Default, Serialize, Deserialize)]
445pub struct DataSourceSchema {
446    /// Data source name.
447    pub name: String,
448    /// Description.
449    pub description: String,
450    /// Supports polling.
451    pub supports_polling: bool,
452    /// Supports streaming.
453    pub supports_streaming: bool,
454    /// Configuration properties.
455    pub properties: HashMap<String, PropertySchema>,
456}
457
458impl DataSourceSchema {
459    /// Creates a new data source schema.
460    pub fn new(name: impl Into<String>) -> Self {
461        Self {
462            name: name.into(),
463            ..Default::default()
464        }
465    }
466}
467
468/// Schema for a transform.
469#[derive(Debug, Clone, Default, Serialize, Deserialize)]
470pub struct TransformSchema {
471    /// Transform name.
472    pub name: String,
473    /// Description.
474    pub description: String,
475    /// Configuration properties.
476    pub properties: HashMap<String, PropertySchema>,
477}
478
479impl TransformSchema {
480    /// Creates a new transform schema.
481    pub fn new(name: impl Into<String>) -> Self {
482        Self {
483            name: name.into(),
484            ..Default::default()
485        }
486    }
487}
488
489/// Schema for an action.
490#[derive(Debug, Clone, Default, Serialize, Deserialize)]
491pub struct ActionSchema {
492    /// Action name.
493    pub name: String,
494    /// Description.
495    pub description: String,
496    /// Whether action is async.
497    pub is_async: bool,
498    /// Configuration properties.
499    pub properties: HashMap<String, PropertySchema>,
500}
501
502impl ActionSchema {
503    /// Creates a new action schema.
504    pub fn new(name: impl Into<String>) -> Self {
505        Self {
506            name: name.into(),
507            ..Default::default()
508        }
509    }
510}
511
512/// Plugin entry point function type.
513///
514/// # Safety
515///
516/// This type is the signature for native plugin entry points loaded via
517/// `dlopen`/`LoadLibrary`. The following invariants must hold:
518///
519/// - **ABI compatibility**: The plugin must be compiled with the same Rust
520///   compiler version and the same `nemo-plugin-api` crate version as the
521///   host. Mismatched versions cause undefined behaviour due to differing
522///   type layouts.
523/// - **Single-threaded call**: The host calls this function on the main
524///   thread. The `PluginRegistrar` reference is valid only for the duration
525///   of the call and must not be stored or sent to other threads.
526/// - **No unwinding**: The entry function must not panic. A panic across
527///   the FFI boundary is undefined behaviour. Use `catch_unwind` internally
528///   if necessary.
529/// - **Library lifetime**: The dynamic library must remain loaded for as
530///   long as any symbols (vtables, function pointers) obtained through the
531///   registrar are in use.
532/// - **No re-entrancy**: The entry function must not call back into the
533///   host's plugin loading machinery.
534#[allow(improper_ctypes_definitions)]
535pub type PluginEntryFn = unsafe extern "C" fn(&mut dyn PluginRegistrar);
536
537/// Macro to declare a plugin entry point.
538///
539/// Generates two `extern "C"` functions:
540/// - `nemo_plugin_manifest() -> PluginManifest` — returns the plugin descriptor
541/// - `nemo_plugin_entry(registrar: &mut dyn PluginRegistrar)` — called to
542///   register capabilities
543///
544/// # Example
545///
546/// ```rust,no_run
547/// # use nemo_plugin_api::*;
548/// # use semver::Version;
549/// declare_plugin!(
550///     PluginManifest::new("hello", "Hello Plugin", Version::new(1, 0, 0))
551///         .with_description("Greets the user"),
552///     |registrar: &mut dyn PluginRegistrar| {
553///         registrar.context().log(LogLevel::Info, "Hello from plugin!");
554///     }
555/// );
556/// ```
557#[macro_export]
558macro_rules! declare_plugin {
559    ($manifest:expr, $init:expr) => {
560        #[no_mangle]
561        pub extern "C" fn nemo_plugin_manifest() -> $crate::PluginManifest {
562            $manifest
563        }
564
565        #[no_mangle]
566        pub extern "C" fn nemo_plugin_entry(registrar: &mut dyn $crate::PluginRegistrar) {
567            $init(registrar)
568        }
569    };
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575
576    #[test]
577    fn test_plugin_manifest() {
578        let manifest = PluginManifest::new("test-plugin", "Test Plugin", Version::new(1, 0, 0))
579            .with_description("A test plugin")
580            .with_capability(Capability::Component("my-component".into()));
581
582        assert_eq!(manifest.id, "test-plugin");
583        assert_eq!(manifest.capabilities.len(), 1);
584    }
585
586    #[test]
587    fn test_component_schema() {
588        let schema = ComponentSchema::new("button")
589            .with_description("A button component")
590            .with_property("label", PropertySchema::string())
591            .require("label");
592
593        assert!(schema.required.contains(&"label".to_string()));
594    }
595
596    #[test]
597    fn test_plugin_value() {
598        let value = PluginValue::Object(IndexMap::from([
599            ("name".to_string(), PluginValue::String("test".to_string())),
600            ("count".to_string(), PluginValue::Integer(42)),
601        ]));
602
603        if let PluginValue::Object(obj) = value {
604            assert!(obj.contains_key("name"));
605        }
606    }
607}