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