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}