Skip to main content

drasi_lib/reactions/
traits.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//! Reaction trait module
16//!
17//! This module provides the core traits that all reaction plugins must implement.
18//! It separates the plugin contract from the reaction manager and implementation details.
19//!
20//! # Plugin Architecture
21//!
22//! Each reaction plugin:
23//! 1. Defines its own typed configuration struct with builder pattern
24//! 2. Creates a `ReactionBase` instance using `ReactionBaseParams`
25//! 3. Implements the `Reaction` trait
26//! 4. Is passed to `DrasiLib` via `add_reaction()` which takes ownership
27//!
28//! drasi-lib has no knowledge of which plugins exist - it only knows about this trait.
29//!
30//! # Runtime Context Initialization
31//!
32//! Reactions receive all drasi-lib services through a single `initialize()` call
33//! when added to DrasiLib. The `ReactionRuntimeContext` provides:
34//! - `event_tx`: Channel for component lifecycle events
35//! - `state_store`: Optional persistent state storage
36//! - `query_provider`: Access to query instances for subscription
37//!
38//! This replaces the previous `inject_*` methods with a cleaner single-call pattern.
39
40use anyhow::Result;
41use async_trait::async_trait;
42use std::sync::Arc;
43
44use crate::channels::ComponentStatus;
45use crate::context::ReactionRuntimeContext;
46use crate::queries::Query;
47
48/// Trait for providing access to queries without requiring full DrasiLib dependency.
49///
50/// This trait provides a way for reactions to access query instances for subscription
51/// without needing a direct dependency on the server core.
52#[async_trait]
53pub trait QueryProvider: Send + Sync {
54    /// Get a query instance by ID
55    async fn get_query_instance(&self, id: &str) -> Result<Arc<dyn Query>>;
56}
57
58/// Trait defining the interface for all reaction implementations.
59///
60/// This is the core abstraction that all reaction plugins must implement.
61/// drasi-lib only interacts with reactions through this trait - it has no
62/// knowledge of specific plugin types or their configurations.
63///
64/// # Lifecycle
65///
66/// Reactions follow this lifecycle:
67/// 1. Created by plugin code with configuration
68/// 2. Added to DrasiLib via `add_reaction()` - dependencies injected automatically
69/// 3. Started via `start()` (auto-start or manual based on `auto_start()`)
70/// 4. Stopped via `stop()` when no longer needed
71///
72/// # Subscription Model
73///
74/// Reactions manage their own subscriptions to queries using the broadcast channel pattern:
75/// - QueryProvider is injected via `inject_query_provider()` at add time
76/// - Reactions access queries via `query_provider.get_query_instance()`
77/// - For each query, reactions call `query.subscribe(reaction_id)`
78/// - Each subscription provides a broadcast receiver for that query's results
79/// - Reactions use a priority queue to process results from multiple queries in timestamp order
80///
81/// # Example Implementation
82///
83/// ```ignore
84/// use drasi_lib::{Reaction, QueryProvider};
85/// use drasi_lib::reactions::{ReactionBase, ReactionBaseParams};
86/// use drasi_lib::context::ReactionRuntimeContext;
87///
88/// pub struct MyReaction {
89///     base: ReactionBase,
90///     // Plugin-specific fields
91/// }
92///
93/// impl MyReaction {
94///     pub fn new(config: MyReactionConfig) -> Self {
95///         let params = ReactionBaseParams::new(&config.id, config.queries.clone())
96///             .with_priority_queue_capacity(config.queue_capacity);
97///
98///         Self {
99///             base: ReactionBase::new(params),
100///         }
101///     }
102/// }
103///
104/// #[async_trait]
105/// impl Reaction for MyReaction {
106///     fn id(&self) -> &str {
107///         &self.base.id
108///     }
109///
110///     fn type_name(&self) -> &str {
111///         "my-reaction"
112///     }
113///
114///     fn query_ids(&self) -> Vec<String> {
115///         self.base.queries.clone()
116///     }
117///
118///     async fn initialize(&self, context: ReactionRuntimeContext) {
119///         self.base.initialize(context).await;
120///     }
121///
122///     async fn start(&self) -> Result<()> {
123///         self.base.subscribe_to_queries().await?;
124///         // ... start processing
125///         Ok(())
126///     }
127///
128///     // ... implement other methods
129/// }
130/// ```
131#[async_trait]
132pub trait Reaction: Send + Sync {
133    /// Get the reaction's unique identifier
134    fn id(&self) -> &str;
135
136    /// Get the reaction type name (e.g., "http", "log", "sse")
137    fn type_name(&self) -> &str;
138
139    /// Get the reaction's configuration properties for inspection
140    ///
141    /// This returns a HashMap representation of the reaction's configuration
142    /// for use in APIs and inspection. The actual typed configuration is
143    /// owned by the plugin - this is just for external visibility.
144    fn properties(&self) -> std::collections::HashMap<String, serde_json::Value>;
145
146    /// Get the list of query IDs this reaction subscribes to
147    fn query_ids(&self) -> Vec<String>;
148
149    /// Whether this reaction should auto-start when DrasiLib starts
150    ///
151    /// Default is `true` to match query behavior. Override to return `false`
152    /// if this reaction should only be started manually via `start_reaction()`.
153    fn auto_start(&self) -> bool {
154        true
155    }
156
157    /// Initialize the reaction with runtime context.
158    ///
159    /// This method is called automatically by DrasiLib when the reaction is added
160    /// via `add_reaction()`. Plugin developers do not need to call this directly.
161    ///
162    /// The context provides access to:
163    /// - `reaction_id`: The reaction's unique identifier
164    /// - `event_tx`: Channel for reporting component lifecycle events
165    /// - `state_store`: Optional persistent state storage
166    /// - `query_provider`: Access to query instances for subscription
167    ///
168    /// Implementation should delegate to `self.base.initialize(context).await`.
169    async fn initialize(&self, context: ReactionRuntimeContext);
170
171    /// Start the reaction
172    ///
173    /// The reaction should:
174    /// 1. Subscribe to all configured queries (using injected QueryProvider)
175    /// 2. Start its processing loop
176    /// 3. Update its status to Running
177    ///
178    /// Note: QueryProvider is already available via `inject_query_provider()` which
179    /// is called when the reaction is added to DrasiLib.
180    async fn start(&self) -> Result<()>;
181
182    /// Stop the reaction, cleaning up all subscriptions and tasks
183    async fn stop(&self) -> Result<()>;
184
185    /// Get the current status of the reaction
186    async fn status(&self) -> ComponentStatus;
187}
188
189/// Blanket implementation of Reaction for `Box<dyn Reaction>`
190///
191/// This allows boxed trait objects to be used with methods expecting `impl Reaction`.
192#[async_trait]
193impl Reaction for Box<dyn Reaction + 'static> {
194    fn id(&self) -> &str {
195        (**self).id()
196    }
197
198    fn type_name(&self) -> &str {
199        (**self).type_name()
200    }
201
202    fn properties(&self) -> std::collections::HashMap<String, serde_json::Value> {
203        (**self).properties()
204    }
205
206    fn query_ids(&self) -> Vec<String> {
207        (**self).query_ids()
208    }
209
210    fn auto_start(&self) -> bool {
211        (**self).auto_start()
212    }
213
214    async fn initialize(&self, context: ReactionRuntimeContext) {
215        (**self).initialize(context).await
216    }
217
218    async fn start(&self) -> Result<()> {
219        (**self).start().await
220    }
221
222    async fn stop(&self) -> Result<()> {
223        (**self).stop().await
224    }
225
226    async fn status(&self) -> ComponentStatus {
227        (**self).status().await
228    }
229}