1#[cfg(feature = "amqp")]
7pub mod amqp;
8#[cfg(feature = "aws")]
9pub mod aws;
10pub mod fanout;
11pub mod file;
12#[cfg(feature = "grpc")]
13pub mod grpc;
14#[cfg(feature = "http")]
15pub mod http;
16#[cfg(feature = "http")]
17mod http_stream;
18#[cfg(feature = "ibm-mq")]
19pub mod ibm_mq;
20#[cfg(feature = "kafka")]
21pub mod kafka;
22pub mod memory;
23#[cfg(feature = "mongodb")]
24pub mod mongodb;
25#[cfg(feature = "mqtt")]
26pub mod mqtt;
27#[cfg(feature = "nats")]
28pub mod nats;
29pub mod null;
30pub mod reader;
31pub mod response;
32#[cfg(feature = "sled")]
33pub mod sled;
34#[cfg(feature = "sqlx")]
35pub mod sqlx;
36pub mod static_endpoint;
37pub mod stream_buffer;
38pub mod switch;
39#[cfg(feature = "websocket")]
40pub mod websocket;
41#[cfg(feature = "zeromq")]
42pub mod zeromq;
43use crate::endpoints::memory::{get_or_create_channel, MemoryChannel};
44use crate::middleware::apply_middlewares_to_consumer;
45use crate::models::{
46 Endpoint, EndpointType, MemoryConfig, Middleware, ResponseConfig, StreamBufferConfig,
47};
48use crate::route::get_endpoint_factory;
49use crate::traits::{BoxFuture, MessageConsumer, MessagePublisher};
50use anyhow::{anyhow, Result};
51use std::sync::Arc;
52
53impl Endpoint {
54 pub fn new(endpoint_type: EndpointType) -> Self {
55 Self {
56 middlewares: Vec::new(),
57 endpoint_type,
58 handler: None,
59 }
60 }
61 pub fn new_memory(topic: &str, capacity: usize) -> Self {
70 Self::new(EndpointType::Memory(MemoryConfig::new(
71 topic,
72 Some(capacity),
73 )))
74 }
75 pub fn new_response() -> Self {
76 Self::new(EndpointType::Response(ResponseConfig::default()))
77 }
78 pub fn new_stream_buffer(topic: &str, correlation_id: Option<&str>, capacity: usize) -> Self {
79 Self::new(EndpointType::StreamBuffer(StreamBufferConfig {
80 topic: topic.to_string(),
81 correlation_id: correlation_id.map(str::to_string),
82 capacity: Some(capacity),
83 }))
84 }
85 pub fn has_retry_middleware(&self) -> bool {
86 self.middlewares
87 .iter()
88 .any(|m| matches!(m, Middleware::Retry(_)))
89 }
90 pub fn add_middleware(mut self, middleware: Middleware) -> Self {
91 self.middlewares.push(middleware);
92 self
93 }
94 pub fn add_middlewares(mut self, mut middlewares: Vec<Middleware>) -> Self {
95 self.middlewares.append(&mut middlewares);
96 self
97 }
98 pub fn channel(&self) -> anyhow::Result<MemoryChannel> {
104 match &self.endpoint_type {
105 EndpointType::Memory(cfg) => Ok(get_or_create_channel(cfg)),
106 _ => Err(anyhow::anyhow!("channel() called on non-memory Endpoint")),
107 }
108 }
109 pub fn null() -> Self {
110 Self::new(EndpointType::Null)
111 }
112
113 pub fn with_retry(mut self, retry: crate::models::RetryMiddleware) -> Self {
114 let mut insert_idx = self.middlewares.len();
117 for (i, m) in self.middlewares.iter().enumerate() {
118 if matches!(m, Middleware::Dlq(_) | Middleware::Metrics(_)) {
119 insert_idx = i;
120 break;
121 }
122 }
123 self.middlewares
124 .insert(insert_idx, Middleware::Retry(retry));
125 self
126 }
127
128 pub fn with_dlq(mut self, dlq: crate::models::DeadLetterQueueMiddleware) -> Self {
129 let mut insert_idx = self.middlewares.len();
131 for (i, m) in self.middlewares.iter().enumerate() {
132 if matches!(m, Middleware::Metrics(_)) {
133 insert_idx = i;
134 break;
135 }
136 }
137 self.middlewares
138 .insert(insert_idx, Middleware::Dlq(Box::new(dlq)));
139 self
140 }
141
142 pub fn with_deduplication(mut self, dedup: crate::models::DeduplicationMiddleware) -> Self {
143 self.middlewares.insert(0, Middleware::Deduplication(dedup));
148 self
149 }
150
151 pub fn with_consumer_metrics(mut self) -> Self {
152 if !self
156 .middlewares
157 .iter()
158 .any(|m| matches!(m, Middleware::Metrics(_)))
159 {
160 self.middlewares
161 .insert(0, Middleware::Metrics(crate::models::MetricsMiddleware {}));
162 }
163 self
164 }
165
166 pub fn with_metrics(mut self) -> Self {
167 if !self
169 .middlewares
170 .iter()
171 .any(|m| matches!(m, Middleware::Metrics(_)))
172 {
173 self.middlewares
174 .push(Middleware::Metrics(crate::models::MetricsMiddleware {}));
175 }
176 self
177 }
178
179 pub async fn create_consumer(
180 &self,
181 route_name: &str,
182 ) -> anyhow::Result<Box<dyn crate::traits::MessageConsumer>> {
183 crate::endpoints::create_consumer_from_route(route_name, self).await
184 }
185
186 pub async fn create_publisher(&self, _route_name: &str) -> anyhow::Result<crate::Publisher> {
187 crate::Publisher::new(self.clone()).await
188 }
189
190 pub fn check_consumer(
191 &self,
192 route_name: &str,
193 allowed_endpoints: Option<&[&str]>,
194 ) -> anyhow::Result<Vec<String>> {
195 crate::endpoints::check_consumer(route_name, self, allowed_endpoints)
196 }
197
198 pub fn check_publisher(
199 &self,
200 route_name: &str,
201 allowed_endpoints: Option<&[&str]>,
202 ) -> anyhow::Result<Vec<String>> {
203 crate::endpoints::check_publisher(route_name, self, allowed_endpoints)
204 }
205}
206
207pub fn check_consumer(
209 route_name: &str,
210 endpoint: &Endpoint,
211 allowed_types: Option<&[&str]>,
212) -> Result<Vec<String>> {
213 check_consumer_recursive(route_name, endpoint, 0, allowed_types)
214}
215
216fn check_consumer_recursive(
217 route_name: &str,
218 endpoint: &Endpoint,
219 depth: usize,
220 allowed_types: Option<&[&str]>,
221) -> Result<Vec<String>> {
222 const MAX_DEPTH: usize = 16;
223 if depth > MAX_DEPTH {
224 return Err(anyhow!(
225 "Ref recursion depth exceeded limit of {}",
226 MAX_DEPTH
227 ));
228 }
229 let mut warnings = Vec::new();
230 if endpoint.handler.is_some() {
231 warnings.push(
232 "Endpoint 'handler' is set on an input endpoint. Handlers are currently only supported on output endpoints (publishers) and will be ignored here."
233 .to_string()
234 );
235 }
236
237 if let Some(allowed) = allowed_types {
238 if !endpoint.endpoint_type.is_core() {
239 let name = endpoint.endpoint_type.name();
240 if !allowed.contains(&name) {
241 return Err(anyhow!(
242 "[route:{}] Endpoint type '{}' is not allowed by policy",
243 route_name,
244 name
245 ));
246 }
247 }
248 }
249 match &endpoint.endpoint_type {
250 EndpointType::Ref(name) => {
251 let referenced = crate::route::get_endpoint(name).ok_or_else(|| {
252 anyhow!(
253 "[route:{}] Referenced endpoint '{}' not found",
254 route_name,
255 name
256 )
257 })?;
258 warnings.extend(check_consumer_recursive(
263 route_name,
264 &referenced,
265 depth + 1,
266 allowed_types,
267 )?);
268 Ok(warnings)
269 }
270 #[cfg(feature = "aws")]
271 EndpointType::Aws(cfg) => {
272 if cfg.topic_arn.is_some() {
273 warnings.push(
274 "Endpoint 'aws' is used as a consumer, but 'topic_arn' is a publisher-only option and will be ignored."
275 .to_string()
276 );
277 }
278 Ok(warnings)
279 }
280 #[cfg(feature = "kafka")]
281 EndpointType::Kafka(cfg) => {
282 if cfg.delayed_ack {
283 warnings.push(
284 "Endpoint 'kafka' is used as a consumer, but 'delayed_ack' is a publisher-only option and will be ignored."
285 .to_string()
286 );
287 }
288 if cfg.producer_options.is_some() {
289 warnings.push(
290 "Endpoint 'kafka' is used as a consumer, but 'producer_options' is a publisher-only option and will be ignored."
291 .to_string()
292 );
293 }
294 Ok(warnings)
295 }
296 #[cfg(feature = "nats")]
297 EndpointType::Nats(cfg) => {
298 if cfg.stream.is_none() {
299 return Err(anyhow!(
300 "[route:{}] NATS consumer must specify a 'stream'",
301 route_name
302 ));
303 }
304 if cfg.request_reply {
305 warnings.push(
306 "Endpoint 'nats' is used as a consumer, but 'request_reply' is a publisher-only option and will be ignored."
307 .to_string()
308 );
309 }
310 if cfg.request_timeout_ms.is_some() {
311 warnings.push(
312 "Endpoint 'nats' is used as a consumer, but 'request_timeout_ms' is a publisher-only option and will be ignored."
313 .to_string()
314 );
315 }
316 if cfg.delayed_ack {
317 warnings.push(
318 "Endpoint 'nats' is used as a consumer, but 'delayed_ack' is a publisher-only option and will be ignored."
319 .to_string()
320 );
321 }
322 if cfg.stream_max_messages.is_some() {
323 warnings.push(
324 "Endpoint 'nats' is used as a consumer, but 'stream_max_messages' is a publisher-only option and will be ignored."
325 .to_string()
326 );
327 }
328 if cfg.stream_max_bytes.is_some() {
329 warnings.push(
330 "Endpoint 'nats' is used as a consumer, but 'stream_max_bytes' is a publisher-only option and will be ignored."
331 .to_string()
332 );
333 }
334 Ok(warnings)
335 }
336 #[cfg(feature = "amqp")]
337 EndpointType::Amqp(cfg) => {
338 if cfg.delayed_ack {
339 warnings.push(
340 "Endpoint 'amqp' is used as a consumer, but 'delayed_ack' is a publisher-only option and will be ignored."
341 .to_string()
342 );
343 }
344 Ok(warnings)
345 }
346 #[cfg(feature = "mqtt")]
347 EndpointType::Mqtt(cfg) => {
348 if cfg.delayed_ack {
349 warnings.push(
350 "Endpoint 'mqtt' is used as a consumer, but 'delayed_ack' is a publisher-only option and will be ignored."
351 .to_string()
352 );
353 }
354 Ok(warnings)
355 }
356 #[cfg(feature = "zeromq")]
357 EndpointType::ZeroMq(_) => Ok(warnings),
358 #[cfg(feature = "ibm-mq")]
359 EndpointType::IbmMq(_) => Ok(warnings),
360 #[cfg(feature = "mongodb")]
361 EndpointType::MongoDb(cfg) => {
362 if cfg.change_stream && matches!(cfg.format, crate::models::MongoDbFormat::Raw) {
363 return Err(anyhow!(
364 "[route:{}] MongoDB raw format cannot be used with change_stream/subscriber mode because raw documents do not include the seq ordering field",
365 route_name
366 ));
367 }
368 if cfg.reply_polling_ms.is_some() {
369 warnings.push(
370 "Endpoint 'mongodb' is used as a consumer, but 'reply_polling_ms' is a publisher-only option and will be ignored."
371 .to_string()
372 );
373 }
374 if cfg.request_reply {
375 warnings.push(
376 "Endpoint 'mongodb' is used as a consumer, but 'request_reply' is a publisher-only option and will be ignored."
377 .to_string()
378 );
379 }
380 if cfg.request_timeout_ms.is_some() {
381 warnings.push(
382 "Endpoint 'mongodb' is used as a consumer, but 'request_timeout_ms' is a publisher-only option and will be ignored."
383 .to_string()
384 );
385 }
386 if cfg.ttl_seconds.is_some() {
387 warnings.push(
388 "Endpoint 'mongodb' is used as a consumer, but 'ttl_seconds' is a publisher-only option and will be ignored."
389 .to_string()
390 );
391 }
392 if cfg.capped_size_bytes.is_some() {
393 warnings.push(
394 "Endpoint 'mongodb' is used as a consumer, but 'capped_size_bytes' is a publisher-only option and will be ignored."
395 .to_string()
396 );
397 }
398 Ok(warnings)
399 }
400 #[cfg(feature = "grpc")]
401 EndpointType::Grpc(_) => Ok(warnings),
402 #[cfg(feature = "http")]
403 EndpointType::Http(cfg) => {
404 if cfg.batch_concurrency.is_some() {
405 warnings.push("Endpoint 'http' is used as a consumer, but 'batch_concurrency' is a publisher-only option and will be ignored.".to_string());
406 }
407 if cfg.tcp_keepalive_ms.is_some() {
408 warnings.push("Endpoint 'http' is used as a consumer, but 'tcp_keepalive_ms' is a publisher-only option and will be ignored.".to_string());
409 }
410 if cfg.pool_idle_timeout_ms.is_some() {
411 warnings.push(
412 "Endpoint 'http' is used as a consumer, but 'pool_idle_timeout_ms' is a publisher-only option and will be ignored."
413 .to_string(),
414 );
415 }
416 if cfg.stream_response_to.is_some() {
417 warnings.push("Endpoint 'http' is used as a consumer, but 'stream_response_to' is a publisher-only option and will be ignored.".to_string());
418 }
419 Ok(warnings)
420 }
421 #[cfg(feature = "sqlx")]
422 EndpointType::Sqlx(cfg) => {
423 if cfg.insert_query.is_some() {
424 warnings.push(
425 "Endpoint 'sqlx' is used as a consumer, but 'insert_query' is a publisher-only option and will be ignored."
426 .to_string()
427 );
428 }
429 Ok(warnings)
430 }
431 #[cfg(feature = "sled")]
432 EndpointType::Sled(_) => Ok(warnings),
433 EndpointType::Static(_) => Ok(warnings),
434 EndpointType::Memory(cfg) => {
435 if cfg.request_reply {
436 warnings.push(
437 "Endpoint 'memory' is used as a consumer, but 'request_reply' is a publisher-only option and will be ignored."
438 .to_string()
439 );
440 }
441 if cfg.request_timeout_ms.is_some() {
442 warnings.push(
443 "Endpoint 'memory' is used as a consumer, but 'request_timeout_ms' is a publisher-only option and will be ignored."
444 .to_string()
445 );
446 }
447 Ok(warnings)
448 }
449 EndpointType::StreamBuffer(cfg) => {
450 if cfg.correlation_id.is_none() {
451 return Err(anyhow!(
452 "[route:{}] stream_buffer consumer must specify 'correlation_id'",
453 route_name
454 ));
455 }
456 Ok(warnings)
457 }
458 EndpointType::File(_) => Ok(warnings),
459 #[cfg(feature = "websocket")]
460 EndpointType::WebSocket(_) => Ok(warnings),
461 EndpointType::Custom { .. } => Ok(warnings),
462 EndpointType::Switch(_) => Err(anyhow!(
463 "[route:{}] Switch endpoint is only supported as an output",
464 route_name
465 )),
466 EndpointType::Reader(_) => Err(anyhow!(
467 "[route:{}] Reader endpoint is only supported as an output",
468 route_name
469 )),
470 #[allow(unreachable_patterns)]
471 _ => {
472 if let Some(allowed) = allowed_types {
473 let name = endpoint.endpoint_type.name();
474 if allowed.contains(&name) {
475 return Ok(warnings);
476 }
477 }
478 Err(anyhow!(
479 "[route:{}] Unsupported consumer endpoint type '{:?}'",
480 route_name,
481 endpoint.endpoint_type
482 ))
483 }
484 }
485}
486
487fn resolve_endpoint(endpoint: &Endpoint, route_name: &str) -> Result<Endpoint> {
488 let mut visited = std::collections::HashSet::new();
489 resolve_endpoint_recursive(endpoint, route_name, &mut visited)
490}
491
492fn resolve_endpoint_recursive(
493 endpoint: &Endpoint,
494 route_name: &str,
495 visited: &mut std::collections::HashSet<String>,
496) -> Result<Endpoint> {
497 const MAX_DEPTH: usize = 16;
498 if visited.len() > MAX_DEPTH {
499 return Err(anyhow!(
500 "Reference recursion depth exceeded limit of {}",
501 MAX_DEPTH
502 ));
503 }
504
505 if let EndpointType::Ref(name) = &endpoint.endpoint_type {
506 if !visited.insert(name.clone()) {
507 return Err(anyhow!(
508 "[route:{}] Circular reference detected for endpoint '{}'",
509 route_name,
510 name
511 ));
512 }
513
514 let referenced_endpoint = crate::route::get_endpoint(name).ok_or_else(|| {
515 anyhow!(
516 "[route:{}] Referenced endpoint '{}' not found",
517 route_name,
518 name
519 )
520 })?;
521
522 let mut resolved = resolve_endpoint_recursive(&referenced_endpoint, route_name, visited)?;
523 let mut new_middlewares = endpoint.middlewares.clone();
526 new_middlewares.extend(resolved.middlewares);
527 resolved.middlewares = new_middlewares;
528 Ok(resolved)
529 } else {
530 Ok(endpoint.clone())
531 }
532}
533
534pub async fn create_consumer_from_route(
536 route_name: &str,
537 endpoint: &Endpoint,
538) -> Result<Box<dyn MessageConsumer>> {
539 let resolved_endpoint = resolve_endpoint(endpoint, route_name)?;
540 check_consumer(route_name, &resolved_endpoint, None)?;
541 let consumer = create_base_consumer(route_name, &resolved_endpoint).await?;
542 apply_middlewares_to_consumer(consumer, &resolved_endpoint, route_name).await
543}
544
545pub(crate) async fn try_run_fast_path_route(
546 route: &crate::models::Route,
547 name: &str,
548 shutdown_rx: async_channel::Receiver<()>,
549 ready_tx: Option<async_channel::Sender<()>>,
550) -> Option<anyhow::Result<bool>> {
551 #[cfg(feature = "http")]
552 {
553 let output_is_inline = matches!(
557 route.output.endpoint_type,
558 EndpointType::Response(_) | EndpointType::Static(_)
559 );
560 if let EndpointType::Http(cfg) = &route.input.endpoint_type {
561 if output_is_inline
562 && cfg.inline_response_fast_path_enabled()
563 && route.input.middlewares.is_empty()
564 && output_middlewares_allow_http_inline_fast_path(&route.output.middlewares)
565 && !cfg.fire_and_forget
566 {
567 return Some(
568 run_http_inline_response_fast_path(
569 route,
570 name,
571 shutdown_rx,
572 ready_tx,
573 cfg.clone(),
574 )
575 .await,
576 );
577 }
578 }
579 }
580
581 #[cfg(feature = "websocket")]
582 {
583 if let EndpointType::WebSocket(cfg) = &route.input.endpoint_type {
584 match websocket_direct_route_support(route) {
585 WebSocketDirectRouteSupport::Supported => {
586 return Some(
587 websocket::run_direct_response_route(
588 name,
589 cfg.clone(),
590 route.output.handler.clone(),
591 shutdown_rx,
592 ready_tx,
593 )
594 .await,
595 );
596 }
597 WebSocketDirectRouteSupport::Unsupported(reason) => match cfg.execution_mode {
598 crate::models::WebSocketExecutionMode::Auto => {
599 tracing::warn!(
600 route = name,
601 reason = reason,
602 "WebSocket route cannot run in direct mode; falling back to routed mode"
603 );
604 }
605 crate::models::WebSocketExecutionMode::DirectOnly => {
606 return Some(Err(anyhow!(
607 "WebSocket route '{}' is configured for direct_only, but direct mode is unsupported: {}",
608 name,
609 reason
610 )));
611 }
612 crate::models::WebSocketExecutionMode::Routed => {}
613 },
614 }
615 }
616 }
617
618 let _ = route;
619 let _ = name;
620 let _ = shutdown_rx;
621 let _ = ready_tx;
622 None
623}
624
625#[cfg(feature = "websocket")]
626enum WebSocketDirectRouteSupport {
627 Supported,
628 Unsupported(&'static str),
629}
630
631#[cfg(feature = "websocket")]
632fn websocket_direct_route_support(route: &crate::models::Route) -> WebSocketDirectRouteSupport {
633 let EndpointType::WebSocket(cfg) = &route.input.endpoint_type else {
634 return WebSocketDirectRouteSupport::Unsupported("input is not websocket");
635 };
636
637 if cfg.execution_mode == crate::models::WebSocketExecutionMode::Routed {
638 return WebSocketDirectRouteSupport::Unsupported("execution_mode is routed");
639 }
640 if !matches!(route.output.endpoint_type, EndpointType::Response(_)) {
641 return WebSocketDirectRouteSupport::Unsupported("output is not response");
642 }
643 if !websocket_direct_route_options_allowed(&route.options) {
644 return WebSocketDirectRouteSupport::Unsupported(
645 "custom route options require routed mode",
646 );
647 }
648 if !route.input.middlewares.is_empty() || !route.output.middlewares.is_empty() {
649 return WebSocketDirectRouteSupport::Unsupported("middleware requires routed mode");
650 }
651
652 WebSocketDirectRouteSupport::Supported
653}
654
655#[cfg(feature = "websocket")]
656fn websocket_direct_route_options_allowed(options: &crate::models::RouteOptions) -> bool {
657 let mut defaults = crate::models::RouteOptions::default();
658 defaults.description.clone_from(&options.description);
659 options == &defaults
660}
661
662#[cfg(feature = "http")]
663fn output_middlewares_allow_http_inline_fast_path(middlewares: &[Middleware]) -> bool {
664 middlewares.iter().all(|middleware| {
665 matches!(
666 middleware,
667 Middleware::Buffer(_)
668 | Middleware::Delay(_)
669 | Middleware::Limiter(_)
670 | Middleware::Metrics(_)
671 )
672 })
673}
674
675#[cfg(feature = "http")]
676async fn run_http_inline_response_fast_path(
677 route: &crate::models::Route,
678 name: &str,
679 shutdown_rx: async_channel::Receiver<()>,
680 ready_tx: Option<async_channel::Sender<()>>,
681 http_config: crate::models::HttpConfig,
682) -> anyhow::Result<bool> {
683 let publisher = create_publisher_from_route(name, &route.output).await?;
684 let consumer =
685 http::HttpConsumer::new_with_inline_publisher(&http_config, Some(publisher.clone()))
686 .await?;
687
688 if let Err(err) = crate::route::run_publisher_connect_hook(name, &publisher).await {
689 crate::route::run_publisher_disconnect_hook(name, &publisher).await;
690 return Err(err);
691 }
692 if let Err(err) = crate::route::run_consumer_connect_hook(name, &consumer).await {
693 crate::route::run_consumer_disconnect_hook(name, &consumer).await;
694 crate::route::run_publisher_disconnect_hook(name, &publisher).await;
695 return Err(err);
696 }
697
698 tracing::info!(
699 route = name,
700 has_output_handler = route.output.handler.is_some(),
701 output_middlewares = route.output.middlewares.len(),
702 "Running HTTP inline response fast path; bypassing the normal route consumer/worker/disposition pipeline while keeping the output publisher chain active"
703 );
704 tracing::debug!(
705 route = name,
706 "HTTP inline response fast path differences: no input middlewares, no fire-and-forget, only buffer/metrics output middlewares allowed, and unchanged request metadata is not echoed back as response headers"
707 );
708 if let Some(tx) = ready_tx {
709 let _ = tx.send(()).await;
710 }
711
712 let stopped = shutdown_rx.recv().await.is_ok();
713 if stopped {
714 tracing::info!(
715 "Shutdown signal received in HTTP inline response runner for route '{}'.",
716 name
717 );
718 }
719 crate::route::run_consumer_disconnect_hook(name, &consumer).await;
720 crate::route::run_publisher_disconnect_hook(name, &publisher).await;
721 Ok(true)
722}
723
724async fn create_base_consumer(
725 route_name: &str,
726 endpoint: &Endpoint,
727) -> Result<Box<dyn MessageConsumer>> {
728 fn boxed<T: MessageConsumer + 'static>(c: T) -> Box<dyn MessageConsumer> {
730 Box::new(c)
731 }
732
733 match &endpoint.endpoint_type {
734 #[cfg(feature = "aws")]
735 EndpointType::Aws(cfg) => Ok(boxed(aws::AwsConsumer::new(cfg).await?)),
736 #[cfg(feature = "kafka")]
737 EndpointType::Kafka(cfg) => {
738 let mut config = cfg.clone();
739 if config.topic.is_none() {
740 config.topic = Some(route_name.to_string());
741 }
742 Ok(boxed(kafka::KafkaConsumer::new(&config).await?))
743 }
744 #[cfg(feature = "nats")]
745 EndpointType::Nats(cfg) => {
746 let mut config = cfg.clone();
747 if config.subject.is_none() {
748 config.subject = Some(route_name.to_string());
749 }
750 Ok(boxed(nats::NatsConsumer::new(&config).await?))
751 }
752 #[cfg(feature = "amqp")]
753 EndpointType::Amqp(cfg) => {
754 let mut config = cfg.clone();
755 if config.queue.is_none() {
756 config.queue = Some(route_name.to_string());
757 }
758 Ok(boxed(amqp::AmqpConsumer::new(&config).await?))
759 }
760 #[cfg(feature = "mqtt")]
761 EndpointType::Mqtt(cfg) => {
762 let mut config = cfg.clone();
763 if config.topic.is_none() {
764 config.topic = Some(route_name.to_string());
765 }
766 if config.client_id.is_none() && !config.clean_session {
767 config.client_id = Some(format!("{}-{}", crate::APP_NAME, route_name));
769 }
770 Ok(boxed(mqtt::MqttConsumer::new(&config).await?))
771 }
772 #[cfg(feature = "ibm-mq")]
773 EndpointType::IbmMq(cfg) => {
774 let mut config = cfg.clone();
775 if config.queue.is_none() && config.topic.is_none() {
776 config.queue = Some(route_name.to_string());
777 }
778 Ok(boxed(ibm_mq::IbmMqConsumer::new(&config).await?))
779 }
780 #[cfg(feature = "zeromq")]
781 EndpointType::ZeroMq(cfg) => Ok(boxed(zeromq::ZeroMqConsumer::new(cfg).await?)),
782 EndpointType::File(cfg) => Ok(boxed(file::FileConsumer::new(cfg).await?)),
783 #[cfg(feature = "grpc")]
784 EndpointType::Grpc(cfg) => {
785 let mut config = cfg.clone();
786 if config.topic.is_none() {
787 config.topic = Some(route_name.to_string());
788 }
789 Ok(boxed(grpc::GrpcConsumer::new(&config).await?))
790 }
791 #[cfg(feature = "sqlx")]
792 EndpointType::Sqlx(cfg) => Ok(boxed(sqlx::SqlxConsumer::new(cfg).await?)),
793 #[cfg(feature = "http")]
794 EndpointType::Http(cfg) => Ok(boxed(http::HttpConsumer::new(cfg).await?)),
795 #[cfg(feature = "websocket")]
796 EndpointType::WebSocket(cfg) => Ok(boxed(websocket::WebSocketConsumer::new(cfg).await?)),
797 EndpointType::Static(cfg) => Ok(boxed(static_endpoint::StaticRequestConsumer::new(cfg)?)),
798 EndpointType::Memory(cfg) => Ok(boxed(memory::MemoryConsumer::new_async(cfg).await?)),
799 EndpointType::StreamBuffer(cfg) => {
800 Ok(boxed(stream_buffer::StreamBufferConsumer::new(cfg)?))
801 }
802 #[cfg(feature = "sled")]
803 EndpointType::Sled(cfg) => Ok(boxed(sled::SledConsumer::new(cfg)?)),
804 #[cfg(feature = "mongodb")]
805 EndpointType::MongoDb(cfg) => {
806 let mut config = cfg.clone();
807 if config.collection.is_none() {
808 config.collection = Some(route_name.to_string());
809 }
810 if config.change_stream {
811 if config.ttl_seconds.is_none() {
812 config.ttl_seconds = Some(86400); }
814 Ok(boxed(mongodb::MongoDbSubscriber::new(&config).await?))
815 } else {
816 Ok(boxed(mongodb::MongoDbConsumer::new(&config).await?))
817 }
818 }
819 EndpointType::Custom { name, config } => {
820 let factory = get_endpoint_factory(name)
821 .ok_or_else(|| anyhow!("Custom endpoint factory '{}' not found", name))?;
822 factory.create_consumer(route_name, config).await
823 }
824 EndpointType::Switch(_) => Err(anyhow!(
825 "[route:{}] Switch endpoint is only supported as an output",
826 route_name
827 )),
828 #[allow(unreachable_patterns)]
829 _ => Err(anyhow!(
830 "[route:{}] Unsupported consumer endpoint type '{:?}'",
831 route_name,
832 endpoint.endpoint_type
833 )),
834 }
835}
836
837pub fn check_publisher(
839 route_name: &str,
840 endpoint: &Endpoint,
841 allowed_types: Option<&[&str]>,
842) -> Result<Vec<String>> {
843 check_publisher_recursive(route_name, endpoint, 0, allowed_types)
844}
845
846fn check_publisher_recursive(
847 route_name: &str,
848 endpoint: &Endpoint,
849 depth: usize,
850 allowed_types: Option<&[&str]>,
851) -> Result<Vec<String>> {
852 let mut warnings = Vec::new();
853 if let Some(allowed) = allowed_types {
854 if !endpoint.endpoint_type.is_core() {
855 let name = endpoint.endpoint_type.name();
856 if !allowed.contains(&name) {
857 return Err(anyhow!(
858 "[route:{}] Endpoint type '{}' is not allowed by policy",
859 route_name,
860 name
861 ));
862 }
863 }
864 }
865 const MAX_DEPTH: usize = 16;
866 if depth > MAX_DEPTH {
867 return Err(anyhow!(
868 "Fanout recursion depth exceeded limit of {}",
869 MAX_DEPTH
870 ));
871 }
872 match &endpoint.endpoint_type {
873 EndpointType::Ref(name) => {
874 let referenced = crate::route::get_endpoint(name).ok_or_else(|| {
875 anyhow!(
876 "[route:{}] Referenced endpoint '{}' not found in endpoint registry",
877 route_name,
878 name
879 )
880 });
881 if let Ok(referenced) = referenced {
882 warnings.extend(check_publisher_recursive(
883 route_name,
884 &referenced,
885 depth + 1,
886 allowed_types,
887 )?);
888 return Ok(warnings);
889 }
890 if crate::publisher::get_publisher(name).is_some() {
891 return Ok(warnings);
892 }
893 Err(anyhow!(
894 "[route:{}] Referenced endpoint '{}' not found in any registry",
895 route_name,
896 name
897 ))
898 }
899 #[cfg(feature = "aws")]
900 EndpointType::Aws(cfg) => {
901 if cfg.max_messages.is_some() {
902 warnings.push(
903 "Endpoint 'aws' is used as a publisher, but 'max_messages' is a consumer-only option and will be ignored."
904 .to_string()
905 );
906 }
907 if cfg.wait_time_seconds.is_some() {
908 warnings.push(
909 "Endpoint 'aws' is used as a publisher, but 'wait_time_seconds' is a consumer-only option and will be ignored."
910 .to_string()
911 );
912 }
913 Ok(warnings)
914 }
915 #[cfg(feature = "kafka")]
916 EndpointType::Kafka(cfg) => {
917 if cfg.group_id.is_some() {
918 warnings.push(
919 "Endpoint 'kafka' is used as a publisher, but 'group_id' is a consumer-only option and will be ignored."
920 .to_string()
921 );
922 }
923 if cfg.consumer_options.is_some() {
924 warnings.push(
925 "Endpoint 'kafka' is used as a publisher, but 'consumer_options' is a consumer-only option and will be ignored."
926 .to_string()
927 );
928 }
929 Ok(warnings)
930 }
931 #[cfg(feature = "nats")]
932 EndpointType::Nats(cfg) => {
933 if cfg.stream.is_some() {
934 warnings.push(
935 "Endpoint 'nats' is used as a publisher, but 'stream' is a consumer-only option and will be ignored."
936 .to_string()
937 );
938 }
939 if cfg.subscriber_mode {
940 warnings.push(
941 "Endpoint 'nats' is used as a publisher, but 'subscriber_mode' is a consumer-only option and will be ignored."
942 .to_string()
943 );
944 }
945 if cfg.prefetch_count.is_some() {
946 warnings.push(
947 "Endpoint 'nats' is used as a publisher, but 'prefetch_count' is a consumer-only option and will be ignored."
948 .to_string()
949 );
950 }
951 Ok(warnings)
952 }
953 #[cfg(feature = "amqp")]
954 EndpointType::Amqp(cfg) => {
955 if cfg.subscribe_mode {
956 warnings.push(
957 "Endpoint 'amqp' is used as a publisher, but 'subscribe_mode' is a consumer-only option and will be ignored."
958 .to_string()
959 );
960 }
961 if cfg.prefetch_count.is_some() {
962 warnings.push(
963 "Endpoint 'amqp' is used as a publisher, but 'prefetch_count' is a consumer-only option and will be ignored."
964 .to_string()
965 );
966 }
967 Ok(warnings)
968 }
969 #[cfg(feature = "mqtt")]
970 EndpointType::Mqtt(cfg) => {
971 if cfg.clean_session {
972 warnings.push(
973 "Endpoint 'mqtt' is used as a publisher, but 'clean_session' is a consumer-only option and will be ignored."
974 .to_string()
975 );
976 }
977 Ok(warnings)
978 }
979 #[cfg(feature = "zeromq")]
980 EndpointType::ZeroMq(cfg) => {
981 if cfg.topic.is_some() {
982 warnings.push(
983 "Endpoint 'zeromq' is used as a publisher, but 'topic' is a consumer-only option and will be ignored."
984 .to_string()
985 );
986 }
987 Ok(warnings)
988 }
989
990 #[cfg(feature = "http")]
991 EndpointType::Http(_cfg) => {
992 if _cfg.path.is_some() {
993 warnings.push(
994 "Endpoint 'http' is used as a publisher, but 'path' is a consumer-only option and will be ignored."
995 .to_string()
996 );
997 }
998 if _cfg.workers.is_some() {
999 warnings.push(
1000 "Endpoint 'http' is used as a publisher, but 'workers' is a consumer-only option and will be ignored."
1001 .to_string()
1002 );
1003 }
1004 if _cfg.message_id_header.is_some() {
1005 warnings.push(
1006 "Endpoint 'http' is used as a publisher, but 'message_id_header' is a consumer-only option and will be ignored."
1007 .to_string()
1008 );
1009 }
1010 if _cfg.internal_buffer_size.is_some() {
1011 warnings.push(
1012 "Endpoint 'http' is used as a publisher, but 'internal_buffer_size' is a consumer-only option and will be ignored."
1013 .to_string()
1014 );
1015 }
1016 if _cfg.fire_and_forget {
1017 warnings.push(
1018 "Endpoint 'http' is used as a publisher, but 'fire_and_forget' is a consumer-only option and will be ignored."
1019 .to_string()
1020 );
1021 }
1022 if _cfg.receive_streamable {
1023 warnings.push(
1024 "Endpoint 'http' is used as a publisher, but 'receive_streamable' is a consumer-only option and will be ignored."
1025 .to_string()
1026 );
1027 }
1028 Ok(warnings)
1029 }
1030 #[cfg(feature = "grpc")]
1031 EndpointType::Grpc(_) => Ok(warnings),
1032 #[cfg(feature = "sqlx")]
1033 EndpointType::Sqlx(cfg) => {
1034 if cfg.select_query.is_some() {
1035 warnings.push(
1036 "Endpoint 'sqlx' is used as a publisher, but 'select_query' is a consumer-only option and will be ignored."
1037 .to_string()
1038 );
1039 }
1040 if cfg.delete_after_read {
1041 warnings.push(
1042 "Endpoint 'sqlx' is used as a publisher, but 'delete_after_read' is a consumer-only option and will be ignored."
1043 .to_string()
1044 );
1045 }
1046 if cfg.polling_interval_ms.is_some() {
1047 warnings.push(
1048 "Endpoint 'sqlx' is used as a publisher, but 'polling_interval_ms' is a consumer-only option and will be ignored."
1049 .to_string()
1050 );
1051 }
1052 Ok(warnings)
1053 }
1054 #[cfg(feature = "ibm-mq")]
1055 EndpointType::IbmMq(cfg) => {
1056 if cfg.wait_timeout_ms != 1000 {
1057 warnings.push(
1058 "Endpoint 'ibmmq' is used as a publisher, but 'wait_timeout_ms' is a consumer-only option and will be ignored."
1059 .to_string()
1060 );
1061 }
1062 Ok(warnings)
1063 }
1064 #[cfg(feature = "mongodb")]
1065 EndpointType::MongoDb(cfg) => {
1066 if cfg.polling_interval_ms.is_some() {
1067 warnings.push(
1068 "Endpoint 'mongodb' is used as a publisher, but 'polling_interval_ms' is a consumer-only option and will be ignored."
1069 .to_string()
1070 );
1071 }
1072 if cfg.change_stream {
1073 warnings.push(
1074 "Endpoint 'mongodb' is used as a publisher, but 'change_stream' is a consumer-only option and will be ignored."
1075 .to_string()
1076 );
1077 }
1078 if cfg.cursor_id.is_some() {
1079 warnings.push(
1080 "Endpoint 'mongodb' is used as a publisher, but 'cursor_id' is a consumer-only option and will be ignored."
1081 .to_string()
1082 );
1083 }
1084 Ok(warnings)
1085 }
1086 EndpointType::File(_) => Ok(warnings),
1087 #[cfg(feature = "websocket")]
1088 EndpointType::WebSocket(_) => Ok(warnings),
1089 EndpointType::Static(_) => Ok(warnings),
1090 EndpointType::Memory(cfg) => {
1091 if cfg.subscribe_mode {
1092 warnings.push(
1093 "Endpoint 'memory' is used as a publisher, but 'subscribe_mode' is a consumer-only option and will be ignored."
1094 .to_string()
1095 );
1096 }
1097 if cfg.enable_nack {
1098 warnings.push(
1099 "Endpoint 'memory' is used as a publisher, but 'enable_nack' is a consumer-only option and will be ignored."
1100 .to_string()
1101 );
1102 }
1103 Ok(warnings)
1104 }
1105 EndpointType::StreamBuffer(cfg) => {
1106 if cfg.correlation_id.is_some() {
1107 warnings.push(
1108 "Endpoint 'stream_buffer' is used as a publisher, but 'correlation_id' is a consumer-only option and will be ignored."
1109 .to_string()
1110 );
1111 }
1112 Ok(warnings)
1113 }
1114 #[cfg(feature = "sled")]
1115 EndpointType::Sled(cfg) => {
1116 if cfg.read_from_start {
1117 warnings.push(
1118 "Endpoint 'sled' is used as a publisher, but 'read_from_start' is a consumer-only option and will be ignored."
1119 .to_string()
1120 );
1121 }
1122 if cfg.delete_after_read {
1123 warnings.push(
1124 "Endpoint 'sled' is used as a publisher, but 'delete_after_read' is a consumer-only option and will be ignored."
1125 .to_string()
1126 );
1127 }
1128 Ok(warnings)
1129 }
1130 EndpointType::Null => Ok(warnings),
1131 EndpointType::Fanout(endpoints) => {
1132 for endpoint in endpoints {
1133 warnings.extend(check_publisher_recursive(
1134 route_name,
1135 endpoint,
1136 depth + 1,
1137 allowed_types,
1138 )?);
1139 }
1140 Ok(warnings)
1141 }
1142 EndpointType::Switch(cfg) => {
1143 for endpoint in cfg.cases.values() {
1144 warnings.extend(check_publisher_recursive(
1145 route_name,
1146 endpoint,
1147 depth + 1,
1148 allowed_types,
1149 )?);
1150 }
1151 if let Some(endpoint) = &cfg.default {
1152 warnings.extend(check_publisher_recursive(
1153 route_name,
1154 endpoint,
1155 depth + 1,
1156 allowed_types,
1157 )?);
1158 }
1159 Ok(warnings)
1160 }
1161 EndpointType::Response(_) => Ok(warnings),
1162 EndpointType::Custom { .. } => Ok(warnings),
1163 EndpointType::Reader(inner) => check_consumer(route_name, inner, allowed_types),
1164 #[allow(unreachable_patterns)]
1165 _ => {
1166 if let Some(allowed) = allowed_types {
1167 let name = endpoint.endpoint_type.name();
1168 if allowed.contains(&name) {
1169 return Ok(warnings);
1170 }
1171 }
1172 Err(anyhow!(
1173 "[route:{}] Unsupported publisher endpoint type '{:?}'",
1174 route_name,
1175 endpoint.endpoint_type
1176 ))
1177 }
1178 }
1179}
1180
1181pub async fn create_publisher_from_route(
1183 route_name: &str,
1184 endpoint: &Endpoint,
1185) -> Result<Arc<dyn MessagePublisher>> {
1186 check_publisher(route_name, endpoint, None)?;
1187 create_publisher_with_depth(route_name.to_string(), endpoint.clone(), 0).await
1188}
1189
1190fn create_publisher_with_depth(
1191 route_name: String,
1192 endpoint: Endpoint,
1193 depth: usize,
1194) -> BoxFuture<'static, Result<Arc<dyn MessagePublisher>>> {
1195 Box::pin(async move {
1196 const MAX_DEPTH: usize = 16;
1197 if depth > MAX_DEPTH {
1198 return Err(anyhow!(
1199 "Fanout/Ref recursion depth exceeded limit of {}",
1200 MAX_DEPTH
1201 ));
1202 }
1203
1204 if let EndpointType::Ref(name) = &endpoint.endpoint_type {
1205 let referenced_opt = crate::route::get_endpoint(name);
1206
1207 if referenced_opt.is_none() {
1208 if let Some(pub_instance) = crate::publisher::get_publisher(name) {
1209 let inner = pub_instance.inner();
1210 let mut publisher: Box<dyn MessagePublisher> = Box::new(inner);
1211
1212 if let Some(handler) = &endpoint.handler {
1213 publisher = Box::new(crate::command_handler::CommandPublisher::new(
1214 publisher,
1215 handler.clone(),
1216 ));
1217 }
1218 return crate::middleware::apply_middlewares_to_publisher(
1219 publisher,
1220 &endpoint,
1221 &route_name,
1222 )
1223 .await;
1224 }
1225 }
1226
1227 let referenced = referenced_opt.ok_or_else(|| {
1228 anyhow!(
1229 "[route:{}] Referenced endpoint '{}' not found",
1230 route_name,
1231 name
1232 )
1233 })?;
1234
1235 let mut merged = referenced;
1236 merged.middlewares.extend(endpoint.middlewares);
1239
1240 if endpoint.handler.is_some() {
1241 if merged.handler.is_some() {
1242 return Err(anyhow!("[route:{}] Both ref endpoint and referenced endpoint '{}' have handlers defined. This is ambiguous.", route_name, name));
1243 }
1244 merged.handler = endpoint.handler;
1245 }
1246
1247 return create_publisher_with_depth(route_name, merged, depth + 1).await;
1248 }
1249
1250 let mut publisher =
1251 create_base_publisher(&route_name, &endpoint.endpoint_type, depth).await?;
1252 if let Some(handler) = &endpoint.handler {
1253 publisher = Box::new(crate::command_handler::CommandPublisher::new(
1254 publisher,
1255 handler.clone(),
1256 ));
1257 }
1258 crate::middleware::apply_middlewares_to_publisher(publisher, &endpoint, &route_name).await
1259 })
1260}
1261
1262async fn create_base_publisher(
1263 route_name: &str,
1264 endpoint_type: &EndpointType,
1265 depth: usize,
1266) -> Result<Box<dyn MessagePublisher>> {
1267 let publisher = match endpoint_type {
1268 #[cfg(feature = "aws")]
1269 EndpointType::Aws(cfg) => {
1270 Ok(Box::new(aws::AwsPublisher::new(cfg).await?) as Box<dyn MessagePublisher>)
1271 }
1272 #[cfg(feature = "kafka")]
1273 EndpointType::Kafka(cfg) => {
1274 let mut config = cfg.clone();
1275 if config.topic.is_none() {
1276 config.topic = Some(route_name.to_string());
1277 }
1278 Ok(Box::new(kafka::KafkaPublisher::new(&config).await?) as Box<dyn MessagePublisher>)
1279 }
1280 #[cfg(feature = "nats")]
1281 EndpointType::Nats(cfg) => {
1282 let mut config = cfg.clone();
1283 if config.subject.is_none() {
1284 config.subject = Some(route_name.to_string());
1285 }
1286 Ok(Box::new(nats::NatsPublisher::new(&config).await?) as Box<dyn MessagePublisher>)
1287 }
1288 #[cfg(feature = "amqp")]
1289 EndpointType::Amqp(cfg) => {
1290 let mut config = cfg.clone();
1291 if config.queue.is_none() {
1292 config.queue = Some(route_name.to_string());
1293 }
1294 Ok(Box::new(amqp::AmqpPublisher::new(&config).await?) as Box<dyn MessagePublisher>)
1295 }
1296 #[cfg(feature = "mqtt")]
1297 EndpointType::Mqtt(cfg) => {
1298 let mut config = cfg.clone();
1299 if config.topic.is_none() {
1300 config.topic = Some(route_name.to_string());
1301 }
1302 if config.client_id.is_none() {
1303 config.client_id = Some(format!("{}-{}", crate::APP_NAME, route_name));
1304 }
1305 Ok(Box::new(mqtt::MqttPublisher::new(&config).await?) as Box<dyn MessagePublisher>)
1306 }
1307 #[cfg(feature = "zeromq")]
1308 EndpointType::ZeroMq(cfg) => {
1309 Ok(Box::new(zeromq::ZeroMqPublisher::new(cfg).await?) as Box<dyn MessagePublisher>)
1310 }
1311 #[cfg(feature = "grpc")]
1312 EndpointType::Grpc(cfg) => {
1313 Ok(Box::new(grpc::GrpcPublisher::new(cfg).await?) as Box<dyn MessagePublisher>)
1314 }
1315 #[cfg(feature = "sqlx")]
1316 EndpointType::Sqlx(cfg) => {
1317 Ok(Box::new(sqlx::SqlxPublisher::new(cfg).await?) as Box<dyn MessagePublisher>)
1318 }
1319 #[cfg(feature = "http")]
1320 EndpointType::Http(cfg) => {
1321 let stream_response_sink =
1322 if let Some(stream_response_to) = cfg.stream_response_to.as_deref() {
1323 Some(
1324 create_publisher_with_depth(
1325 route_name.to_string(),
1326 stream_response_to.clone(),
1327 depth + 1,
1328 )
1329 .await?,
1330 )
1331 } else {
1332 None
1333 };
1334 let sink =
1335 http::HttpPublisher::new_with_stream_response_sink(cfg, stream_response_sink)
1336 .await?;
1337 Ok(Box::new(sink) as Box<dyn MessagePublisher>)
1338 }
1339 #[cfg(feature = "websocket")]
1340 EndpointType::WebSocket(cfg) => {
1341 let sink = websocket::WebSocketPublisher::new(cfg);
1342 Ok(Box::new(sink) as Box<dyn MessagePublisher>)
1343 }
1344 #[cfg(feature = "mongodb")]
1345 EndpointType::MongoDb(cfg) => {
1346 let mut config = cfg.clone();
1347 if config.collection.is_none() {
1348 config.collection = Some(route_name.to_string());
1349 }
1350 Ok(Box::new(mongodb::MongoDbPublisher::new(&config).await?)
1351 as Box<dyn MessagePublisher>)
1352 }
1353 EndpointType::File(cfg) => {
1354 Ok(Box::new(file::FilePublisher::new(cfg).await?) as Box<dyn MessagePublisher>)
1355 }
1356 EndpointType::Static(cfg) => Ok(Box::new(static_endpoint::StaticEndpointPublisher::new(
1357 cfg,
1358 )?) as Box<dyn MessagePublisher>),
1359 EndpointType::Memory(cfg) => {
1360 Ok(Box::new(memory::MemoryPublisher::new_async(cfg).await?)
1361 as Box<dyn MessagePublisher>)
1362 }
1363 EndpointType::StreamBuffer(cfg) => {
1364 Ok(Box::new(stream_buffer::StreamBufferPublisher::new(cfg)?)
1365 as Box<dyn MessagePublisher>)
1366 }
1367 #[cfg(feature = "sled")]
1368 EndpointType::Sled(cfg) => {
1369 Ok(Box::new(sled::SledPublisher::new(cfg)?) as Box<dyn MessagePublisher>)
1370 }
1371 #[cfg(feature = "ibm-mq")]
1372 EndpointType::IbmMq(cfg) => {
1373 Ok(Box::new(ibm_mq::IbmMqPublisher::new(cfg).await?) as Box<dyn MessagePublisher>)
1374 }
1375 EndpointType::Null => Ok(Box::new(null::NullPublisher) as Box<dyn MessagePublisher>),
1376 EndpointType::Fanout(endpoints) => {
1377 let mut publishers = Vec::with_capacity(endpoints.len());
1378 for endpoint in endpoints {
1379 let p = create_publisher_with_depth(
1380 route_name.to_string(),
1381 endpoint.clone(),
1382 depth + 1,
1383 )
1384 .await?;
1385 publishers.push(p);
1386 }
1387 Ok(Box::new(fanout::FanoutPublisher::new(publishers)) as Box<dyn MessagePublisher>)
1388 }
1389 EndpointType::Switch(cfg) => {
1390 let mut cases = std::collections::HashMap::new();
1391 for (key, endpoint) in &cfg.cases {
1392 let p = create_publisher_with_depth(
1393 route_name.to_string(),
1394 endpoint.clone(),
1395 depth + 1,
1396 )
1397 .await?;
1398 cases.insert(key.clone(), p);
1399 }
1400 let default = if let Some(endpoint) = &cfg.default {
1401 Some(
1402 create_publisher_with_depth(
1403 route_name.to_string(),
1404 (**endpoint).clone(),
1405 depth + 1,
1406 )
1407 .await?,
1408 )
1409 } else {
1410 None
1411 };
1412 Ok(Box::new(switch::SwitchPublisher::new(
1413 cfg.metadata_key.clone(),
1414 cases,
1415 default,
1416 )) as Box<dyn MessagePublisher>)
1417 }
1418 EndpointType::Response(_) => {
1419 Ok(Box::new(response::ResponsePublisher) as Box<dyn MessagePublisher>)
1420 }
1421 EndpointType::Reader(inner) => {
1422 let consumer = create_consumer_from_route(route_name, inner).await?;
1423 Ok(Box::new(reader::ReaderPublisher::new(consumer)) as Box<dyn MessagePublisher>)
1424 }
1425 EndpointType::Custom { name, config } => {
1426 let factory = get_endpoint_factory(name)
1427 .ok_or_else(|| anyhow!("Custom endpoint factory '{}' not found", name))?;
1428 factory.create_publisher(route_name, config).await
1429 }
1430 #[allow(unreachable_patterns)]
1431 _ => Err(anyhow!(
1432 "[route:{}] Unsupported publisher endpoint type '{:?}'",
1433 route_name,
1434 endpoint_type
1435 )),
1436 }?;
1437 Ok(publisher)
1438}
1439
1440#[cfg(feature = "rustls")]
1450#[allow(unused)]
1451pub(crate) fn get_crypto_provider() -> anyhow::Result<std::sync::Arc<rustls::crypto::CryptoProvider>>
1452{
1453 rustls::crypto::CryptoProvider::get_default()
1454 .cloned()
1455 .ok_or_else(|| {
1456 anyhow!("No rustls CryptoProvider is installed.\n\
1457Fix: enable the `rustls-ring` or `rustls-aws-lc` feature of mq-bridge, or call `rustls::crypto::CryptoProvider::install_default()` in your application binary before creating any TLS endpoint.")
1458 })
1459}
1460
1461#[cfg(test)]
1462mod tests {
1463 use super::*;
1464 use crate::models::{Endpoint, EndpointType};
1465 use crate::CanonicalMessage;
1466
1467 #[tokio::test]
1468 async fn test_fanout_publisher_integration() {
1469 let ep1 = Endpoint::new_memory("fanout_1", 10);
1470 let ep2 = Endpoint::new_memory("fanout_2", 10);
1471
1472 let chan1 = ep1.channel().unwrap();
1473 let chan2 = ep2.channel().unwrap();
1474 let fanout_ep = Endpoint::new(EndpointType::Fanout(vec![ep1, ep2]));
1475
1476 let publisher = create_publisher_from_route("test_fanout", &fanout_ep)
1477 .await
1478 .expect("Failed to create fanout publisher");
1479
1480 let msg = CanonicalMessage::new(b"fanout_payload".to_vec(), None);
1481 publisher.send(msg).await.expect("Failed to send message");
1482
1483 assert_eq!(chan1.len(), 1);
1484 assert_eq!(chan2.len(), 1);
1485
1486 let msg1 = chan1.drain_messages().pop().unwrap();
1487 let msg2 = chan2.drain_messages().pop().unwrap();
1488
1489 assert_eq!(msg1.payload, "fanout_payload".as_bytes());
1490 assert_eq!(msg2.payload, "fanout_payload".as_bytes());
1491 }
1492
1493 use crate::models::MemoryConfig;
1494 #[tokio::test]
1495 async fn test_factory_creates_memory_subscriber() {
1496 let endpoint = Endpoint {
1497 endpoint_type: EndpointType::Memory(
1498 MemoryConfig::new("mem".to_string(), None).with_subscribe(true),
1499 ),
1500 middlewares: vec![],
1501 handler: None,
1502 };
1503
1504 let consumer = create_consumer_from_route("test", &endpoint).await.unwrap();
1505 let is_subscriber = consumer
1507 .as_any()
1508 .is::<crate::endpoints::memory::MemoryConsumer>();
1509 assert!(is_subscriber, "Factory should create MemoryConsumer");
1510 }
1511
1512 #[cfg(feature = "websocket")]
1513 #[test]
1514 fn websocket_direct_route_support_requires_default_route_options() {
1515 let mut options = crate::models::RouteOptions::default();
1516 assert!(websocket_direct_route_options_allowed(&options));
1517
1518 options.batch_size = 128;
1519 assert!(!websocket_direct_route_options_allowed(&options));
1520 }
1521
1522 #[cfg(feature = "websocket")]
1523 #[test]
1524 fn websocket_direct_route_support_respects_execution_mode_and_output() {
1525 let input = Endpoint::new(EndpointType::WebSocket(
1526 crate::models::WebSocketConfig::new("127.0.0.1:0"),
1527 ));
1528 let response_route = crate::models::Route::new(input.clone(), Endpoint::new_response());
1529 assert!(matches!(
1530 websocket_direct_route_support(&response_route),
1531 WebSocketDirectRouteSupport::Supported
1532 ));
1533
1534 let memory_route = crate::models::Route::new(input.clone(), Endpoint::new_memory("ws", 1));
1535 assert!(matches!(
1536 websocket_direct_route_support(&memory_route),
1537 WebSocketDirectRouteSupport::Unsupported("output is not response")
1538 ));
1539
1540 let routed_input = Endpoint::new(EndpointType::WebSocket(
1541 crate::models::WebSocketConfig::new("127.0.0.1:0")
1542 .with_execution_mode(crate::models::WebSocketExecutionMode::Routed),
1543 ));
1544 let routed_route = crate::models::Route::new(routed_input, Endpoint::new_response());
1545 assert!(matches!(
1546 websocket_direct_route_support(&routed_route),
1547 WebSocketDirectRouteSupport::Unsupported("execution_mode is routed")
1548 ));
1549 }
1550
1551 #[test]
1552 fn test_endpoint_middleware_ordering_helpers() {
1553 let endpoint = Endpoint::new_memory("test", 10)
1554 .with_metrics()
1555 .with_dlq(crate::models::DeadLetterQueueMiddleware::default())
1556 .with_retry(crate::models::RetryMiddleware::default());
1557
1558 assert_eq!(endpoint.middlewares.len(), 3);
1560 assert!(matches!(endpoint.middlewares[0], Middleware::Retry(_)));
1561 assert!(matches!(endpoint.middlewares[1], Middleware::Dlq(_)));
1562 assert!(matches!(endpoint.middlewares[2], Middleware::Metrics(_)));
1563 }
1564
1565 #[cfg(feature = "http")]
1566 #[test]
1567 fn test_http_inline_fast_path_allows_simple_output_publisher_middlewares() {
1568 assert!(output_middlewares_allow_http_inline_fast_path(&[
1569 Middleware::Buffer(crate::models::BufferMiddleware {
1570 max_messages: 16,
1571 max_delay_ms: 0,
1572 }),
1573 Middleware::Delay(crate::models::DelayMiddleware { delay_ms: 0 }),
1574 Middleware::Limiter(crate::models::LimiterMiddleware {
1575 messages_per_second: 1_000_000.0,
1576 }),
1577 ]));
1578
1579 assert!(!output_middlewares_allow_http_inline_fast_path(&[
1580 Middleware::Retry(crate::models::RetryMiddleware::default()),
1581 ]));
1582 assert!(!output_middlewares_allow_http_inline_fast_path(&[
1583 Middleware::Dlq(Box::default()),
1584 ]));
1585 }
1586
1587 #[test]
1588 fn test_consumer_middleware_ordering() {
1589 let endpoint = Endpoint::new_memory("test", 10)
1590 .with_deduplication(crate::models::DeduplicationMiddleware {
1591 sled_path: "".into(),
1592 ttl_seconds: 10,
1593 })
1594 .with_consumer_metrics();
1595
1596 assert_eq!(endpoint.middlewares.len(), 2);
1600 assert!(matches!(endpoint.middlewares[0], Middleware::Metrics(_)));
1601 assert!(matches!(
1602 endpoint.middlewares[1],
1603 Middleware::Deduplication(_)
1604 ));
1605 }
1606
1607 #[test]
1608 fn test_check_consumer_invalid_config() {
1609 let config = crate::models::MemoryConfig {
1610 topic: "test".to_string(),
1611 request_reply: true, ..Default::default()
1613 };
1614 let endpoint = Endpoint::new(EndpointType::Memory(config));
1615
1616 let warnings = check_consumer("test_route", &endpoint, None).unwrap();
1617 assert!(warnings
1618 .iter()
1619 .any(|w| w.contains("request_reply") && w.contains("publisher-only")));
1620 }
1621}