Skip to main content

drasi_reaction_http/
lib.rs

1#![allow(unexpected_cfgs)]
2// Copyright 2025 The Drasi Authors.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! HTTP reaction plugin for Drasi
17//!
18//! This plugin implements HTTP reactions for Drasi and provides extension traits
19//! for configuring HTTP reactions in the Drasi plugin architecture.
20//!
21//! ## Instance-based Usage
22//!
23//! ```rust,ignore
24//! use drasi_reaction_http::{HttpReaction, HttpReactionConfig};
25//! use drasi_lib::channels::ComponentEventSender;
26//! use std::sync::Arc;
27//!
28//! // Create configuration
29//! let config = HttpReactionConfig {
30//!     base_url: "http://api.example.com".to_string(),
31//!     token: Some("secret-token".to_string()),
32//!     timeout_ms: 5000,
33//!     routes: Default::default(),
34//! };
35//!
36//! // Create instance and add to DrasiLib
37//! let reaction = Arc::new(HttpReaction::new(
38//!     "my-http-reaction",
39//!     vec!["query1".to_string()],
40//!     config,
41//!     event_tx,
42//! ));
43//! drasi.add_reaction(reaction).await?;
44//! ```
45
46pub mod config;
47pub mod descriptor;
48pub mod http;
49
50pub use config::{CallSpec, HttpReactionConfig, QueryConfig};
51pub use http::HttpReaction;
52
53use std::collections::HashMap;
54
55/// Builder for HTTP reaction
56///
57/// Creates an HttpReaction instance with a fluent API.
58///
59/// # Example
60/// ```rust,ignore
61/// use drasi_reaction_http::{HttpReaction, HttpReactionBuilder};
62///
63/// let reaction = HttpReaction::builder("my-http-reaction")
64///     .with_queries(vec!["query1".to_string()])
65///     .with_base_url("http://api.example.com")
66///     .with_token("secret-token")
67///     .with_timeout_ms(10000)
68///     .build()?;
69/// ```
70pub struct HttpReactionBuilder {
71    id: String,
72    queries: Vec<String>,
73    base_url: String,
74    token: Option<String>,
75    timeout_ms: u64,
76    routes: HashMap<String, QueryConfig>,
77    priority_queue_capacity: Option<usize>,
78    auto_start: bool,
79}
80
81impl HttpReactionBuilder {
82    /// Create a new HTTP reaction builder with the given ID
83    pub fn new(id: impl Into<String>) -> Self {
84        Self {
85            id: id.into(),
86            queries: Vec::new(),
87            base_url: "http://localhost".to_string(),
88            token: None,
89            timeout_ms: 5000,
90            routes: HashMap::new(),
91            priority_queue_capacity: None,
92            auto_start: true,
93        }
94    }
95
96    /// Set the query IDs to subscribe to
97    pub fn with_queries(mut self, queries: Vec<String>) -> Self {
98        self.queries = queries;
99        self
100    }
101
102    /// Add a query ID to subscribe to
103    pub fn with_query(mut self, query_id: impl Into<String>) -> Self {
104        self.queries.push(query_id.into());
105        self
106    }
107
108    /// Set the base URL for HTTP requests
109    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
110        self.base_url = base_url.into();
111        self
112    }
113
114    /// Set the authentication token
115    pub fn with_token(mut self, token: impl Into<String>) -> Self {
116        self.token = Some(token.into());
117        self
118    }
119
120    /// Set the request timeout in milliseconds
121    pub fn with_timeout_ms(mut self, timeout_ms: u64) -> Self {
122        self.timeout_ms = timeout_ms;
123        self
124    }
125
126    /// Add a route configuration for a specific query
127    pub fn with_route(mut self, query_id: impl Into<String>, config: QueryConfig) -> Self {
128        self.routes.insert(query_id.into(), config);
129        self
130    }
131
132    /// Set the priority queue capacity
133    pub fn with_priority_queue_capacity(mut self, capacity: usize) -> Self {
134        self.priority_queue_capacity = Some(capacity);
135        self
136    }
137
138    /// Set whether the reaction should auto-start
139    pub fn with_auto_start(mut self, auto_start: bool) -> Self {
140        self.auto_start = auto_start;
141        self
142    }
143
144    /// Set the full configuration at once
145    pub fn with_config(mut self, config: HttpReactionConfig) -> Self {
146        self.base_url = config.base_url;
147        self.token = config.token;
148        self.timeout_ms = config.timeout_ms;
149        self.routes = config.routes;
150        self
151    }
152
153    /// Build the HTTP reaction
154    pub fn build(self) -> anyhow::Result<HttpReaction> {
155        let config = HttpReactionConfig {
156            base_url: self.base_url,
157            token: self.token,
158            timeout_ms: self.timeout_ms,
159            routes: self.routes,
160        };
161
162        Ok(HttpReaction::from_builder(
163            self.id,
164            self.queries,
165            config,
166            self.priority_queue_capacity,
167            self.auto_start,
168        ))
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use drasi_lib::Reaction;
176
177    #[test]
178    fn test_http_builder_defaults() {
179        let reaction = HttpReactionBuilder::new("test-reaction").build().unwrap();
180        assert_eq!(reaction.id(), "test-reaction");
181        let props = reaction.properties();
182        assert_eq!(
183            props.get("base_url"),
184            Some(&serde_json::Value::String("http://localhost".to_string()))
185        );
186        assert_eq!(
187            props.get("timeout_ms"),
188            Some(&serde_json::Value::Number(5000.into()))
189        );
190    }
191
192    #[test]
193    fn test_http_builder_custom_values() {
194        let reaction = HttpReaction::builder("test-reaction")
195            .with_base_url("http://api.example.com") // DevSkim: ignore DS137138
196            .with_token("secret-token")
197            .with_timeout_ms(10000)
198            .with_queries(vec!["query1".to_string()])
199            .build()
200            .unwrap();
201
202        assert_eq!(reaction.id(), "test-reaction");
203        let props = reaction.properties();
204        assert_eq!(
205            props.get("base_url"),
206            Some(&serde_json::Value::String(
207                "http://api.example.com".to_string() // DevSkim: ignore DS137138
208            ))
209        );
210        assert_eq!(
211            props.get("timeout_ms"),
212            Some(&serde_json::Value::Number(10000.into()))
213        );
214        assert_eq!(reaction.query_ids(), vec!["query1".to_string()]);
215    }
216
217    #[test]
218    fn test_http_builder_with_query() {
219        let reaction = HttpReaction::builder("test-reaction")
220            .with_query("query1")
221            .with_query("query2")
222            .build()
223            .unwrap();
224
225        assert_eq!(reaction.query_ids(), vec!["query1", "query2"]);
226    }
227
228    #[test]
229    fn test_http_new_constructor() {
230        let config = HttpReactionConfig {
231            base_url: "http://test.example.com".to_string(), // DevSkim: ignore DS137138
232            token: Some("test-token".to_string()),
233            timeout_ms: 3000,
234            routes: Default::default(),
235        };
236
237        let reaction = HttpReaction::new("test-reaction", vec!["query1".to_string()], config);
238
239        assert_eq!(reaction.id(), "test-reaction");
240        assert_eq!(reaction.query_ids(), vec!["query1".to_string()]);
241    }
242}
243
244/// Dynamic plugin entry point.
245///
246/// Dynamic plugin entry point.
247#[cfg(feature = "dynamic-plugin")]
248drasi_plugin_sdk::export_plugin!(
249    plugin_id = "http-reaction",
250    core_version = env!("CARGO_PKG_VERSION"),
251    lib_version = env!("CARGO_PKG_VERSION"),
252    plugin_version = env!("CARGO_PKG_VERSION"),
253    source_descriptors = [],
254    reaction_descriptors = [descriptor::HttpReactionDescriptor],
255    bootstrap_descriptors = [],
256);