rustbridge_core/plugin.rs
1//! Plugin trait and context types
2
3use crate::{LifecycleState, PluginConfig, PluginError, PluginResult};
4use async_trait::async_trait;
5
6/// Context provided to plugin operations
7pub struct PluginContext {
8 /// Plugin configuration
9 pub config: PluginConfig,
10 /// Current lifecycle state
11 state: std::sync::atomic::AtomicU8,
12}
13
14impl PluginContext {
15 /// Create a new plugin context
16 pub fn new(config: PluginConfig) -> Self {
17 Self {
18 config,
19 state: std::sync::atomic::AtomicU8::new(LifecycleState::Installed as u8),
20 }
21 }
22
23 /// Get current lifecycle state
24 pub fn state(&self) -> LifecycleState {
25 let value = self.state.load(std::sync::atomic::Ordering::SeqCst);
26 match value {
27 0 => LifecycleState::Installed,
28 1 => LifecycleState::Starting,
29 2 => LifecycleState::Active,
30 3 => LifecycleState::Stopping,
31 4 => LifecycleState::Stopped,
32 _ => LifecycleState::Failed,
33 }
34 }
35
36 /// Set lifecycle state directly (bypassing transition validation)
37 ///
38 /// Use this for error recovery scenarios where normal transitions don't apply.
39 pub fn set_state(&self, state: LifecycleState) {
40 let value = match state {
41 LifecycleState::Installed => 0,
42 LifecycleState::Starting => 1,
43 LifecycleState::Active => 2,
44 LifecycleState::Stopping => 3,
45 LifecycleState::Stopped => 4,
46 LifecycleState::Failed => 5,
47 };
48 self.state.store(value, std::sync::atomic::Ordering::SeqCst);
49 }
50
51 /// Attempt to transition to a new state
52 pub fn transition_to(&self, target: LifecycleState) -> PluginResult<()> {
53 let current = self.state();
54 if current.can_transition_to(target) {
55 self.set_state(target);
56 Ok(())
57 } else {
58 Err(PluginError::InvalidState {
59 expected: format!("state that can transition to {}", target),
60 actual: current.to_string(),
61 })
62 }
63 }
64}
65
66/// Main trait for implementing rustbridge plugins
67///
68/// Plugins must implement this trait to define their behavior.
69/// The async methods are executed on the Tokio runtime.
70///
71/// # Example
72///
73/// ```ignore
74/// use rustbridge_core::prelude::*;
75///
76/// struct MyPlugin;
77///
78/// #[async_trait::async_trait]
79/// impl Plugin for MyPlugin {
80/// async fn on_start(&self, ctx: &PluginContext) -> PluginResult<()> {
81/// // Initialize resources
82/// Ok(())
83/// }
84///
85/// async fn handle_request(
86/// &self,
87/// ctx: &PluginContext,
88/// type_tag: &str,
89/// payload: &[u8],
90/// ) -> PluginResult<Vec<u8>> {
91/// match type_tag {
92/// "echo" => Ok(payload.to_vec()),
93/// _ => Err(PluginError::UnknownMessageType(type_tag.to_string())),
94/// }
95/// }
96///
97/// async fn on_stop(&self, ctx: &PluginContext) -> PluginResult<()> {
98/// // Cleanup resources
99/// Ok(())
100/// }
101/// }
102/// ```
103#[async_trait]
104pub trait Plugin: Send + Sync + 'static {
105 /// Called when the plugin is starting up
106 ///
107 /// Use this to initialize resources, connections, etc.
108 /// The plugin transitions to Active state after this returns successfully.
109 async fn on_start(&self, ctx: &PluginContext) -> PluginResult<()>;
110
111 /// Handle an incoming request
112 ///
113 /// - `type_tag`: Message type identifier (e.g., "user.create")
114 /// - `payload`: JSON-encoded request payload
115 ///
116 /// Returns JSON-encoded response payload
117 async fn handle_request(
118 &self,
119 ctx: &PluginContext,
120 type_tag: &str,
121 payload: &[u8],
122 ) -> PluginResult<Vec<u8>>;
123
124 /// Called when the plugin is shutting down
125 ///
126 /// Use this to cleanup resources, close connections, etc.
127 /// The plugin transitions to Stopped state after this returns.
128 async fn on_stop(&self, ctx: &PluginContext) -> PluginResult<()>;
129
130 /// Get plugin metadata
131 ///
132 /// Override this to provide plugin information
133 fn metadata(&self) -> Option<crate::PluginMetadata> {
134 None
135 }
136
137 /// List supported message types
138 ///
139 /// Override this to provide a list of supported type tags
140 fn supported_types(&self) -> Vec<&'static str> {
141 Vec::new()
142 }
143}
144
145/// Factory trait for creating plugins with optional configuration.
146///
147/// The framework calls `create()` which handles the null-config case
148/// automatically by falling back to `Default::default()`.
149///
150/// Plugin authors only need to override `create_configured()` if they
151/// want to use config.data for initialization.
152///
153/// # Example
154///
155/// ```ignore
156/// use rustbridge_core::prelude::*;
157///
158/// struct MyPlugin {
159/// cache_size: usize,
160/// }
161///
162/// impl Default for MyPlugin {
163/// fn default() -> Self {
164/// Self { cache_size: 100 }
165/// }
166/// }
167///
168/// impl PluginFactory for MyPlugin {
169/// fn create_configured(config: &PluginConfig) -> PluginResult<Self> {
170/// let size = config.get::<usize>("cache_size").unwrap_or(100);
171/// Ok(Self { cache_size: size })
172/// }
173/// }
174/// ```
175pub trait PluginFactory: Default + Plugin + Sized {
176 /// Create a plugin instance. Called by the framework.
177 ///
178 /// Default implementation: uses `Default` when config.data is null,
179 /// otherwise calls `create_configured()`.
180 fn create(config: &PluginConfig) -> PluginResult<Self> {
181 if config.data.is_null() {
182 Ok(Self::default())
183 } else {
184 Self::create_configured(config)
185 }
186 }
187
188 /// Create a plugin with non-null config.data.
189 ///
190 /// Override this to parse and use configuration data.
191 /// Default implementation ignores config and uses `Default`.
192 fn create_configured(_config: &PluginConfig) -> PluginResult<Self> {
193 Ok(Self::default())
194 }
195}
196
197#[cfg(test)]
198#[path = "plugin/plugin_tests.rs"]
199mod plugin_tests;