Skip to main content

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}