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::channels::ComponentEventSender;
64use crate::reactions::QueryProvider;
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/// - `source_id`: The unique identifier for this source instance
75/// - `status_tx`: Channel for reporting component status/lifecycle events
76/// - `state_store`: Optional persistent state storage (if configured)
77///
78/// # Clone
79///
80/// This struct implements `Clone` and all fields use `Arc` internally,
81/// making cloning cheap (just reference count increments).
82#[derive(Clone)]
83pub struct SourceRuntimeContext {
84    /// Unique identifier for this source instance
85    pub source_id: String,
86
87    /// Channel for reporting component status/lifecycle events.
88    ///
89    /// Use this to send status updates (Starting, Running, Stopped, Error)
90    /// back to DrasiLib for monitoring and lifecycle management.
91    pub status_tx: ComponentEventSender,
92
93    /// Optional persistent state storage.
94    ///
95    /// This is `Some` if a state store provider was configured on DrasiLib,
96    /// otherwise `None`. Sources can use this to persist state across restarts.
97    pub state_store: Option<Arc<dyn StateStoreProvider>>,
98}
99
100impl SourceRuntimeContext {
101    /// Create a new source runtime context.
102    ///
103    /// This is typically called by `SourceManager` when adding a source to DrasiLib.
104    /// Plugin developers do not need to call this directly.
105    ///
106    /// # Arguments
107    ///
108    /// * `source_id` - The unique identifier for this source
109    /// * `status_tx` - Channel for reporting component status/lifecycle events
110    /// * `state_store` - Optional persistent state storage
111    pub fn new(
112        source_id: impl Into<String>,
113        status_tx: ComponentEventSender,
114        state_store: Option<Arc<dyn StateStoreProvider>>,
115    ) -> Self {
116        Self {
117            source_id: source_id.into(),
118            status_tx,
119            state_store,
120        }
121    }
122
123    /// Get the source's unique identifier.
124    pub fn source_id(&self) -> &str {
125        &self.source_id
126    }
127
128    /// Get a reference to the status channel.
129    ///
130    /// Use this to send component status updates (Starting, Running, Stopped, Error)
131    /// back to DrasiLib.
132    pub fn status_tx(&self) -> &ComponentEventSender {
133        &self.status_tx
134    }
135
136    /// Get a reference to the state store if configured.
137    ///
138    /// Returns `Some(&Arc<dyn StateStoreProvider>)` if a state store was configured,
139    /// otherwise `None`.
140    pub fn state_store(&self) -> Option<&Arc<dyn StateStoreProvider>> {
141        self.state_store.as_ref()
142    }
143}
144
145impl std::fmt::Debug for SourceRuntimeContext {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("SourceRuntimeContext")
148            .field("source_id", &self.source_id)
149            .field("status_tx", &"<ComponentEventSender>")
150            .field(
151                "state_store",
152                &self.state_store.as_ref().map(|_| "<StateStoreProvider>"),
153            )
154            .finish()
155    }
156}
157
158/// Context provided to Reaction plugins during initialization.
159///
160/// Contains `Arc<T>` instances for all drasi-lib provided services.
161/// DrasiLib constructs this context when a reaction is added via `add_reaction()`.
162///
163/// # Available Services
164///
165/// - `reaction_id`: The unique identifier for this reaction instance
166/// - `status_tx`: Channel for reporting component status/lifecycle events
167/// - `state_store`: Optional persistent state storage (if configured)
168/// - `query_provider`: Access to query instances for subscription
169///
170/// # Clone
171///
172/// This struct implements `Clone` and all fields use `Arc` internally,
173/// making cloning cheap (just reference count increments).
174#[derive(Clone)]
175pub struct ReactionRuntimeContext {
176    /// Unique identifier for this reaction instance
177    pub reaction_id: String,
178
179    /// Channel for reporting component status/lifecycle events.
180    ///
181    /// Use this to send status updates (Starting, Running, Stopped, Error)
182    /// back to DrasiLib for monitoring and lifecycle management.
183    pub status_tx: ComponentEventSender,
184
185    /// Optional persistent state storage.
186    ///
187    /// This is `Some` if a state store provider was configured on DrasiLib,
188    /// otherwise `None`. Reactions can use this to persist state across restarts.
189    pub state_store: Option<Arc<dyn StateStoreProvider>>,
190
191    /// Access to query instances for subscription.
192    ///
193    /// Reactions use this to get query instances and subscribe to their results.
194    /// This is always available (not optional) since reactions require queries.
195    pub query_provider: Arc<dyn QueryProvider>,
196}
197
198impl ReactionRuntimeContext {
199    /// Create a new reaction runtime context.
200    ///
201    /// This is typically called by `ReactionManager` when adding a reaction to DrasiLib.
202    /// Plugin developers do not need to call this directly.
203    ///
204    /// # Arguments
205    ///
206    /// * `reaction_id` - The unique identifier for this reaction
207    /// * `status_tx` - Channel for reporting component status/lifecycle events
208    /// * `state_store` - Optional persistent state storage
209    /// * `query_provider` - Access to query instances for subscription
210    pub fn new(
211        reaction_id: impl Into<String>,
212        status_tx: ComponentEventSender,
213        state_store: Option<Arc<dyn StateStoreProvider>>,
214        query_provider: Arc<dyn QueryProvider>,
215    ) -> Self {
216        Self {
217            reaction_id: reaction_id.into(),
218            status_tx,
219            state_store,
220            query_provider,
221        }
222    }
223
224    /// Get the reaction's unique identifier.
225    pub fn reaction_id(&self) -> &str {
226        &self.reaction_id
227    }
228
229    /// Get a reference to the status channel.
230    ///
231    /// Use this to send component status updates (Starting, Running, Stopped, Error)
232    /// back to DrasiLib.
233    pub fn status_tx(&self) -> &ComponentEventSender {
234        &self.status_tx
235    }
236
237    /// Get a reference to the state store if configured.
238    ///
239    /// Returns `Some(&Arc<dyn StateStoreProvider>)` if a state store was configured,
240    /// otherwise `None`.
241    pub fn state_store(&self) -> Option<&Arc<dyn StateStoreProvider>> {
242        self.state_store.as_ref()
243    }
244
245    /// Get a reference to the query provider.
246    ///
247    /// Use this to get query instances and subscribe to their results.
248    pub fn query_provider(&self) -> &Arc<dyn QueryProvider> {
249        &self.query_provider
250    }
251}
252
253impl std::fmt::Debug for ReactionRuntimeContext {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        f.debug_struct("ReactionRuntimeContext")
256            .field("reaction_id", &self.reaction_id)
257            .field("status_tx", &"<ComponentEventSender>")
258            .field(
259                "state_store",
260                &self.state_store.as_ref().map(|_| "<StateStoreProvider>"),
261            )
262            .field("query_provider", &"<QueryProvider>")
263            .finish()
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::queries::Query;
271    use crate::state_store::MemoryStateStoreProvider;
272    use anyhow::Result;
273    use async_trait::async_trait;
274    use std::sync::Arc;
275    use tokio::sync::mpsc;
276
277    // Mock QueryProvider for testing
278    struct MockQueryProvider;
279
280    #[async_trait]
281    impl QueryProvider for MockQueryProvider {
282        async fn get_query_instance(&self, _id: &str) -> Result<Arc<dyn Query>> {
283            Err(anyhow::anyhow!("MockQueryProvider: query not found"))
284        }
285    }
286
287    #[tokio::test]
288    async fn test_source_runtime_context_creation() {
289        let (status_tx, _rx) = mpsc::channel(100);
290        let state_store = Arc::new(MemoryStateStoreProvider::new());
291
292        let context = SourceRuntimeContext::new("test-source", status_tx, Some(state_store));
293
294        assert_eq!(context.source_id(), "test-source");
295        assert!(context.state_store().is_some());
296    }
297
298    #[tokio::test]
299    async fn test_source_runtime_context_without_state_store() {
300        let (status_tx, _rx) = mpsc::channel(100);
301
302        let context = SourceRuntimeContext::new("test-source", status_tx, None);
303
304        assert_eq!(context.source_id(), "test-source");
305        assert!(context.state_store().is_none());
306    }
307
308    #[tokio::test]
309    async fn test_source_runtime_context_clone() {
310        let (status_tx, _rx) = mpsc::channel(100);
311        let state_store = Arc::new(MemoryStateStoreProvider::new());
312
313        let context = SourceRuntimeContext::new("test-source", status_tx, Some(state_store));
314
315        let cloned = context.clone();
316        assert_eq!(cloned.source_id(), context.source_id());
317    }
318
319    #[tokio::test]
320    async fn test_reaction_runtime_context_creation() {
321        let (status_tx, _rx) = mpsc::channel(100);
322        let state_store = Arc::new(MemoryStateStoreProvider::new());
323        let query_provider = Arc::new(MockQueryProvider);
324
325        let context = ReactionRuntimeContext::new(
326            "test-reaction",
327            status_tx,
328            Some(state_store),
329            query_provider,
330        );
331
332        assert_eq!(context.reaction_id(), "test-reaction");
333        assert!(context.state_store().is_some());
334    }
335
336    #[tokio::test]
337    async fn test_reaction_runtime_context_without_state_store() {
338        let (status_tx, _rx) = mpsc::channel(100);
339        let query_provider = Arc::new(MockQueryProvider);
340
341        let context = ReactionRuntimeContext::new("test-reaction", status_tx, None, query_provider);
342
343        assert_eq!(context.reaction_id(), "test-reaction");
344        assert!(context.state_store().is_none());
345    }
346
347    #[tokio::test]
348    async fn test_reaction_runtime_context_clone() {
349        let (status_tx, _rx) = mpsc::channel(100);
350        let state_store = Arc::new(MemoryStateStoreProvider::new());
351        let query_provider = Arc::new(MockQueryProvider);
352
353        let context = ReactionRuntimeContext::new(
354            "test-reaction",
355            status_tx,
356            Some(state_store),
357            query_provider,
358        );
359
360        let cloned = context.clone();
361        assert_eq!(cloned.reaction_id(), context.reaction_id());
362    }
363
364    #[test]
365    fn test_source_runtime_context_debug() {
366        let (status_tx, _rx) = mpsc::channel::<crate::channels::ComponentEvent>(100);
367        let context = SourceRuntimeContext::new("test", status_tx, None);
368        let debug_str = format!("{context:?}");
369        assert!(debug_str.contains("SourceRuntimeContext"));
370        assert!(debug_str.contains("test"));
371    }
372
373    #[test]
374    fn test_reaction_runtime_context_debug() {
375        let (status_tx, _rx) = mpsc::channel::<crate::channels::ComponentEvent>(100);
376        let query_provider = Arc::new(MockQueryProvider);
377        let context = ReactionRuntimeContext::new("test", status_tx, None, query_provider);
378        let debug_str = format!("{context:?}");
379        assert!(debug_str.contains("ReactionRuntimeContext"));
380        assert!(debug_str.contains("test"));
381    }
382}