mq_bridge/endpoints/
static_endpoint.rs1use crate::models::StaticConfig;
6use crate::traits::{
7 BoxFuture, ConsumerError, MessageConsumer, MessageDisposition, MessagePublisher,
8 PublisherError, Received, ReceivedBatch, Sent, SentBatch,
9};
10use crate::CanonicalMessage;
11use anyhow::Context;
12use async_trait::async_trait;
13use bytes::Bytes;
14use serde_json::Value;
15use std::any::Any;
16use tracing::trace;
17
18#[derive(Clone)]
20pub struct StaticEndpointPublisher {
21 payload: Vec<u8>,
22 content_raw: String,
23 metadata: std::collections::HashMap<String, String>,
24}
25
26impl StaticEndpointPublisher {
27 pub fn new(config: &StaticConfig) -> anyhow::Result<Self> {
28 let payload = if config.raw {
31 config.body.clone().into_bytes()
32 } else {
33 serde_json::to_vec(&Value::String(config.body.clone()))
34 .context("Failed to serialize static response to JSON")?
35 };
36 Ok(Self {
37 payload,
38 content_raw: config.body.clone(),
39 metadata: config.metadata.clone(),
40 })
41 }
42}
43
44#[async_trait]
45impl MessagePublisher for StaticEndpointPublisher {
46 async fn send(&self, _message: CanonicalMessage) -> Result<Sent, PublisherError> {
47 let mut response_msg = CanonicalMessage::new(self.payload.clone(), None);
48 for (key, value) in &self.metadata {
52 response_msg.metadata.insert(key.clone(), value.clone());
53 }
54 trace!(
55 message_id = %format!("{:032x}", response_msg.message_id),
56 response = %self.content_raw, "Sending static response"
57 );
58 Ok(Sent::Response(response_msg))
59 }
60
61 async fn send_batch(
62 &self,
63 messages: Vec<CanonicalMessage>,
64 ) -> Result<SentBatch, PublisherError> {
65 crate::traits::send_batch_helper(self, messages, |publisher, message| {
66 Box::pin(publisher.send(message))
67 })
68 .await
69 }
70
71 async fn flush(&self) -> anyhow::Result<()> {
72 Ok(()) }
74
75 fn as_any(&self) -> &dyn Any {
76 self
77 }
78}
79
80#[derive(Clone)]
82pub struct StaticRequestConsumer {
83 payload: Bytes,
84 #[allow(dead_code)]
85 content: String, metadata: std::collections::HashMap<String, String>,
87}
88
89impl StaticRequestConsumer {
90 pub fn new(config: &StaticConfig) -> anyhow::Result<Self> {
91 Ok(Self {
92 payload: Bytes::copy_from_slice(config.body.as_bytes()),
93 content: config.body.clone(),
94 metadata: config.metadata.clone(),
95 })
96 }
97}
98
99#[async_trait]
100impl MessageConsumer for StaticRequestConsumer {
101 async fn receive(&mut self) -> Result<Received, ConsumerError> {
102 let mut message = CanonicalMessage::new_bytes(self.payload.clone(), None);
103 message.metadata = self.metadata.clone();
104 trace!(message_id = %format!("{:032x}", message.message_id), "Producing static message");
105 let commit = Box::new(|_disposition: MessageDisposition| {
106 Box::pin(async { Ok(()) }) as BoxFuture<'static, anyhow::Result<()>>
107 });
108 Ok(Received { message, commit })
109 }
110
111 async fn receive_batch(
112 &mut self,
113 _max_messages: usize,
114 ) -> Result<ReceivedBatch, ConsumerError> {
115 let mut messages = Vec::with_capacity(_max_messages);
118 for _ in 0.._max_messages {
119 let mut message = CanonicalMessage::new_bytes(self.payload.clone(), None);
120 message.metadata = self.metadata.clone();
121 messages.push(message);
122 }
123 let commit = Box::new(|_disposition: Vec<MessageDisposition>| {
125 Box::pin(async { Ok(()) }) as BoxFuture<'static, anyhow::Result<()>>
126 });
127 Ok(ReceivedBatch { messages, commit })
128 }
129
130 fn as_any(&self) -> &dyn Any {
131 self
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::CanonicalMessage;
139 use serde_json::Value;
140
141 fn config(body: &str) -> StaticConfig {
142 StaticConfig {
143 body: body.to_string(),
144 raw: false,
145 metadata: std::collections::HashMap::new(),
146 }
147 }
148
149 #[tokio::test]
150 async fn test_static_publisher() {
151 let content = "static_response";
152 let publisher = StaticEndpointPublisher::new(&config(content)).unwrap();
153 let msg = CanonicalMessage::new(vec![], None);
154
155 let response = publisher.send(msg).await.unwrap();
156 let response_msg = match response {
157 Sent::Response(msg) => msg,
158 _ => panic!("Expected response"),
159 };
160 let expected_payload = serde_json::to_vec(&Value::String(content.to_string())).unwrap();
161 assert_eq!(response_msg.payload, expected_payload);
162 }
163
164 #[tokio::test]
165 async fn test_static_publisher_raw_with_metadata() {
166 let mut metadata = std::collections::HashMap::new();
167 metadata.insert("content-type".to_string(), "text/plain".to_string());
168 metadata.insert("server".to_string(), "mq-bridge".to_string());
169 let publisher = StaticEndpointPublisher::new(&StaticConfig {
170 body: "Hello, World!".to_string(),
171 raw: true,
172 metadata,
173 })
174 .unwrap();
175
176 let response = publisher
177 .send(CanonicalMessage::new(vec![], None))
178 .await
179 .unwrap();
180 let response_msg = match response {
181 Sent::Response(msg) => msg,
182 _ => panic!("Expected response"),
183 };
184 assert_eq!(response_msg.payload.as_ref(), b"Hello, World!");
186 assert_eq!(
187 response_msg
188 .metadata
189 .get("content-type")
190 .map(String::as_str),
191 Some("text/plain")
192 );
193 assert_eq!(
194 response_msg.metadata.get("server").map(String::as_str),
195 Some("mq-bridge")
196 );
197 }
198
199 #[tokio::test]
200 async fn test_static_consumer() {
201 let content = "static_message";
202 let mut consumer = StaticRequestConsumer::new(&config(content)).unwrap();
203
204 let received = consumer.receive().await.unwrap();
205 assert_eq!(received.message.payload, content.as_bytes());
206 }
207
208 #[test]
209 fn test_static_config_yaml() {
210 use crate::models::{Config, EndpointType};
211
212 let yaml = r#"
213test_route:
214 input:
215 static: "static_input_value"
216 output:
217 static: "static_output_value"
218"#;
219 let config: Config = serde_yaml_ng::from_str(yaml).expect("Failed to parse YAML");
220 let route = config.get("test_route").expect("Route should exist");
221
222 if let EndpointType::Static(val) = &route.input.endpoint_type {
223 assert_eq!(val.body, "static_input_value");
224 } else {
225 panic!("Input was not static");
226 }
227
228 if let EndpointType::Static(val) = &route.output.endpoint_type {
229 assert_eq!(val.body, "static_output_value");
230 } else {
231 panic!("Output was not static");
232 }
233 }
234
235 #[test]
236 fn test_static_config_simple_serializes_as_bare_string() {
237 let cfg = StaticConfig::from("hello");
241 let yaml = serde_yaml_ng::to_string(&cfg).unwrap();
242 assert_eq!(yaml.trim(), "hello");
243
244 let cfg = StaticConfig {
245 body: "hi".to_string(),
246 raw: true,
247 metadata: std::collections::HashMap::new(),
248 };
249 let yaml = serde_yaml_ng::to_string(&cfg).unwrap();
250 assert!(yaml.contains("body: hi"), "expected map form, got: {yaml}");
251 assert!(yaml.contains("raw: true"));
252 }
253
254 #[test]
255 fn test_static_config_map_form() {
256 use crate::models::{Config, EndpointType};
257
258 let yaml = r#"
259test_route:
260 input:
261 static: "in"
262 output:
263 static:
264 body: "Hello, World!"
265 raw: true
266 metadata:
267 content-type: "text/plain"
268"#;
269 let config: Config = serde_yaml_ng::from_str(yaml).expect("Failed to parse YAML");
270 let route = config.get("test_route").expect("Route should exist");
271
272 if let EndpointType::Static(val) = &route.output.endpoint_type {
273 assert_eq!(val.body, "Hello, World!");
274 assert_eq!(
275 val.metadata.get("content-type").map(String::as_str),
276 Some("text/plain")
277 );
278 assert!(val.raw);
279 } else {
280 panic!("Output was not static");
281 }
282 }
283}