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