Skip to main content

drasi_lib/context/
mod.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Runtime context types for plugin service injection.
16//!
17//! This module provides `SourceRuntimeContext` and `ReactionRuntimeContext` structs
18//! that contain `Arc<T>` instances for drasi-lib provided services. These contexts
19//! are provided to plugins when they are added to DrasiLib via the `initialize()` method.
20//!
21//! # Overview
22//!
23//! Instead of multiple `inject_*` methods, plugins now receive all services through
24//! a single `initialize(context)` call. This provides:
25//!
26//! - **Single initialization point**: One call instead of multiple inject calls
27//! - **Guaranteed complete initialization**: Context is complete or doesn't exist
28//! - **Clearer API contract**: All available services visible at a glance
29//! - **Extensibility**: New services can be added without changing traits
30//!
31//! # Example - Source Plugin
32//!
33//! ```ignore
34//! use drasi_lib::context::SourceRuntimeContext;
35//!
36//! #[async_trait]
37//! impl Source for MySource {
38//!     async fn initialize(&self, context: SourceRuntimeContext) {
39//!         // Store context for later use
40//!         self.base.initialize(context).await;
41//!     }
42//!     // ...
43//! }
44//! ```
45//!
46//! # Example - Reaction Plugin
47//!
48//! ```ignore
49//! use drasi_lib::context::ReactionRuntimeContext;
50//!
51//! #[async_trait]
52//! impl Reaction for MyReaction {
53//!     async fn initialize(&self, context: ReactionRuntimeContext) {
54//!         // Store context for later use
55//!         self.base.initialize(context).await;
56//!     }
57//!     // ...
58//! }
59//! ```
60
61use std::sync::Arc;
62
63use crate::component_graph::ComponentUpdateSender;
64use crate::identity::IdentityProvider;
65use crate::state_store::StateStoreProvider;
66
67/// Context provided to Source plugins during initialization.
68///
69/// Contains `Arc<T>` instances for all drasi-lib provided services.
70/// DrasiLib constructs this context when a source is added via `add_source()`.
71///
72/// # Available Services
73///
74/// - `instance_id`: The DrasiLib instance ID (for log routing isolation)
75/// - `source_id`: The unique identifier for this source instance
76/// - `state_store`: Optional persistent state storage (if configured)
77/// - `update_tx`: mpsc sender for fire-and-forget status updates to the component graph
78///
79/// # Clone
80///
81/// This struct implements `Clone` and all fields use `Arc` internally,
82/// making cloning cheap (just reference count increments).
83#[derive(Clone)]
84pub struct SourceRuntimeContext {
85    /// DrasiLib instance ID (for log routing isolation)
86    pub instance_id: String,
87
88    /// Unique identifier for this source instance
89    pub source_id: String,
90
91    /// Optional persistent state storage.
92    ///
93    /// This is `Some` if a state store provider was configured on DrasiLib,
94    /// otherwise `None`. Sources can use this to persist state across restarts.
95    pub state_store: Option<Arc<dyn StateStoreProvider>>,
96
97    /// mpsc sender for fire-and-forget component status updates.
98    ///
99    /// Status changes sent here are applied to the component graph by the
100    /// graph update loop, which emits broadcast events to all subscribers.
101    pub update_tx: ComponentUpdateSender,
102
103    /// Optional identity provider for credential injection.
104    ///
105    /// This is `Some` if the host has configured an identity provider for this component.
106    /// Sources can use this to obtain authentication credentials (passwords, tokens,
107    /// certificates) for connecting to external systems.
108    pub identity_provider: Option<Arc<dyn IdentityProvider>>,
109}
110
111impl SourceRuntimeContext {
112    /// Create a new source runtime context.
113    ///
114    /// This is typically called by `SourceManager` when adding a source to DrasiLib.
115    /// Plugin developers do not need to call this directly.
116    ///
117    /// # Arguments
118    ///
119    /// * `instance_id` - The DrasiLib instance ID
120    /// * `source_id` - The unique identifier for this source
121    /// * `state_store` - Optional persistent state storage
122    /// * `update_tx` - mpsc sender for status updates to the component graph
123    /// * `identity_provider` - Optional identity provider for credential injection
124    pub fn new(
125        instance_id: impl Into<String>,
126        source_id: impl Into<String>,
127        state_store: Option<Arc<dyn StateStoreProvider>>,
128        update_tx: ComponentUpdateSender,
129        identity_provider: Option<Arc<dyn IdentityProvider>>,
130    ) -> Self {
131        Self {
132            instance_id: instance_id.into(),
133            source_id: source_id.into(),
134            state_store,
135            update_tx,
136            identity_provider,
137        }
138    }
139
140    /// Get the DrasiLib instance ID.
141    pub fn instance_id(&self) -> &str {
142        &self.instance_id
143    }
144
145    /// Get the source's unique identifier.
146    pub fn source_id(&self) -> &str {
147        &self.source_id
148    }
149
150    /// Get a reference to the state store if configured.
151    ///
152    /// Returns `Some(&Arc<dyn StateStoreProvider>)` if a state store was configured,
153    /// otherwise `None`.
154    pub fn state_store(&self) -> Option<&Arc<dyn StateStoreProvider>> {
155        self.state_store.as_ref()
156    }
157}
158
159impl std::fmt::Debug for SourceRuntimeContext {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        f.debug_struct("SourceRuntimeContext")
162            .field("instance_id", &self.instance_id)
163            .field("source_id", &self.source_id)
164            .field(
165                "state_store",
166                &self.state_store.as_ref().map(|_| "<StateStoreProvider>"),
167            )
168            .field("update_tx", &"<ComponentUpdateSender>")
169            .field(
170                "identity_provider",
171                &self
172                    .identity_provider
173                    .as_ref()
174                    .map(|_| "<IdentityProvider>"),
175            )
176            .finish()
177    }
178}
179
180/// Context provided to Reaction plugins during initialization.
181///
182/// Contains `Arc<T>` instances for all drasi-lib provided services.
183/// DrasiLib constructs this context when a reaction is added via `add_reaction()`.
184///
185/// # Available Services
186///
187/// - `instance_id`: The DrasiLib instance ID (for log routing isolation)
188/// - `reaction_id`: The unique identifier for this reaction instance
189/// - `state_store`: Optional persistent state storage (if configured)
190/// - `update_tx`: mpsc sender for fire-and-forget status updates to the component graph
191/// - `identity_provider`: Optional identity provider for credential injection
192///
193/// # Clone
194///
195/// This struct implements `Clone` and all fields use `Arc` internally,
196/// making cloning cheap (just reference count increments).
197#[derive(Clone)]
198pub struct ReactionRuntimeContext {
199    /// DrasiLib instance ID (for log routing isolation)
200    pub instance_id: String,
201
202    /// Unique identifier for this reaction instance
203    pub reaction_id: String,
204
205    /// Optional persistent state storage.
206    ///
207    /// This is `Some` if a state store provider was configured on DrasiLib,
208    /// otherwise `None`. Reactions can use this to persist state across restarts.
209    pub state_store: Option<Arc<dyn StateStoreProvider>>,
210
211    /// mpsc sender for fire-and-forget component status updates.
212    ///
213    /// Status changes sent here are applied to the component graph by the
214    /// graph update loop, which emits broadcast events to all subscribers.
215    pub update_tx: ComponentUpdateSender,
216
217    /// Optional identity provider for credential injection.
218    ///
219    /// This is `Some` if the host has configured an identity provider for this component.
220    /// Reactions can use this to obtain authentication credentials (passwords, tokens,
221    /// certificates) for connecting to external systems.
222    pub identity_provider: Option<Arc<dyn IdentityProvider>>,
223}
224
225impl ReactionRuntimeContext {
226    /// Create a new reaction runtime context.
227    ///
228    /// This is typically called by `ReactionManager` when adding a reaction to DrasiLib.
229    /// Plugin developers do not need to call this directly.
230    ///
231    /// # Arguments
232    ///
233    /// * `instance_id` - The DrasiLib instance ID
234    /// * `reaction_id` - The unique identifier for this reaction
235    /// * `state_store` - Optional persistent state storage
236    /// * `update_tx` - mpsc sender for status updates to the component graph
237    /// * `identity_provider` - Optional identity provider for credential injection
238    pub fn new(
239        instance_id: impl Into<String>,
240        reaction_id: impl Into<String>,
241        state_store: Option<Arc<dyn StateStoreProvider>>,
242        update_tx: ComponentUpdateSender,
243        identity_provider: Option<Arc<dyn IdentityProvider>>,
244    ) -> Self {
245        Self {
246            instance_id: instance_id.into(),
247            reaction_id: reaction_id.into(),
248            state_store,
249            update_tx,
250            identity_provider,
251        }
252    }
253
254    /// Get the DrasiLib instance ID.
255    pub fn instance_id(&self) -> &str {
256        &self.instance_id
257    }
258
259    /// Get the reaction's unique identifier.
260    pub fn reaction_id(&self) -> &str {
261        &self.reaction_id
262    }
263
264    /// Get a reference to the state store if configured.
265    pub fn state_store(&self) -> Option<&Arc<dyn StateStoreProvider>> {
266        self.state_store.as_ref()
267    }
268}
269
270impl std::fmt::Debug for ReactionRuntimeContext {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        f.debug_struct("ReactionRuntimeContext")
273            .field("instance_id", &self.instance_id)
274            .field("reaction_id", &self.reaction_id)
275            .field(
276                "state_store",
277                &self.state_store.as_ref().map(|_| "<StateStoreProvider>"),
278            )
279            .field("update_tx", &"<ComponentUpdateSender>")
280            .field(
281                "identity_provider",
282                &self
283                    .identity_provider
284                    .as_ref()
285                    .map(|_| "<IdentityProvider>"),
286            )
287            .finish()
288    }
289}
290
291/// Context provided to Query components during initialization.
292///
293/// Contains the DrasiLib instance ID and update channel for status reporting.
294/// Constructed by `QueryManager` when a query is added via `add_query()`.
295///
296/// Unlike sources and reactions, queries are internal to drasi-lib (not plugins),
297/// but still follow the same context-based initialization pattern for consistency.
298///
299/// # Clone
300///
301/// This struct implements `Clone` and uses `Arc` internally for the update channel,
302/// making cloning cheap (just reference count increments).
303#[derive(Clone)]
304pub struct QueryRuntimeContext {
305    /// DrasiLib instance ID (for log routing isolation)
306    pub instance_id: String,
307
308    /// Unique identifier for this query instance
309    pub query_id: String,
310
311    /// mpsc sender for fire-and-forget component status updates.
312    ///
313    /// Status changes sent here are applied to the component graph by the
314    /// graph update loop, which emits broadcast events to all subscribers.
315    pub update_tx: ComponentUpdateSender,
316}
317
318impl QueryRuntimeContext {
319    /// Create a new query runtime context.
320    ///
321    /// This is typically called by `QueryManager` when adding a query to DrasiLib.
322    ///
323    /// # Arguments
324    ///
325    /// * `instance_id` - The DrasiLib instance ID
326    /// * `query_id` - The unique identifier for this query
327    /// * `update_tx` - mpsc sender for status updates to the component graph
328    pub fn new(
329        instance_id: impl Into<String>,
330        query_id: impl Into<String>,
331        update_tx: ComponentUpdateSender,
332    ) -> Self {
333        Self {
334            instance_id: instance_id.into(),
335            query_id: query_id.into(),
336            update_tx,
337        }
338    }
339
340    /// Get the DrasiLib instance ID.
341    pub fn instance_id(&self) -> &str {
342        &self.instance_id
343    }
344
345    /// Get the query's unique identifier.
346    pub fn query_id(&self) -> &str {
347        &self.query_id
348    }
349}
350
351impl std::fmt::Debug for QueryRuntimeContext {
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        f.debug_struct("QueryRuntimeContext")
354            .field("instance_id", &self.instance_id)
355            .field("query_id", &self.query_id)
356            .field("update_tx", &"<ComponentUpdateSender>")
357            .finish()
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364    use crate::component_graph::ComponentGraph;
365    use crate::state_store::MemoryStateStoreProvider;
366    use std::sync::Arc;
367
368    fn test_update_tx() -> ComponentUpdateSender {
369        let (graph, _rx) = ComponentGraph::new("test-instance");
370        graph.update_sender()
371    }
372
373    #[tokio::test]
374    async fn test_source_runtime_context_creation() {
375        let state_store = Arc::new(MemoryStateStoreProvider::new());
376        let update_tx = test_update_tx();
377
378        let context = SourceRuntimeContext::new(
379            "test-instance",
380            "test-source",
381            Some(state_store),
382            update_tx,
383            None,
384        );
385
386        assert_eq!(context.instance_id(), "test-instance");
387        assert_eq!(context.source_id(), "test-source");
388        assert!(context.state_store().is_some());
389    }
390
391    #[tokio::test]
392    async fn test_source_runtime_context_without_state_store() {
393        let update_tx = test_update_tx();
394
395        let context =
396            SourceRuntimeContext::new("test-instance", "test-source", None, update_tx, None);
397
398        assert_eq!(context.source_id(), "test-source");
399        assert!(context.state_store().is_none());
400    }
401
402    #[tokio::test]
403    async fn test_source_runtime_context_clone() {
404        let state_store = Arc::new(MemoryStateStoreProvider::new());
405        let update_tx = test_update_tx();
406
407        let context = SourceRuntimeContext::new(
408            "test-instance",
409            "test-source",
410            Some(state_store),
411            update_tx,
412            None,
413        );
414
415        let cloned = context.clone();
416        assert_eq!(cloned.source_id(), context.source_id());
417    }
418
419    #[tokio::test]
420    async fn test_reaction_runtime_context_creation() {
421        let state_store = Arc::new(MemoryStateStoreProvider::new());
422        let update_tx = test_update_tx();
423
424        let context = ReactionRuntimeContext::new(
425            "test-instance",
426            "test-reaction",
427            Some(state_store),
428            update_tx,
429            None,
430        );
431
432        assert_eq!(context.instance_id(), "test-instance");
433        assert_eq!(context.reaction_id(), "test-reaction");
434        assert!(context.state_store().is_some());
435    }
436
437    #[tokio::test]
438    async fn test_reaction_runtime_context_without_state_store() {
439        let update_tx = test_update_tx();
440
441        let context =
442            ReactionRuntimeContext::new("test-instance", "test-reaction", None, update_tx, None);
443
444        assert_eq!(context.reaction_id(), "test-reaction");
445        assert!(context.state_store().is_none());
446    }
447
448    #[tokio::test]
449    async fn test_reaction_runtime_context_clone() {
450        let state_store = Arc::new(MemoryStateStoreProvider::new());
451        let update_tx = test_update_tx();
452
453        let context = ReactionRuntimeContext::new(
454            "test-instance",
455            "test-reaction",
456            Some(state_store),
457            update_tx,
458            None,
459        );
460
461        let cloned = context.clone();
462        assert_eq!(cloned.reaction_id(), context.reaction_id());
463    }
464
465    #[test]
466    fn test_source_runtime_context_debug() {
467        let update_tx = test_update_tx();
468        let context = SourceRuntimeContext::new("test-instance", "test", None, update_tx, None);
469        let debug_str = format!("{context:?}");
470        assert!(debug_str.contains("SourceRuntimeContext"));
471        assert!(debug_str.contains("test"));
472    }
473
474    #[test]
475    fn test_reaction_runtime_context_debug() {
476        let update_tx = test_update_tx();
477        let context = ReactionRuntimeContext::new("test-instance", "test", None, update_tx, None);
478        let debug_str = format!("{context:?}");
479        assert!(debug_str.contains("ReactionRuntimeContext"));
480        assert!(debug_str.contains("test"));
481    }
482
483    #[tokio::test]
484    async fn test_query_runtime_context_creation() {
485        let update_tx = test_update_tx();
486        let context = QueryRuntimeContext::new("test-instance", "test-query", update_tx);
487
488        assert_eq!(context.instance_id(), "test-instance");
489        assert_eq!(context.query_id(), "test-query");
490    }
491
492    #[tokio::test]
493    async fn test_query_runtime_context_clone() {
494        let update_tx = test_update_tx();
495        let context = QueryRuntimeContext::new("test-instance", "test-query", update_tx);
496
497        let cloned = context.clone();
498        assert_eq!(cloned.query_id(), context.query_id());
499        assert_eq!(cloned.instance_id(), context.instance_id());
500    }
501
502    #[test]
503    fn test_query_runtime_context_debug() {
504        let update_tx = test_update_tx();
505        let context = QueryRuntimeContext::new("test-instance", "test-query", update_tx);
506        let debug_str = format!("{context:?}");
507        assert!(debug_str.contains("QueryRuntimeContext"));
508        assert!(debug_str.contains("test-query"));
509    }
510}