cypherlite_core/plugin/mod.rs
1//! Plugin system core types and traits.
2//!
3//! Defines the base `Plugin` trait, four extension traits
4//! (`ScalarFunction`, `IndexPlugin`, `Serializer`, `Trigger`),
5//! and a generic `PluginRegistry` for managing plugin instances.
6
7use std::collections::HashMap;
8
9use crate::error::CypherLiteError;
10use crate::types::{NodeId, PropertyValue};
11
12// Re-export trigger types from this module for backward compatibility.
13// The canonical definitions live in crate::trigger_types (always available).
14pub use crate::trigger_types::{EntityType, TriggerContext, TriggerOperation};
15
16// ---------------------------------------------------------------------------
17// Base trait
18// ---------------------------------------------------------------------------
19
20/// Base trait that every plugin must implement.
21///
22/// The trait is object-safe and requires `Send + Sync` so that plugin
23/// instances can be shared across threads.
24pub trait Plugin: Send + Sync {
25 /// Returns the unique name of this plugin.
26 fn name(&self) -> &str;
27
28 /// Returns the version string (e.g. `"1.2.3"`).
29 fn version(&self) -> &str;
30}
31
32// ---------------------------------------------------------------------------
33// Extension traits
34// ---------------------------------------------------------------------------
35
36/// A user-defined scalar function callable from Cypher queries.
37///
38/// Accepts zero or more [`PropertyValue`] arguments and returns a single
39/// [`PropertyValue`].
40pub trait ScalarFunction: Plugin {
41 /// Execute the function with the given arguments.
42 fn call(&self, args: &[PropertyValue]) -> Result<PropertyValue, CypherLiteError>;
43}
44
45/// A custom index implementation that can be plugged into the storage layer.
46pub trait IndexPlugin: Plugin {
47 /// Returns the identifier for this index type (e.g. `"btree"`, `"rtree"`).
48 fn index_type(&self) -> &str;
49
50 /// Insert a `(key, node_id)` entry into the index.
51 fn insert(&mut self, key: &PropertyValue, node_id: NodeId) -> Result<(), CypherLiteError>;
52
53 /// Remove a `(key, node_id)` entry from the index.
54 fn remove(&mut self, key: &PropertyValue, node_id: NodeId) -> Result<(), CypherLiteError>;
55
56 /// Look up all node IDs associated with the given key.
57 fn lookup(&self, key: &PropertyValue) -> Result<Vec<NodeId>, CypherLiteError>;
58}
59
60/// A custom serialization format for import/export operations.
61///
62/// Uses `HashMap<String, PropertyValue>` as the row representation to avoid
63/// a circular dependency on the query crate's `Row` type.
64pub trait Serializer: Plugin {
65 /// Returns the format identifier (e.g. `"json"`, `"csv"`).
66 fn format(&self) -> &str;
67
68 /// Serialize a slice of rows into bytes.
69 fn export(&self, data: &[HashMap<String, PropertyValue>]) -> Result<Vec<u8>, CypherLiteError>;
70
71 /// Deserialize bytes into a vector of rows.
72 fn import(&self, bytes: &[u8]) -> Result<Vec<HashMap<String, PropertyValue>>, CypherLiteError>;
73}
74
75/// A trigger that fires before/after create, update, and delete operations.
76pub trait Trigger: Plugin {
77 /// Called before a new entity is created.
78 fn on_before_create(&self, ctx: &TriggerContext) -> Result<(), CypherLiteError>;
79
80 /// Called after a new entity has been created.
81 fn on_after_create(&self, ctx: &TriggerContext) -> Result<(), CypherLiteError>;
82
83 /// Called before an existing entity is updated.
84 fn on_before_update(&self, ctx: &TriggerContext) -> Result<(), CypherLiteError>;
85
86 /// Called after an existing entity has been updated.
87 fn on_after_update(&self, ctx: &TriggerContext) -> Result<(), CypherLiteError>;
88
89 /// Called before an entity is deleted.
90 fn on_before_delete(&self, ctx: &TriggerContext) -> Result<(), CypherLiteError>;
91
92 /// Called after an entity has been deleted.
93 fn on_after_delete(&self, ctx: &TriggerContext) -> Result<(), CypherLiteError>;
94}
95
96// ---------------------------------------------------------------------------
97// Supporting types (re-exported from crate::trigger_types)
98// ---------------------------------------------------------------------------
99
100// ---------------------------------------------------------------------------
101// Plugin registry
102// ---------------------------------------------------------------------------
103
104/// A type-safe registry that stores and retrieves plugin instances by name.
105///
106/// `T` is typically a trait object such as `dyn Plugin`, `dyn ScalarFunction`,
107/// or any other extension trait.
108pub struct PluginRegistry<T: Plugin + ?Sized> {
109 plugins: HashMap<String, Box<T>>,
110}
111
112impl<T: Plugin + ?Sized> PluginRegistry<T> {
113 /// Create a new, empty registry.
114 pub fn new() -> Self {
115 Self {
116 plugins: HashMap::new(),
117 }
118 }
119
120 /// Register a plugin. Returns an error if a plugin with the same name is
121 /// already registered.
122 pub fn register(&mut self, plugin: Box<T>) -> Result<(), CypherLiteError> {
123 let name = plugin.name().to_string();
124 if self.plugins.contains_key(&name) {
125 return Err(CypherLiteError::PluginError(format!(
126 "Plugin already registered: {name}"
127 )));
128 }
129 self.plugins.insert(name, plugin);
130 Ok(())
131 }
132
133 /// Get an immutable reference to a plugin by name.
134 pub fn get(&self, name: &str) -> Option<&T> {
135 self.plugins.get(name).map(|b| b.as_ref())
136 }
137
138 /// Get a mutable reference to a plugin by name.
139 pub fn get_mut(&mut self, name: &str) -> Option<&mut T> {
140 self.plugins.get_mut(name).map(|b| b.as_mut())
141 }
142
143 /// Returns an iterator over the names of all registered plugins.
144 pub fn list(&self) -> impl Iterator<Item = &str> {
145 self.plugins.keys().map(|s| s.as_str())
146 }
147
148 /// Check whether a plugin with the given name is registered.
149 pub fn contains(&self, name: &str) -> bool {
150 self.plugins.contains_key(name)
151 }
152}
153
154impl<T: Plugin + ?Sized> Default for PluginRegistry<T> {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 // Verify EntityType and TriggerOperation derive Debug
165 #[test]
166 fn test_entity_type_debug() {
167 assert_eq!(format!("{:?}", EntityType::Node), "Node");
168 assert_eq!(format!("{:?}", EntityType::Edge), "Edge");
169 }
170
171 #[test]
172 fn test_trigger_operation_debug() {
173 assert_eq!(format!("{:?}", TriggerOperation::Create), "Create");
174 assert_eq!(format!("{:?}", TriggerOperation::Update), "Update");
175 assert_eq!(format!("{:?}", TriggerOperation::Delete), "Delete");
176 }
177
178 #[test]
179 fn test_trigger_context_debug() {
180 let ctx = TriggerContext {
181 entity_type: EntityType::Node,
182 entity_id: 1,
183 label_or_type: None,
184 properties: HashMap::new(),
185 operation: TriggerOperation::Delete,
186 };
187 let debug_str = format!("{ctx:?}");
188 assert!(debug_str.contains("Node"));
189 assert!(debug_str.contains("Delete"));
190 }
191}