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}