Skip to main content

plexus_core/plexus/
hub_context.rs

1//! HubContext - generic parent context injection for hub-aware plugins
2//!
3//! This module defines the `HubContext` trait that allows plugins to receive
4//! typed context from their parent hub. The pattern enables:
5//!
6//! - Symmetric context passing: every hub passes something to its children
7//! - Generic plugins: `Plugin<P: HubContext>` works with any parent type
8//! - Type safety: children know what capabilities their parent provides
9//!
10//! # Example
11//!
12//! ```ignore
13//! // A plugin generic over its parent context
14//! pub struct Cone<P: HubContext> {
15//!     parent: Arc<OnceLock<P>>,
16//!     storage: Arc<ConeStorage>,
17//! }
18//!
19//! impl<P: HubContext> Cone<P> {
20//!     pub fn inject_parent(&self, parent: P) {
21//!         let _ = self.parent.set(parent);
22//!     }
23//!
24//!     async fn resolve_foreign_handle(&self, handle: &Handle) -> Option<Value> {
25//!         self.parent.get()?.resolve_handle(handle).await
26//!     }
27//! }
28//!
29//! // Plexus injects itself (as Weak<Plexus>)
30//! let cone: Cone<Weak<Plexus>> = Cone::new();
31//! cone.inject_parent(Arc::downgrade(&plexus));
32//!
33//! // A sub-hub could inject its own context
34//! let widget: Widget<DashboardContext> = Widget::new();
35//! widget.inject_parent(dashboard_ctx);
36//! ```
37
38use crate::plexus::streaming::PlexusStream;
39use crate::plexus::PlexusError;
40use crate::types::Handle;
41use async_trait::async_trait;
42
43/// Trait for parent context that can be injected into child plugins.
44///
45/// This trait defines the capabilities a child plugin expects from its parent.
46/// Any hub that wants to inject context into children should implement this trait
47/// (or provide a type that implements it).
48///
49/// The trait is object-safe to allow dynamic dispatch when needed, though
50/// most usage will be through generic bounds.
51#[async_trait]
52pub trait HubContext: Clone + Send + Sync + 'static {
53    /// Resolve a handle through the parent's plugin registry.
54    ///
55    /// This allows children to resolve handles from sibling plugins without
56    /// knowing about them directly - they just ask the parent to route it.
57    async fn resolve_handle(&self, handle: &Handle) -> Result<PlexusStream, PlexusError>;
58
59    /// Route a method call through the parent.
60    ///
61    /// Enables children to call methods on sibling plugins via the parent hub.
62    async fn call(&self, method: &str, params: serde_json::Value) -> Result<PlexusStream, PlexusError>;
63
64    /// Check if the parent context is still valid.
65    ///
66    /// For weak references, this checks if the parent is still alive.
67    /// For strong references or owned contexts, this always returns true.
68    fn is_valid(&self) -> bool {
69        true
70    }
71}
72
73/// Marker trait for plugins that accept parent context injection.
74///
75/// Plugins that implement this trait can receive context from their parent hub
76/// after construction. The associated `Parent` type specifies what kind of
77/// context the plugin expects.
78pub trait ParentAware {
79    /// The type of parent context this plugin expects.
80    type Parent: HubContext;
81
82    /// Inject the parent context.
83    ///
84    /// Called by the parent hub after plugin construction.
85    /// Typically stores the context in an `OnceLock` for later use.
86    fn inject_parent(&self, parent: Self::Parent);
87
88    /// Check if parent has been injected.
89    fn has_parent(&self) -> bool;
90}
91
92/// A no-op context for plugins that don't need parent access.
93///
94/// This allows plugins to be instantiated without a parent, useful for
95/// testing or standalone operation.
96#[derive(Clone, Debug, Default)]
97pub struct NoParent;
98
99#[async_trait]
100impl HubContext for NoParent {
101    async fn resolve_handle(&self, _handle: &Handle) -> Result<PlexusStream, PlexusError> {
102        Err(PlexusError::ExecutionError(
103            "No parent context available for handle resolution".to_string(),
104        ))
105    }
106
107    async fn call(&self, method: &str, _params: serde_json::Value) -> Result<PlexusStream, PlexusError> {
108        Err(PlexusError::ExecutionError(format!(
109            "No parent context available to route call: {}",
110            method
111        )))
112    }
113
114    fn is_valid(&self) -> bool {
115        false
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn no_parent_is_invalid() {
125        let ctx = NoParent;
126        assert!(!ctx.is_valid());
127    }
128
129    #[tokio::test]
130    async fn no_parent_resolve_fails() {
131        let ctx = NoParent;
132        let handle = Handle::new(uuid::Uuid::new_v4(), "1.0.0", "test");
133        let result = ctx.resolve_handle(&handle).await;
134        assert!(result.is_err());
135    }
136}