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}