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}