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}