1#[cfg(feature = "amqp")]
7pub mod amqp;
8#[cfg(feature = "aws")]
9pub mod aws;
10#[cfg(feature = "clickhouse")]
11pub mod clickhouse;
12pub mod file;
13#[cfg(feature = "grpc")]
14pub mod grpc;
15#[cfg(feature = "http")]
16pub mod http;
17#[cfg(any(feature = "ibm-mq-static", feature = "ibm-mq"))]
18pub mod ibm_mq;
19#[cfg(feature = "kafka")]
20pub mod kafka;
21pub mod memory;
22#[cfg(feature = "mongodb")]
23pub mod mongodb;
24#[cfg(feature = "mqtt")]
25pub mod mqtt;
26#[cfg(feature = "nats")]
27pub mod nats;
28#[cfg(feature = "object-store")]
29pub mod object_store;
30#[cfg(any(feature = "sqlx", feature = "clickhouse"))]
31mod poll;
32#[cfg(feature = "postgres-cdc")]
33pub mod postgres;
34#[cfg(feature = "redis-streams")]
35pub mod redis_streams;
36#[cfg(feature = "sled")]
37pub mod sled;
38#[cfg(feature = "sqlx")]
39pub mod sqlx;
40pub mod structural;
44#[cfg(feature = "websocket")]
45pub mod websocket;
46#[cfg(any(feature = "zeromq", feature = "zeromq-omq"))]
47pub mod zeromq;
48use crate::endpoints::memory::{get_or_create_channel, MemoryChannel};
49#[doc(hidden)]
52pub use crate::endpoints::structural::{
53 fanout, null, reader, request, response, static_endpoint, stream_buffer, switch,
54};
55use crate::middleware::apply_middlewares_to_consumer;
56use crate::models::{
57 Endpoint, EndpointType, MemoryConfig, Middleware, ResponseConfig, StreamBufferConfig,
58};
59use crate::route::get_endpoint_factory;
60use crate::traits::{BoxFuture, MessageConsumer, MessagePublisher};
61use anyhow::{anyhow, Result};
62use std::sync::Arc;
63
64impl Endpoint {
65 pub fn new(endpoint_type: EndpointType) -> Self {
66 Self {
67 middlewares: Vec::new(),
68 endpoint_type,
69 handler: None,
70 }
71 }
72 pub fn new_memory(topic: &str, capacity: usize) -> Self {
81 Self::new(EndpointType::Memory(MemoryConfig::new(
82 topic,
83 Some(capacity),
84 )))
85 }
86 pub fn new_response() -> Self {
87 Self::new(EndpointType::Response(ResponseConfig::default()))
88 }
89 pub fn new_stream_buffer(topic: &str, correlation_id: Option<&str>, capacity: usize) -> Self {
90 Self::new(EndpointType::StreamBuffer(StreamBufferConfig {
91 topic: topic.to_string(),
92 correlation_id: correlation_id.map(str::to_string),
93 capacity: Some(capacity),
94 }))
95 }
96 pub fn has_retry_middleware(&self) -> bool {
97 self.middlewares
98 .iter()
99 .any(|m| matches!(m, Middleware::Retry(_)))
100 }
101 pub fn add_middleware(mut self, middleware: Middleware) -> Self {
102 self.middlewares.push(middleware);
103 self
104 }
105 pub fn add_middlewares(mut self, mut middlewares: Vec<Middleware>) -> Self {
106 self.middlewares.append(&mut middlewares);
107 self
108 }
109 pub fn channel(&self) -> anyhow::Result<MemoryChannel> {
115 match &self.endpoint_type {
116 EndpointType::Memory(cfg) => Ok(get_or_create_channel(cfg)),
117 _ => Err(anyhow::anyhow!("channel() called on non-memory Endpoint")),
118 }
119 }
120 pub fn null() -> Self {
121 Self::new(EndpointType::Null)
122 }
123
124 pub fn with_retry(mut self, retry: crate::models::RetryMiddleware) -> Self {
125 let mut insert_idx = self.middlewares.len();
128 for (i, m) in self.middlewares.iter().enumerate() {
129 if matches!(m, Middleware::Dlq(_) | Middleware::Metrics(_)) {
130 insert_idx = i;
131 break;
132 }
133 }
134 self.middlewares
135 .insert(insert_idx, Middleware::Retry(retry));
136 self
137 }
138
139 pub fn with_dlq(mut self, dlq: crate::models::DeadLetterQueueMiddleware) -> Self {
140 let mut insert_idx = self.middlewares.len();
142 for (i, m) in self.middlewares.iter().enumerate() {
143 if matches!(m, Middleware::Metrics(_)) {
144 insert_idx = i;
145 break;
146 }
147 }
148 self.middlewares
149 .insert(insert_idx, Middleware::Dlq(Box::new(dlq)));
150 self
151 }
152
153 pub fn with_deduplication(mut self, dedup: crate::models::DeduplicationMiddleware) -> Self {
154 self.middlewares.insert(0, Middleware::Deduplication(dedup));
159 self
160 }
161
162 pub fn with_consumer_metrics(mut self) -> Self {
163 if !self
167 .middlewares
168 .iter()
169 .any(|m| matches!(m, Middleware::Metrics(_)))
170 {
171 self.middlewares
172 .insert(0, Middleware::Metrics(crate::models::MetricsMiddleware {}));
173 }
174 self
175 }
176
177 pub fn with_metrics(mut self) -> Self {
178 if !self
180 .middlewares
181 .iter()
182 .any(|m| matches!(m, Middleware::Metrics(_)))
183 {
184 self.middlewares
185 .push(Middleware::Metrics(crate::models::MetricsMiddleware {}));
186 }
187 self
188 }
189
190 pub async fn create_consumer(
191 &self,
192 route_name: &str,
193 ) -> anyhow::Result<Box<dyn crate::traits::MessageConsumer>> {
194 crate::endpoints::create_consumer_from_route(route_name, self).await
195 }
196
197 pub async fn create_publisher(&self, _route_name: &str) -> anyhow::Result<crate::Publisher> {
198 crate::Publisher::new(self.clone()).await
199 }
200
201 pub fn check_consumer(
202 &self,
203 route_name: &str,
204 allowed_endpoints: Option<&[&str]>,
205 ) -> anyhow::Result<Vec<String>> {
206 crate::endpoints::check_consumer(route_name, self, allowed_endpoints)
207 }
208
209 pub fn check_publisher(
210 &self,
211 route_name: &str,
212 allowed_endpoints: Option<&[&str]>,
213 ) -> anyhow::Result<Vec<String>> {
214 crate::endpoints::check_publisher(route_name, self, allowed_endpoints)
215 }
216}
217
218pub fn check_consumer(
220 route_name: &str,
221 endpoint: &Endpoint,
222 allowed_types: Option<&[&str]>,
223) -> Result<Vec<String>> {
224 check_consumer_recursive(route_name, endpoint, 0, allowed_types)
225}
226
227fn check_consumer_recursive(
228 route_name: &str,
229 endpoint: &Endpoint,
230 depth: usize,
231 allowed_types: Option<&[&str]>,
232) -> Result<Vec<String>> {
233 const MAX_DEPTH: usize = 16;
234 if depth > MAX_DEPTH {
235 return Err(anyhow!(
236 "Ref recursion depth exceeded limit of {}",
237 MAX_DEPTH
238 ));
239 }
240 let mut warnings = Vec::new();
241 if endpoint.handler.is_some() {
242 warnings.push(
243 "Endpoint 'handler' is set on an input endpoint. Handlers are currently only supported on output endpoints (publishers) and will be ignored here."
244 .to_string()
245 );
246 }
247
248 if let Some(allowed) = allowed_types {
249 if !endpoint.endpoint_type.is_core() {
250 let name = endpoint.endpoint_type.name();
251 if !allowed.contains(&name) {
252 return Err(anyhow!(
253 "[route:{}] Endpoint type '{}' is not allowed by policy",
254 route_name,
255 name
256 ));
257 }
258 }
259 }
260 match &endpoint.endpoint_type {
261 EndpointType::Ref(name) => {
262 let referenced = crate::route::get_endpoint(name).ok_or_else(|| {
263 anyhow!(
264 "[route:{}] Referenced endpoint '{}' not found",
265 route_name,
266 name
267 )
268 })?;
269 warnings.extend(check_consumer_recursive(
274 route_name,
275 &referenced,
276 depth + 1,
277 allowed_types,
278 )?);
279 Ok(warnings)
280 }
281 #[cfg(feature = "aws")]
282 EndpointType::Aws(cfg) => {
283 if cfg.topic_arn.is_some() {
284 warnings.push(
285 "Endpoint 'aws' is used as a consumer, but 'topic_arn' is a publisher-only option and will be ignored."
286 .to_string()
287 );
288 }
289 Ok(warnings)
290 }
291 #[cfg(feature = "kafka")]
292 EndpointType::Kafka(cfg) => {
293 if cfg.delayed_ack {
294 warnings.push(
295 "Endpoint 'kafka' is used as a consumer, but 'delayed_ack' is a publisher-only option and will be ignored."
296 .to_string()
297 );
298 }
299 if cfg.producer_options.is_some() {
300 warnings.push(
301 "Endpoint 'kafka' is used as a consumer, but 'producer_options' is a publisher-only option and will be ignored."
302 .to_string()
303 );
304 }
305 Ok(warnings)
306 }
307 #[cfg(feature = "nats")]
308 EndpointType::Nats(cfg) => {
309 if cfg.stream.is_none() {
310 return Err(anyhow!(
311 "[route:{}] NATS consumer must specify a 'stream'",
312 route_name
313 ));
314 }
315 if cfg.request_reply {
316 warnings.push(
317 "Endpoint 'nats' is used as a consumer, but 'request_reply' is a publisher-only option and will be ignored."
318 .to_string()
319 );
320 }
321 if cfg.request_timeout_ms.is_some() {
322 warnings.push(
323 "Endpoint 'nats' is used as a consumer, but 'request_timeout_ms' is a publisher-only option and will be ignored."
324 .to_string()
325 );
326 }
327 if cfg.delayed_ack {
328 warnings.push(
329 "Endpoint 'nats' is used as a consumer, but 'delayed_ack' is a publisher-only option and will be ignored."
330 .to_string()
331 );
332 }
333 if cfg.stream_max_messages.is_some() {
334 warnings.push(
335 "Endpoint 'nats' is used as a consumer, but 'stream_max_messages' is a publisher-only option and will be ignored."
336 .to_string()
337 );
338 }
339 if cfg.stream_max_bytes.is_some() {
340 warnings.push(
341 "Endpoint 'nats' is used as a consumer, but 'stream_max_bytes' is a publisher-only option and will be ignored."
342 .to_string()
343 );
344 }
345 Ok(warnings)
346 }
347 #[cfg(feature = "amqp")]
348 EndpointType::Amqp(cfg) => {
349 if cfg.delayed_ack {
350 warnings.push(
351 "Endpoint 'amqp' is used as a consumer, but 'delayed_ack' is a publisher-only option and will be ignored."
352 .to_string()
353 );
354 }
355 Ok(warnings)
356 }
357 #[cfg(feature = "mqtt")]
358 EndpointType::Mqtt(cfg) => {
359 if cfg.delayed_ack {
360 warnings.push(
361 "Endpoint 'mqtt' is used as a consumer, but 'delayed_ack' is a publisher-only option and will be ignored."
362 .to_string()
363 );
364 }
365 Ok(warnings)
366 }
367 #[cfg(feature = "zeromq")]
368 EndpointType::ZeroMq(_) => Ok(warnings),
369 #[cfg(feature = "redis-streams")]
370 EndpointType::RedisStreams(cfg) => {
371 if cfg.maxlen.is_some() {
372 warnings.push(
373 "Endpoint 'redis_streams' is used as a consumer, but 'maxlen' is a publisher-only option and will be ignored."
374 .to_string()
375 );
376 }
377 if cfg.approx_trim.is_some() {
378 warnings.push(
379 "Endpoint 'redis_streams' is used as a consumer, but 'approx_trim' is a publisher-only option and will be ignored."
380 .to_string()
381 );
382 }
383 Ok(warnings)
384 }
385 #[cfg(any(feature = "ibm-mq-static", feature = "ibm-mq"))]
386 EndpointType::IbmMq(_) => Ok(warnings),
387 #[cfg(feature = "mongodb")]
388 EndpointType::MongoDb(cfg) => {
389 use crate::models::MongoConsume;
390 let mode = cfg.resolved_consume();
391 if mode == MongoConsume::Subscriber
392 && matches!(cfg.format, crate::models::MongoDbFormat::Raw)
393 {
394 return Err(anyhow!(
395 "[route:{}] MongoDB raw format cannot be used with subscriber mode because raw documents do not include the seq ordering field",
396 route_name
397 ));
398 }
399 if cfg.consume.is_some() && cfg.change_stream {
400 warnings.push(
401 "Endpoint 'mongodb' sets 'consume'; the deprecated 'change_stream' boolean is ignored and should be removed."
402 .to_string(),
403 );
404 } else if cfg.change_stream {
405 warnings.push(
406 "Endpoint 'mongodb' option 'change_stream' is deprecated; use 'consume: subscriber'."
407 .to_string(),
408 );
409 }
410 if cfg.reply_polling_ms.is_some() {
411 warnings.push(
412 "Endpoint 'mongodb' is used as a consumer, but 'reply_polling_ms' is a publisher-only option and will be ignored."
413 .to_string()
414 );
415 }
416 if cfg.request_reply {
417 warnings.push(
418 "Endpoint 'mongodb' is used as a consumer, but 'request_reply' is a publisher-only option and will be ignored."
419 .to_string()
420 );
421 }
422 if cfg.request_timeout_ms.is_some() {
423 warnings.push(
424 "Endpoint 'mongodb' is used as a consumer, but 'request_timeout_ms' is a publisher-only option and will be ignored."
425 .to_string()
426 );
427 }
428 if cfg.ttl_seconds.is_some() {
429 warnings.push(
430 "Endpoint 'mongodb' is used as a consumer, but 'ttl_seconds' is a publisher-only option and will be ignored."
431 .to_string()
432 );
433 }
434 if cfg.capped_size_bytes.is_some() {
435 warnings.push(
436 "Endpoint 'mongodb' is used as a consumer, but 'capped_size_bytes' is a publisher-only option and will be ignored."
437 .to_string()
438 );
439 }
440 Ok(warnings)
441 }
442 #[cfg(feature = "grpc")]
443 EndpointType::Grpc(_) => Ok(warnings),
444 #[cfg(feature = "http")]
445 EndpointType::Http(cfg) => {
446 if cfg.batch_concurrency.is_some() {
447 warnings.push("Endpoint 'http' is used as a consumer, but 'batch_concurrency' is a publisher-only option and will be ignored.".to_string());
448 }
449 if cfg.tcp_keepalive_ms.is_some() {
450 warnings.push("Endpoint 'http' is used as a consumer, but 'tcp_keepalive_ms' is a publisher-only option and will be ignored.".to_string());
451 }
452 if cfg.pool_idle_timeout_ms.is_some() {
453 warnings.push(
454 "Endpoint 'http' is used as a consumer, but 'pool_idle_timeout_ms' is a publisher-only option and will be ignored."
455 .to_string(),
456 );
457 }
458 if cfg.stream_response_to.is_some() {
459 warnings.push("Endpoint 'http' is used as a consumer, but 'stream_response_to' is a publisher-only option and will be ignored.".to_string());
460 }
461 Ok(warnings)
462 }
463 #[cfg(feature = "clickhouse")]
464 EndpointType::ClickHouse(cfg) => {
465 if cfg.cursor_column.is_none() {
466 return Err(anyhow!(
467 "ClickHouse endpoint used as a consumer requires 'cursor_column' (ClickHouse has no native queue; only non-destructive cursor reads are supported)."
468 ));
469 }
470 if cfg.columns.is_some() {
471 warnings.push("Endpoint 'clickhouse' is used as a consumer, but 'columns' is a publisher-only option and will be ignored.".to_string());
472 }
473 if cfg.async_insert {
474 warnings.push("Endpoint 'clickhouse' is used as a consumer, but 'async_insert' is a publisher-only option and will be ignored.".to_string());
475 }
476 Ok(warnings)
477 }
478 #[cfg(feature = "sqlx")]
479 EndpointType::Sqlx(cfg) => {
480 if cfg.insert_query.is_some() {
481 warnings.push(
482 "Endpoint 'sqlx' is used as a consumer, but 'insert_query' is a publisher-only option and will be ignored."
483 .to_string()
484 );
485 }
486 Ok(warnings)
487 }
488 #[cfg(feature = "postgres-cdc")]
489 EndpointType::PostgresCdc(cfg) => {
490 if cfg.publication.trim().is_empty() {
491 return Err(anyhow!(
492 "postgres_cdc consumer requires a 'publication' (defines which tables are captured)."
493 ));
494 }
495 Ok(warnings)
496 }
497 #[cfg(feature = "sled")]
498 EndpointType::Sled(_) => Ok(warnings),
499 EndpointType::Static(_) => Ok(warnings),
500 EndpointType::Memory(cfg) => {
501 if cfg.request_reply {
502 warnings.push(
503 "Endpoint 'memory' is used as a consumer, but 'request_reply' is a publisher-only option and will be ignored."
504 .to_string()
505 );
506 }
507 if cfg.request_timeout_ms.is_some() {
508 warnings.push(
509 "Endpoint 'memory' is used as a consumer, but 'request_timeout_ms' is a publisher-only option and will be ignored."
510 .to_string()
511 );
512 }
513 Ok(warnings)
514 }
515 EndpointType::StreamBuffer(cfg) => {
516 if cfg.correlation_id.is_none() {
517 return Err(anyhow!(
518 "[route:{}] stream_buffer consumer must specify 'correlation_id'",
519 route_name
520 ));
521 }
522 Ok(warnings)
523 }
524 EndpointType::File(_) => Ok(warnings),
525 #[cfg(feature = "object-store")]
526 EndpointType::ObjectStore(cfg) => {
527 if cfg.extension.is_some() {
528 warnings.push("Endpoint 'object_store' is used as a consumer, but 'extension' is a publisher-only option and will be ignored.".to_string());
529 }
530 if cfg.date_partition {
531 warnings.push("Endpoint 'object_store' is used as a consumer, but 'date_partition' is a publisher-only option and will be ignored.".to_string());
532 }
533 Ok(warnings)
534 }
535 #[cfg(feature = "websocket")]
536 EndpointType::WebSocket(_) => Ok(warnings),
537 EndpointType::Custom { .. } => Ok(warnings),
538 EndpointType::Switch(_) => Err(anyhow!(
539 "[route:{}] Switch endpoint is only supported as an output",
540 route_name
541 )),
542 EndpointType::Reader(_) => Err(anyhow!(
543 "[route:{}] Reader endpoint is only supported as an output",
544 route_name
545 )),
546 #[allow(unreachable_patterns)]
547 _ => {
548 if let Some(allowed) = allowed_types {
549 let name = endpoint.endpoint_type.name();
550 if allowed.contains(&name) {
551 return Ok(warnings);
552 }
553 }
554 Err(anyhow!(
555 "[route:{}] Unsupported consumer endpoint type '{:?}'",
556 route_name,
557 endpoint.endpoint_type
558 ))
559 }
560 }
561}
562
563fn resolve_endpoint(endpoint: &Endpoint, route_name: &str) -> Result<Endpoint> {
564 let mut visited = std::collections::HashSet::new();
565 resolve_endpoint_recursive(endpoint, route_name, &mut visited)
566}
567
568fn resolve_endpoint_recursive(
569 endpoint: &Endpoint,
570 route_name: &str,
571 visited: &mut std::collections::HashSet<String>,
572) -> Result<Endpoint> {
573 const MAX_DEPTH: usize = 16;
574 if visited.len() > MAX_DEPTH {
575 return Err(anyhow!(
576 "Reference recursion depth exceeded limit of {}",
577 MAX_DEPTH
578 ));
579 }
580
581 if let EndpointType::Ref(name) = &endpoint.endpoint_type {
582 if !visited.insert(name.clone()) {
583 return Err(anyhow!(
584 "[route:{}] Circular reference detected for endpoint '{}'",
585 route_name,
586 name
587 ));
588 }
589
590 let referenced_endpoint = crate::route::get_endpoint(name).ok_or_else(|| {
591 anyhow!(
592 "[route:{}] Referenced endpoint '{}' not found",
593 route_name,
594 name
595 )
596 })?;
597
598 let mut resolved = resolve_endpoint_recursive(&referenced_endpoint, route_name, visited)?;
599 let mut new_middlewares = endpoint.middlewares.clone();
602 new_middlewares.extend(resolved.middlewares);
603 resolved.middlewares = new_middlewares;
604 Ok(resolved)
605 } else {
606 Ok(endpoint.clone())
607 }
608}
609
610#[cfg(all(feature = "sqlx", feature = "postgres-cdc"))]
614fn sqlx_cfg_to_cdc(
615 cfg: &crate::models::SqlxConfig,
616) -> anyhow::Result<crate::models::PostgresCdcConfig> {
617 let url = cfg.url.trim();
618 if !(url.starts_with("postgres://") || url.starts_with("postgresql://")) {
619 return Err(anyhow!(
620 "sqlx `publication` (CDC) is only supported for PostgreSQL URLs; got '{}'. \
621 Use a postgres:// URL or the dedicated `postgres_cdc` endpoint.",
622 cfg.url
623 ));
624 }
625 if cfg.cursor_column.is_some() {
626 return Err(anyhow!(
627 "sqlx endpoint sets both `publication` (CDC) and `cursor_column` (polling); pick one."
628 ));
629 }
630 Ok(crate::models::PostgresCdcConfig {
631 url: cfg.url.clone(),
632 publication: cfg.publication.clone().unwrap_or_default(),
633 slot_name: cfg
634 .slot_name
635 .clone()
636 .unwrap_or_else(|| "mq_bridge_slot".to_string()),
637 create_slot: true,
638 create_publication: cfg.create_publication,
639 publication_tables: if cfg.create_publication {
640 vec![cfg.table.clone()]
641 } else {
642 Vec::new()
643 },
644 temporary_slot: false,
645 cursor_id: cfg.cursor_id.clone(),
646 checkpoint_store: cfg.checkpoint_store.clone(),
647 status_interval_ms: 10_000,
648 tls: cfg.tls.clone(),
649 })
650}
651
652pub async fn create_consumer_from_route(
654 route_name: &str,
655 endpoint: &Endpoint,
656) -> Result<Box<dyn MessageConsumer>> {
657 let resolved_endpoint = resolve_endpoint(endpoint, route_name)?;
658 check_consumer(route_name, &resolved_endpoint, None)?;
659 let consumer = create_base_consumer(route_name, &resolved_endpoint).await?;
660 apply_middlewares_to_consumer(consumer, &resolved_endpoint, route_name).await
661}
662
663pub(crate) async fn try_run_fast_path_route(
664 route: &crate::models::Route,
665 name: &str,
666 shutdown_rx: async_channel::Receiver<()>,
667 ready_tx: Option<async_channel::Sender<()>>,
668) -> Option<anyhow::Result<bool>> {
669 #[cfg(feature = "http")]
670 {
671 let output_is_inline = matches!(
675 route.output.endpoint_type,
676 EndpointType::Response(_) | EndpointType::Static(_)
677 );
678 if let EndpointType::Http(cfg) = &route.input.endpoint_type {
679 if output_is_inline
680 && cfg.inline_response_fast_path_enabled()
681 && route.input.middlewares.is_empty()
682 && output_middlewares_allow_http_inline_fast_path(&route.output.middlewares)
683 && !cfg.fire_and_forget
684 {
685 return Some(
686 run_http_inline_response_fast_path(
687 route,
688 name,
689 shutdown_rx,
690 ready_tx,
691 cfg.clone(),
692 )
693 .await,
694 );
695 }
696 }
697 }
698
699 #[cfg(feature = "websocket")]
700 {
701 if let EndpointType::WebSocket(cfg) = &route.input.endpoint_type {
702 match websocket_direct_route_support(route) {
703 WebSocketDirectRouteSupport::Supported => {
704 return Some(
705 websocket::run_direct_response_route(
706 name,
707 cfg.clone(),
708 route.output.handler.clone(),
709 shutdown_rx,
710 ready_tx,
711 )
712 .await,
713 );
714 }
715 WebSocketDirectRouteSupport::Unsupported(reason) => match cfg.execution_mode {
716 crate::models::WebSocketExecutionMode::Auto => {
717 tracing::warn!(
718 route = name,
719 reason = reason,
720 "WebSocket route cannot run in direct mode; falling back to routed mode"
721 );
722 }
723 crate::models::WebSocketExecutionMode::DirectOnly => {
724 return Some(Err(anyhow!(
725 "WebSocket route '{}' is configured for direct_only, but direct mode is unsupported: {}",
726 name,
727 reason
728 )));
729 }
730 crate::models::WebSocketExecutionMode::Routed => {}
731 },
732 }
733 }
734 }
735
736 let _ = route;
737 let _ = name;
738 let _ = shutdown_rx;
739 let _ = ready_tx;
740 None
741}
742
743#[cfg(feature = "websocket")]
744enum WebSocketDirectRouteSupport {
745 Supported,
746 Unsupported(&'static str),
747}
748
749#[cfg(feature = "websocket")]
750fn websocket_direct_route_support(route: &crate::models::Route) -> WebSocketDirectRouteSupport {
751 let EndpointType::WebSocket(cfg) = &route.input.endpoint_type else {
752 return WebSocketDirectRouteSupport::Unsupported("input is not websocket");
753 };
754
755 if cfg.execution_mode == crate::models::WebSocketExecutionMode::Routed {
756 return WebSocketDirectRouteSupport::Unsupported("execution_mode is routed");
757 }
758 if !matches!(route.output.endpoint_type, EndpointType::Response(_)) {
759 return WebSocketDirectRouteSupport::Unsupported("output is not response");
760 }
761 if !websocket_direct_route_options_allowed(&route.options) {
762 return WebSocketDirectRouteSupport::Unsupported(
763 "custom route options require routed mode",
764 );
765 }
766 if !route.input.middlewares.is_empty() || !route.output.middlewares.is_empty() {
767 return WebSocketDirectRouteSupport::Unsupported("middleware requires routed mode");
768 }
769
770 WebSocketDirectRouteSupport::Supported
771}
772
773#[cfg(feature = "websocket")]
774fn websocket_direct_route_options_allowed(options: &crate::models::RouteOptions) -> bool {
775 let mut defaults = crate::models::RouteOptions::default();
776 defaults.description.clone_from(&options.description);
777 options == &defaults
778}
779
780#[cfg(feature = "http")]
781fn output_middlewares_allow_http_inline_fast_path(middlewares: &[Middleware]) -> bool {
782 middlewares.iter().all(|middleware| {
783 matches!(
784 middleware,
785 Middleware::Buffer(_)
786 | Middleware::Delay(_)
787 | Middleware::Limiter(_)
788 | Middleware::Metrics(_)
789 )
790 })
791}
792
793#[cfg(feature = "http")]
794async fn run_http_inline_response_fast_path(
795 route: &crate::models::Route,
796 name: &str,
797 shutdown_rx: async_channel::Receiver<()>,
798 ready_tx: Option<async_channel::Sender<()>>,
799 http_config: crate::models::HttpConfig,
800) -> anyhow::Result<bool> {
801 let publisher = create_publisher_from_route(name, &route.output).await?;
802 let consumer =
803 http::HttpConsumer::new_with_inline_publisher(&http_config, Some(publisher.clone()))
804 .await?;
805
806 if let Err(err) = crate::route::run_publisher_connect_hook(name, &publisher).await {
807 crate::route::run_publisher_disconnect_hook(name, &publisher).await;
808 return Err(err);
809 }
810 if let Err(err) = crate::route::run_consumer_connect_hook(name, &consumer).await {
811 crate::route::run_consumer_disconnect_hook(name, &consumer).await;
812 crate::route::run_publisher_disconnect_hook(name, &publisher).await;
813 return Err(err);
814 }
815
816 tracing::info!(
817 route = name,
818 has_output_handler = route.output.handler.is_some(),
819 output_middlewares = route.output.middlewares.len(),
820 "Running HTTP inline response fast path; bypassing the normal route consumer/worker/disposition pipeline while keeping the output publisher chain active"
821 );
822 tracing::debug!(
823 route = name,
824 "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"
825 );
826 if let Some(tx) = ready_tx {
827 let _ = tx.send(()).await;
828 }
829
830 let stopped = shutdown_rx.recv().await.is_ok();
831 if stopped {
832 tracing::info!(
833 "Shutdown signal received in HTTP inline response runner for route '{}'.",
834 name
835 );
836 }
837 crate::route::run_consumer_disconnect_hook(name, &consumer).await;
838 crate::route::run_publisher_disconnect_hook(name, &publisher).await;
839 Ok(true)
840}
841
842async fn create_base_consumer(
843 route_name: &str,
844 endpoint: &Endpoint,
845) -> Result<Box<dyn MessageConsumer>> {
846 fn boxed<T: MessageConsumer + 'static>(c: T) -> Box<dyn MessageConsumer> {
848 Box::new(c)
849 }
850
851 match &endpoint.endpoint_type {
852 #[cfg(feature = "aws")]
853 EndpointType::Aws(cfg) => Ok(boxed(aws::AwsConsumer::new(cfg).await?)),
854 #[cfg(feature = "kafka")]
855 EndpointType::Kafka(cfg) => {
856 let mut config = cfg.clone();
857 if config.topic.is_none() {
858 config.topic = Some(route_name.to_string());
859 }
860 Ok(boxed(kafka::KafkaConsumer::new(&config).await?))
861 }
862 #[cfg(feature = "nats")]
863 EndpointType::Nats(cfg) => {
864 let mut config = cfg.clone();
865 if config.subject.is_none() {
866 config.subject = Some(route_name.to_string());
867 }
868 Ok(boxed(nats::NatsConsumer::new(&config).await?))
869 }
870 #[cfg(feature = "amqp")]
871 EndpointType::Amqp(cfg) => {
872 let mut config = cfg.clone();
873 if config.queue.is_none() {
874 config.queue = Some(route_name.to_string());
875 }
876 Ok(boxed(amqp::AmqpConsumer::new(&config).await?))
877 }
878 #[cfg(feature = "mqtt")]
879 EndpointType::Mqtt(cfg) => {
880 let mut config = cfg.clone();
881 if config.topic.is_none() {
882 config.topic = Some(route_name.to_string());
883 }
884 if config.client_id.is_none() && !config.clean_session {
885 config.client_id = Some(format!("{}-{}", crate::APP_NAME, route_name));
887 }
888 Ok(boxed(mqtt::MqttConsumer::new(&config).await?))
889 }
890 #[cfg(any(feature = "ibm-mq-static", feature = "ibm-mq"))]
891 EndpointType::IbmMq(cfg) => {
892 let mut config = cfg.clone();
893 if config.queue.is_none() && config.topic.is_none() {
894 config.queue = Some(route_name.to_string());
895 }
896 Ok(boxed(ibm_mq::IbmMqConsumer::new(&config).await?))
897 }
898 #[cfg(any(feature = "zeromq", feature = "zeromq-omq"))]
899 EndpointType::ZeroMq(cfg) => zeromq::create_consumer(cfg).await,
900 #[cfg(feature = "redis-streams")]
901 EndpointType::RedisStreams(cfg) => {
902 let mut config = cfg.clone();
903 if config.stream.is_none() {
904 config.stream = Some(route_name.to_string());
905 }
906 Ok(boxed(
907 redis_streams::RedisStreamsConsumer::new(&config).await?,
908 ))
909 }
910 EndpointType::File(cfg) => Ok(boxed(file::FileConsumer::new(cfg).await?)),
911 #[cfg(feature = "object-store")]
912 EndpointType::ObjectStore(cfg) => {
913 Ok(boxed(object_store::ObjectStoreConsumer::new(cfg).await?))
914 }
915 #[cfg(feature = "grpc")]
916 EndpointType::Grpc(cfg) => {
917 let mut config = cfg.clone();
918 if config.topic.is_none() {
919 config.topic = Some(route_name.to_string());
920 }
921 Ok(boxed(grpc::GrpcConsumer::new(&config).await?))
922 }
923 #[cfg(feature = "sqlx")]
924 EndpointType::Sqlx(cfg) => {
925 if cfg.publication.is_some() {
926 #[cfg(feature = "postgres-cdc")]
929 {
930 Ok(boxed(
931 postgres::PostgresCdcConsumer::new(&sqlx_cfg_to_cdc(cfg)?).await?,
932 ))
933 }
934 #[cfg(not(feature = "postgres-cdc"))]
935 {
936 Err(anyhow!(
937 "sqlx endpoint with `publication` set uses Postgres CDC, which requires \
938 the `postgres-cdc` feature to be enabled."
939 ))
940 }
941 } else if cfg.cursor_column.is_some() {
942 Ok(boxed(sqlx::SqlxCursorReader::new(cfg).await?))
944 } else {
945 Ok(boxed(sqlx::SqlxConsumer::new(cfg).await?))
946 }
947 }
948 #[cfg(feature = "clickhouse")]
949 EndpointType::ClickHouse(cfg) => {
950 if cfg.cursor_column.is_some() {
951 Ok(boxed(clickhouse::ClickHouseCursorReader::new(cfg).await?))
953 } else {
954 Err(anyhow::anyhow!(
955 "ClickHouse endpoint used as a consumer requires 'cursor_column' (ClickHouse has no native queue; only non-destructive cursor reads are supported)."
956 ))
957 }
958 }
959 #[cfg(feature = "postgres-cdc")]
960 EndpointType::PostgresCdc(cfg) => Ok(boxed(postgres::PostgresCdcConsumer::new(cfg).await?)),
961 #[cfg(feature = "http")]
962 EndpointType::Http(cfg) => Ok(boxed(http::HttpConsumer::new(cfg).await?)),
963 #[cfg(feature = "websocket")]
964 EndpointType::WebSocket(cfg) => Ok(boxed(websocket::WebSocketConsumer::new(cfg).await?)),
965 EndpointType::Static(cfg) => Ok(boxed(static_endpoint::StaticRequestConsumer::new(cfg)?)),
966 EndpointType::Memory(cfg) => Ok(boxed(memory::MemoryConsumer::new_async(cfg).await?)),
967 EndpointType::StreamBuffer(cfg) => {
968 Ok(boxed(stream_buffer::StreamBufferConsumer::new(cfg)?))
969 }
970 #[cfg(feature = "sled")]
971 EndpointType::Sled(cfg) => Ok(boxed(sled::SledConsumer::new(cfg)?)),
972 #[cfg(feature = "mongodb")]
973 EndpointType::MongoDb(cfg) => {
974 use crate::models::MongoConsume;
975 let mut config = cfg.clone();
976 if config.collection.is_none() {
977 config.collection = Some(route_name.to_string());
978 }
979 match config.resolved_consume() {
980 MongoConsume::Consumer => {
981 Ok(boxed(mongodb::MongoDbConsumer::new(&config).await?))
983 }
984 MongoConsume::Subscriber => {
985 if config.ttl_seconds.is_none() {
986 config.ttl_seconds = Some(86400); }
988 Ok(boxed(mongodb::MongoDbSubscriber::new(&config).await?))
989 }
990 MongoConsume::CaptureNew => {
991 Ok(boxed(
994 mongodb::MongoDbChangeStreamReader::new(&config, false).await?,
995 ))
996 }
997 MongoConsume::CaptureAll => {
998 match mongodb::MongoDbChangeStreamReader::new(&config, true).await {
1002 Ok(reader) => Ok(boxed(reader)),
1003 Err(e) if mongodb::is_change_stream_unsupported(&e) => {
1006 tracing::warn!(error = %e, "MongoDB change streams unavailable (needs a replica set); 'capture_all' falling back to an insert-only read");
1007 Ok(boxed(mongodb::MongoDbIdReader::new(&config).await?))
1008 }
1009 Err(e) => Err(e),
1010 }
1011 }
1012 }
1013 }
1014 EndpointType::Custom { name, config } => {
1015 let factory = get_endpoint_factory(name)
1016 .ok_or_else(|| anyhow!("Custom endpoint factory '{}' not found", name))?;
1017 factory.create_consumer(route_name, config).await
1018 }
1019 EndpointType::Switch(_) => Err(anyhow!(
1020 "[route:{}] Switch endpoint is only supported as an output",
1021 route_name
1022 )),
1023 #[allow(unreachable_patterns)]
1024 _ => Err(anyhow!(
1025 "[route:{}] Unsupported consumer endpoint type '{:?}'",
1026 route_name,
1027 endpoint.endpoint_type
1028 )),
1029 }
1030}
1031
1032pub fn check_publisher(
1034 route_name: &str,
1035 endpoint: &Endpoint,
1036 allowed_types: Option<&[&str]>,
1037) -> Result<Vec<String>> {
1038 check_publisher_recursive(route_name, endpoint, 0, allowed_types)
1039}
1040
1041fn check_publisher_recursive(
1042 route_name: &str,
1043 endpoint: &Endpoint,
1044 depth: usize,
1045 allowed_types: Option<&[&str]>,
1046) -> Result<Vec<String>> {
1047 let mut warnings = Vec::new();
1048 if let Some(allowed) = allowed_types {
1049 if !endpoint.endpoint_type.is_core() {
1050 let name = endpoint.endpoint_type.name();
1051 if !allowed.contains(&name) {
1052 return Err(anyhow!(
1053 "[route:{}] Endpoint type '{}' is not allowed by policy",
1054 route_name,
1055 name
1056 ));
1057 }
1058 }
1059 }
1060 const MAX_DEPTH: usize = 16;
1061 if depth > MAX_DEPTH {
1062 return Err(anyhow!(
1063 "Fanout recursion depth exceeded limit of {}",
1064 MAX_DEPTH
1065 ));
1066 }
1067 match &endpoint.endpoint_type {
1068 EndpointType::Ref(name) => {
1069 let referenced = crate::route::get_endpoint(name).ok_or_else(|| {
1070 anyhow!(
1071 "[route:{}] Referenced endpoint '{}' not found in endpoint registry",
1072 route_name,
1073 name
1074 )
1075 });
1076 if let Ok(referenced) = referenced {
1077 warnings.extend(check_publisher_recursive(
1078 route_name,
1079 &referenced,
1080 depth + 1,
1081 allowed_types,
1082 )?);
1083 return Ok(warnings);
1084 }
1085 if crate::publisher::get_publisher(name).is_some() {
1086 return Ok(warnings);
1087 }
1088 Err(anyhow!(
1089 "[route:{}] Referenced endpoint '{}' not found in any registry",
1090 route_name,
1091 name
1092 ))
1093 }
1094 #[cfg(feature = "aws")]
1095 EndpointType::Aws(cfg) => {
1096 if cfg.max_messages.is_some() {
1097 warnings.push(
1098 "Endpoint 'aws' is used as a publisher, but 'max_messages' is a consumer-only option and will be ignored."
1099 .to_string()
1100 );
1101 }
1102 if cfg.wait_time_seconds.is_some() {
1103 warnings.push(
1104 "Endpoint 'aws' is used as a publisher, but 'wait_time_seconds' is a consumer-only option and will be ignored."
1105 .to_string()
1106 );
1107 }
1108 Ok(warnings)
1109 }
1110 #[cfg(feature = "kafka")]
1111 EndpointType::Kafka(cfg) => {
1112 if cfg.group_id.is_some() {
1113 warnings.push(
1114 "Endpoint 'kafka' is used as a publisher, but 'group_id' is a consumer-only option and will be ignored."
1115 .to_string()
1116 );
1117 }
1118 if cfg.consumer_options.is_some() {
1119 warnings.push(
1120 "Endpoint 'kafka' is used as a publisher, but 'consumer_options' is a consumer-only option and will be ignored."
1121 .to_string()
1122 );
1123 }
1124 Ok(warnings)
1125 }
1126 #[cfg(feature = "nats")]
1127 EndpointType::Nats(cfg) => {
1128 if cfg.stream.is_some() {
1129 warnings.push(
1130 "Endpoint 'nats' is used as a publisher, but 'stream' is a consumer-only option and will be ignored."
1131 .to_string()
1132 );
1133 }
1134 if cfg.subscriber_mode {
1135 warnings.push(
1136 "Endpoint 'nats' is used as a publisher, but 'subscriber_mode' is a consumer-only option and will be ignored."
1137 .to_string()
1138 );
1139 }
1140 if cfg.prefetch_count.is_some() {
1141 warnings.push(
1142 "Endpoint 'nats' is used as a publisher, but 'prefetch_count' is a consumer-only option and will be ignored."
1143 .to_string()
1144 );
1145 }
1146 Ok(warnings)
1147 }
1148 #[cfg(feature = "amqp")]
1149 EndpointType::Amqp(cfg) => {
1150 if cfg.subscribe_mode {
1151 warnings.push(
1152 "Endpoint 'amqp' is used as a publisher, but 'subscribe_mode' is a consumer-only option and will be ignored."
1153 .to_string()
1154 );
1155 }
1156 if cfg.prefetch_count.is_some() {
1157 warnings.push(
1158 "Endpoint 'amqp' is used as a publisher, but 'prefetch_count' is a consumer-only option and will be ignored."
1159 .to_string()
1160 );
1161 }
1162 Ok(warnings)
1163 }
1164 #[cfg(feature = "mqtt")]
1165 EndpointType::Mqtt(cfg) => {
1166 if cfg.clean_session {
1167 warnings.push(
1168 "Endpoint 'mqtt' is used as a publisher, but 'clean_session' is a consumer-only option and will be ignored."
1169 .to_string()
1170 );
1171 }
1172 Ok(warnings)
1173 }
1174 #[cfg(feature = "zeromq")]
1175 EndpointType::ZeroMq(cfg) => {
1176 if cfg.topic.is_some() {
1177 warnings.push(
1178 "Endpoint 'zeromq' is used as a publisher, but 'topic' is a consumer-only option and will be ignored."
1179 .to_string()
1180 );
1181 }
1182 Ok(warnings)
1183 }
1184
1185 #[cfg(feature = "http")]
1186 EndpointType::Http(_cfg) => {
1187 if _cfg.path.is_some() {
1188 warnings.push(
1189 "Endpoint 'http' is used as a publisher, but 'path' is a consumer-only option and will be ignored."
1190 .to_string()
1191 );
1192 }
1193 if _cfg.workers.is_some() {
1194 warnings.push(
1195 "Endpoint 'http' is used as a publisher, but 'workers' is a consumer-only option and will be ignored."
1196 .to_string()
1197 );
1198 }
1199 if _cfg.message_id_header.is_some() {
1200 warnings.push(
1201 "Endpoint 'http' is used as a publisher, but 'message_id_header' is a consumer-only option and will be ignored."
1202 .to_string()
1203 );
1204 }
1205 if _cfg.internal_buffer_size.is_some() {
1206 warnings.push(
1207 "Endpoint 'http' is used as a publisher, but 'internal_buffer_size' is a consumer-only option and will be ignored."
1208 .to_string()
1209 );
1210 }
1211 if _cfg.fire_and_forget {
1212 warnings.push(
1213 "Endpoint 'http' is used as a publisher, but 'fire_and_forget' is a consumer-only option and will be ignored."
1214 .to_string()
1215 );
1216 }
1217 if _cfg.receive_streamable {
1218 warnings.push(
1219 "Endpoint 'http' is used as a publisher, but 'receive_streamable' is a consumer-only option and will be ignored."
1220 .to_string()
1221 );
1222 }
1223 Ok(warnings)
1224 }
1225 #[cfg(feature = "redis-streams")]
1226 EndpointType::RedisStreams(cfg) => {
1227 if cfg.group.is_some() {
1228 warnings.push(
1229 "Endpoint 'redis_streams' is used as a publisher, but 'group' is a consumer-only option and will be ignored."
1230 .to_string()
1231 );
1232 }
1233 if cfg.consumer_name.is_some() {
1234 warnings.push(
1235 "Endpoint 'redis_streams' is used as a publisher, but 'consumer_name' is a consumer-only option and will be ignored."
1236 .to_string()
1237 );
1238 }
1239 if cfg.subscriber_mode {
1240 warnings.push(
1241 "Endpoint 'redis_streams' is used as a publisher, but 'subscriber_mode' is a consumer-only option and will be ignored."
1242 .to_string()
1243 );
1244 }
1245 if cfg.block_ms.is_some() {
1246 warnings.push(
1247 "Endpoint 'redis_streams' is used as a publisher, but 'block_ms' is a consumer-only option and will be ignored."
1248 .to_string()
1249 );
1250 }
1251 if cfg.read_from_start {
1252 warnings.push(
1253 "Endpoint 'redis_streams' is used as a publisher, but 'read_from_start' is a consumer-only option and will be ignored."
1254 .to_string()
1255 );
1256 }
1257 if cfg.redelivery_timeout_ms.is_some() {
1258 warnings.push(
1259 "Endpoint 'redis_streams' is used as a publisher, but 'redelivery_timeout_ms' is a consumer-only option and will be ignored."
1260 .to_string()
1261 );
1262 }
1263 if cfg.internal_buffer_size.is_some() {
1264 warnings.push(
1265 "Endpoint 'redis_streams' is used as a publisher, but 'internal_buffer_size' is a consumer-only option and will be ignored."
1266 .to_string()
1267 );
1268 }
1269 Ok(warnings)
1270 }
1271 #[cfg(feature = "grpc")]
1272 EndpointType::Grpc(_) => Ok(warnings),
1273 #[cfg(feature = "sqlx")]
1274 EndpointType::Sqlx(cfg) => {
1275 if cfg.select_query.is_some() {
1276 warnings.push(
1277 "Endpoint 'sqlx' is used as a publisher, but 'select_query' is a consumer-only option and will be ignored."
1278 .to_string()
1279 );
1280 }
1281 if cfg.delete_after_read {
1282 warnings.push(
1283 "Endpoint 'sqlx' is used as a publisher, but 'delete_after_read' is a consumer-only option and will be ignored."
1284 .to_string()
1285 );
1286 }
1287 if cfg.polling_interval_ms.is_some() {
1288 warnings.push(
1289 "Endpoint 'sqlx' is used as a publisher, but 'polling_interval_ms' is a consumer-only option and will be ignored."
1290 .to_string()
1291 );
1292 }
1293 Ok(warnings)
1294 }
1295 #[cfg(feature = "clickhouse")]
1296 EndpointType::ClickHouse(cfg) => {
1297 if cfg.cursor_column.is_some() {
1298 warnings.push("Endpoint 'clickhouse' is used as a publisher, but 'cursor_column' is a consumer-only option and will be ignored.".to_string());
1299 }
1300 if cfg.checkpoint_store.is_some() {
1301 warnings.push("Endpoint 'clickhouse' is used as a publisher, but 'checkpoint_store' is a consumer-only option and will be ignored.".to_string());
1302 }
1303 if cfg.polling_interval_ms.is_some() {
1304 warnings.push("Endpoint 'clickhouse' is used as a publisher, but 'polling_interval_ms' is a consumer-only option and will be ignored.".to_string());
1305 }
1306 Ok(warnings)
1307 }
1308 #[cfg(any(feature = "ibm-mq-static", feature = "ibm-mq"))]
1309 EndpointType::IbmMq(cfg) => {
1310 if cfg.wait_timeout_ms != 1000 {
1311 warnings.push(
1312 "Endpoint 'ibmmq' is used as a publisher, but 'wait_timeout_ms' is a consumer-only option and will be ignored."
1313 .to_string()
1314 );
1315 }
1316 Ok(warnings)
1317 }
1318 #[cfg(feature = "mongodb")]
1319 EndpointType::MongoDb(cfg) => {
1320 if cfg.polling_interval_ms.is_some() {
1321 warnings.push(
1322 "Endpoint 'mongodb' is used as a publisher, but 'polling_interval_ms' is a consumer-only option and will be ignored."
1323 .to_string()
1324 );
1325 }
1326 if cfg.change_stream {
1327 warnings.push(
1328 "Endpoint 'mongodb' is used as a publisher, but 'change_stream' is a consumer-only option and will be ignored."
1329 .to_string()
1330 );
1331 }
1332 if cfg.cursor_id.is_some() {
1333 warnings.push(
1334 "Endpoint 'mongodb' is used as a publisher, but 'cursor_id' is a consumer-only option and will be ignored."
1335 .to_string()
1336 );
1337 }
1338 Ok(warnings)
1339 }
1340 EndpointType::File(_) => Ok(warnings),
1341 #[cfg(feature = "object-store")]
1342 EndpointType::ObjectStore(cfg) => {
1343 if cfg.checkpoint_store.is_some() {
1344 warnings.push("Endpoint 'object_store' is used as a publisher, but 'checkpoint_store' is a consumer-only option and will be ignored.".to_string());
1345 }
1346 if cfg.cursor_id.is_some() {
1347 warnings.push("Endpoint 'object_store' is used as a publisher, but 'cursor_id' is a consumer-only option and will be ignored.".to_string());
1348 }
1349 if cfg.polling_interval_ms.is_some() {
1350 warnings.push("Endpoint 'object_store' is used as a publisher, but 'polling_interval_ms' is a consumer-only option and will be ignored.".to_string());
1351 }
1352 if cfg.max_object_bytes.is_some() {
1353 warnings.push("Endpoint 'object_store' is used as a publisher, but 'max_object_bytes' is a consumer-only option and will be ignored.".to_string());
1354 }
1355 Ok(warnings)
1356 }
1357 #[cfg(feature = "websocket")]
1358 EndpointType::WebSocket(_) => Ok(warnings),
1359 EndpointType::Static(_) => Ok(warnings),
1360 EndpointType::Memory(cfg) => {
1361 if cfg.subscribe_mode {
1362 warnings.push(
1363 "Endpoint 'memory' is used as a publisher, but 'subscribe_mode' is a consumer-only option and will be ignored."
1364 .to_string()
1365 );
1366 }
1367 if cfg.enable_nack {
1368 warnings.push(
1369 "Endpoint 'memory' is used as a publisher, but 'enable_nack' is a consumer-only option and will be ignored."
1370 .to_string()
1371 );
1372 }
1373 Ok(warnings)
1374 }
1375 EndpointType::StreamBuffer(cfg) => {
1376 if cfg.correlation_id.is_some() {
1377 warnings.push(
1378 "Endpoint 'stream_buffer' is used as a publisher, but 'correlation_id' is a consumer-only option and will be ignored."
1379 .to_string()
1380 );
1381 }
1382 Ok(warnings)
1383 }
1384 #[cfg(feature = "sled")]
1385 EndpointType::Sled(cfg) => {
1386 if cfg.read_from_start {
1387 warnings.push(
1388 "Endpoint 'sled' is used as a publisher, but 'read_from_start' is a consumer-only option and will be ignored."
1389 .to_string()
1390 );
1391 }
1392 if cfg.delete_after_read {
1393 warnings.push(
1394 "Endpoint 'sled' is used as a publisher, but 'delete_after_read' is a consumer-only option and will be ignored."
1395 .to_string()
1396 );
1397 }
1398 Ok(warnings)
1399 }
1400 EndpointType::Null => Ok(warnings),
1401 EndpointType::Fanout(endpoints) => {
1402 for endpoint in endpoints {
1403 warnings.extend(check_publisher_recursive(
1404 route_name,
1405 endpoint,
1406 depth + 1,
1407 allowed_types,
1408 )?);
1409 }
1410 Ok(warnings)
1411 }
1412 EndpointType::Switch(cfg) => {
1413 for endpoint in cfg.cases.values() {
1414 warnings.extend(check_publisher_recursive(
1415 route_name,
1416 endpoint,
1417 depth + 1,
1418 allowed_types,
1419 )?);
1420 }
1421 if let Some(endpoint) = &cfg.default {
1422 warnings.extend(check_publisher_recursive(
1423 route_name,
1424 endpoint,
1425 depth + 1,
1426 allowed_types,
1427 )?);
1428 }
1429 Ok(warnings)
1430 }
1431 EndpointType::Response(_) => Ok(warnings),
1432 EndpointType::Custom { .. } => Ok(warnings),
1433 EndpointType::Reader(inner) => check_consumer(route_name, inner, allowed_types),
1434 EndpointType::Request(cfg) => {
1435 warnings.extend(check_publisher_recursive(
1436 route_name,
1437 &cfg.to,
1438 depth + 1,
1439 allowed_types,
1440 )?);
1441 warnings.extend(check_publisher_recursive(
1442 route_name,
1443 &cfg.forward_to,
1444 depth + 1,
1445 allowed_types,
1446 )?);
1447 Ok(warnings)
1448 }
1449 #[allow(unreachable_patterns)]
1450 _ => {
1451 if let Some(allowed) = allowed_types {
1452 let name = endpoint.endpoint_type.name();
1453 if allowed.contains(&name) {
1454 return Ok(warnings);
1455 }
1456 }
1457 Err(anyhow!(
1458 "[route:{}] Unsupported publisher endpoint type '{:?}'",
1459 route_name,
1460 endpoint.endpoint_type
1461 ))
1462 }
1463 }
1464}
1465
1466pub async fn create_publisher_from_route(
1468 route_name: &str,
1469 endpoint: &Endpoint,
1470) -> Result<Arc<dyn MessagePublisher>> {
1471 check_publisher(route_name, endpoint, None)?;
1472 create_publisher_with_depth(route_name.to_string(), endpoint.clone(), 0).await
1473}
1474
1475fn create_publisher_with_depth(
1476 route_name: String,
1477 endpoint: Endpoint,
1478 depth: usize,
1479) -> BoxFuture<'static, Result<Arc<dyn MessagePublisher>>> {
1480 Box::pin(async move {
1481 const MAX_DEPTH: usize = 16;
1482 if depth > MAX_DEPTH {
1483 return Err(anyhow!(
1484 "Fanout/Ref recursion depth exceeded limit of {}",
1485 MAX_DEPTH
1486 ));
1487 }
1488
1489 if let EndpointType::Ref(name) = &endpoint.endpoint_type {
1490 let referenced_opt = crate::route::get_endpoint(name);
1491
1492 if referenced_opt.is_none() {
1493 if let Some(pub_instance) = crate::publisher::get_publisher(name) {
1494 let inner = pub_instance.inner();
1495 let mut publisher: Box<dyn MessagePublisher> = Box::new(inner);
1496
1497 if let Some(handler) = &endpoint.handler {
1498 publisher = Box::new(crate::command_handler::CommandPublisher::new(
1499 publisher,
1500 handler.clone(),
1501 ));
1502 }
1503 return crate::middleware::apply_middlewares_to_publisher(
1504 publisher,
1505 &endpoint,
1506 &route_name,
1507 )
1508 .await;
1509 }
1510 }
1511
1512 let referenced = referenced_opt.ok_or_else(|| {
1513 anyhow!(
1514 "[route:{}] Referenced endpoint '{}' not found",
1515 route_name,
1516 name
1517 )
1518 })?;
1519
1520 let mut merged = referenced;
1521 merged.middlewares.extend(endpoint.middlewares);
1524
1525 if endpoint.handler.is_some() {
1526 if merged.handler.is_some() {
1527 return Err(anyhow!("[route:{}] Both ref endpoint and referenced endpoint '{}' have handlers defined. This is ambiguous.", route_name, name));
1528 }
1529 merged.handler = endpoint.handler;
1530 }
1531
1532 return create_publisher_with_depth(route_name, merged, depth + 1).await;
1533 }
1534
1535 let mut publisher =
1536 create_base_publisher(&route_name, &endpoint.endpoint_type, depth).await?;
1537 if let Some(handler) = &endpoint.handler {
1538 publisher = Box::new(crate::command_handler::CommandPublisher::new(
1539 publisher,
1540 handler.clone(),
1541 ));
1542 }
1543 crate::middleware::apply_middlewares_to_publisher(publisher, &endpoint, &route_name).await
1544 })
1545}
1546
1547async fn create_base_publisher(
1548 route_name: &str,
1549 endpoint_type: &EndpointType,
1550 depth: usize,
1551) -> Result<Box<dyn MessagePublisher>> {
1552 let publisher = match endpoint_type {
1553 #[cfg(feature = "aws")]
1554 EndpointType::Aws(cfg) => {
1555 Ok(Box::new(aws::AwsPublisher::new(cfg).await?) as Box<dyn MessagePublisher>)
1556 }
1557 #[cfg(feature = "kafka")]
1558 EndpointType::Kafka(cfg) => {
1559 let mut config = cfg.clone();
1560 if config.topic.is_none() {
1561 config.topic = Some(route_name.to_string());
1562 }
1563 Ok(Box::new(kafka::KafkaPublisher::new(&config).await?) as Box<dyn MessagePublisher>)
1564 }
1565 #[cfg(feature = "nats")]
1566 EndpointType::Nats(cfg) => {
1567 let mut config = cfg.clone();
1568 if config.subject.is_none() {
1569 config.subject = Some(route_name.to_string());
1570 }
1571 Ok(Box::new(nats::NatsPublisher::new(&config).await?) as Box<dyn MessagePublisher>)
1572 }
1573 #[cfg(feature = "amqp")]
1574 EndpointType::Amqp(cfg) => {
1575 let mut config = cfg.clone();
1576 if config.queue.is_none() {
1577 config.queue = Some(route_name.to_string());
1578 }
1579 Ok(Box::new(amqp::AmqpPublisher::new(&config).await?) as Box<dyn MessagePublisher>)
1580 }
1581 #[cfg(feature = "mqtt")]
1582 EndpointType::Mqtt(cfg) => {
1583 let mut config = cfg.clone();
1584 if config.topic.is_none() {
1585 config.topic = Some(route_name.to_string());
1586 }
1587 if config.client_id.is_none() {
1588 config.client_id = Some(format!("{}-{}", crate::APP_NAME, route_name));
1589 }
1590 Ok(Box::new(mqtt::MqttPublisher::new(&config).await?) as Box<dyn MessagePublisher>)
1591 }
1592 #[cfg(any(feature = "zeromq", feature = "zeromq-omq"))]
1593 EndpointType::ZeroMq(cfg) => zeromq::create_publisher(cfg).await,
1594 #[cfg(feature = "redis-streams")]
1595 EndpointType::RedisStreams(cfg) => {
1596 let mut config = cfg.clone();
1597 if config.stream.is_none() {
1598 config.stream = Some(route_name.to_string());
1599 }
1600 Ok(
1601 Box::new(redis_streams::RedisStreamsPublisher::new(&config).await?)
1602 as Box<dyn MessagePublisher>,
1603 )
1604 }
1605 #[cfg(feature = "grpc")]
1606 EndpointType::Grpc(cfg) => {
1607 Ok(Box::new(grpc::GrpcPublisher::new(cfg).await?) as Box<dyn MessagePublisher>)
1608 }
1609 #[cfg(feature = "sqlx")]
1610 EndpointType::Sqlx(cfg) => {
1611 Ok(Box::new(sqlx::SqlxPublisher::new(cfg).await?) as Box<dyn MessagePublisher>)
1612 }
1613 #[cfg(feature = "clickhouse")]
1614 EndpointType::ClickHouse(cfg) => {
1615 Ok(Box::new(clickhouse::ClickHousePublisher::new(cfg).await?)
1616 as Box<dyn MessagePublisher>)
1617 }
1618 #[cfg(feature = "http")]
1619 EndpointType::Http(cfg) => {
1620 let stream_response_sink =
1621 if let Some(stream_response_to) = cfg.stream_response_to.as_deref() {
1622 Some(
1623 create_publisher_with_depth(
1624 route_name.to_string(),
1625 stream_response_to.clone(),
1626 depth + 1,
1627 )
1628 .await?,
1629 )
1630 } else {
1631 None
1632 };
1633 let sink =
1634 http::HttpPublisher::new_with_stream_response_sink(cfg, stream_response_sink)
1635 .await?;
1636 Ok(Box::new(sink) as Box<dyn MessagePublisher>)
1637 }
1638 #[cfg(feature = "websocket")]
1639 EndpointType::WebSocket(cfg) => {
1640 let sink = websocket::WebSocketPublisher::new(cfg);
1641 Ok(Box::new(sink) as Box<dyn MessagePublisher>)
1642 }
1643 #[cfg(feature = "mongodb")]
1644 EndpointType::MongoDb(cfg) => {
1645 let mut config = cfg.clone();
1646 if config.collection.is_none() {
1647 config.collection = Some(route_name.to_string());
1648 }
1649 Ok(Box::new(mongodb::MongoDbPublisher::new(&config).await?)
1650 as Box<dyn MessagePublisher>)
1651 }
1652 EndpointType::File(cfg) => {
1653 Ok(Box::new(file::FilePublisher::new(cfg).await?) as Box<dyn MessagePublisher>)
1654 }
1655 #[cfg(feature = "object-store")]
1656 EndpointType::ObjectStore(cfg) => Ok(Box::new(
1657 object_store::ObjectStorePublisher::new(cfg).await?,
1658 ) as Box<dyn MessagePublisher>),
1659 EndpointType::Static(cfg) => Ok(Box::new(static_endpoint::StaticEndpointPublisher::new(
1660 cfg,
1661 )?) as Box<dyn MessagePublisher>),
1662 EndpointType::Memory(cfg) => {
1663 Ok(Box::new(memory::MemoryPublisher::new_async(cfg).await?)
1664 as Box<dyn MessagePublisher>)
1665 }
1666 EndpointType::StreamBuffer(cfg) => {
1667 Ok(Box::new(stream_buffer::StreamBufferPublisher::new(cfg)?)
1668 as Box<dyn MessagePublisher>)
1669 }
1670 #[cfg(feature = "sled")]
1671 EndpointType::Sled(cfg) => {
1672 Ok(Box::new(sled::SledPublisher::new(cfg)?) as Box<dyn MessagePublisher>)
1673 }
1674 #[cfg(any(feature = "ibm-mq-static", feature = "ibm-mq"))]
1675 EndpointType::IbmMq(cfg) => {
1676 Ok(Box::new(ibm_mq::IbmMqPublisher::new(cfg).await?) as Box<dyn MessagePublisher>)
1677 }
1678 EndpointType::Null => Ok(Box::new(null::NullPublisher) as Box<dyn MessagePublisher>),
1679 EndpointType::Fanout(endpoints) => {
1680 let mut publishers = Vec::with_capacity(endpoints.len());
1681 for endpoint in endpoints {
1682 let p = create_publisher_with_depth(
1683 route_name.to_string(),
1684 endpoint.clone(),
1685 depth + 1,
1686 )
1687 .await?;
1688 publishers.push(p);
1689 }
1690 Ok(Box::new(fanout::FanoutPublisher::new(publishers)) as Box<dyn MessagePublisher>)
1691 }
1692 EndpointType::Switch(cfg) => {
1693 let mut cases = std::collections::HashMap::new();
1694 for (key, endpoint) in &cfg.cases {
1695 let p = create_publisher_with_depth(
1696 route_name.to_string(),
1697 endpoint.clone(),
1698 depth + 1,
1699 )
1700 .await?;
1701 cases.insert(key.clone(), p);
1702 }
1703 let default = if let Some(endpoint) = &cfg.default {
1704 Some(
1705 create_publisher_with_depth(
1706 route_name.to_string(),
1707 (**endpoint).clone(),
1708 depth + 1,
1709 )
1710 .await?,
1711 )
1712 } else {
1713 None
1714 };
1715 Ok(Box::new(switch::SwitchPublisher::new(
1716 cfg.metadata_key.clone(),
1717 cases,
1718 default,
1719 )) as Box<dyn MessagePublisher>)
1720 }
1721 EndpointType::Response(_) => {
1722 Ok(Box::new(response::ResponsePublisher) as Box<dyn MessagePublisher>)
1723 }
1724 EndpointType::Reader(inner) => {
1725 let consumer = create_consumer_from_route(route_name, inner).await?;
1726 Ok(Box::new(reader::ReaderPublisher::new(consumer)) as Box<dyn MessagePublisher>)
1727 }
1728 EndpointType::Request(cfg) => {
1729 let request =
1730 create_publisher_with_depth(route_name.to_string(), (*cfg.to).clone(), depth + 1)
1731 .await?;
1732 let forward = create_publisher_with_depth(
1733 route_name.to_string(),
1734 (*cfg.forward_to).clone(),
1735 depth + 1,
1736 )
1737 .await?;
1738 Ok(
1739 Box::new(request::RequestForwardPublisher::new(request, forward))
1740 as Box<dyn MessagePublisher>,
1741 )
1742 }
1743 EndpointType::Custom { name, config } => {
1744 let factory = get_endpoint_factory(name)
1745 .ok_or_else(|| anyhow!("Custom endpoint factory '{}' not found", name))?;
1746 factory.create_publisher(route_name, config).await
1747 }
1748 #[allow(unreachable_patterns)]
1749 _ => Err(anyhow!(
1750 "[route:{}] Unsupported publisher endpoint type '{:?}'",
1751 route_name,
1752 endpoint_type
1753 )),
1754 }?;
1755 Ok(publisher)
1756}
1757
1758#[cfg(feature = "rustls")]
1768#[allow(unused)]
1769pub(crate) fn get_crypto_provider() -> anyhow::Result<std::sync::Arc<rustls::crypto::CryptoProvider>>
1770{
1771 rustls::crypto::CryptoProvider::get_default()
1772 .cloned()
1773 .ok_or_else(|| {
1774 anyhow!("No rustls CryptoProvider is installed.\n\
1775Fix: 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.")
1776 })
1777}
1778
1779#[cfg(test)]
1780mod tests {
1781 use super::*;
1782 use crate::models::{Endpoint, EndpointType};
1783 use crate::CanonicalMessage;
1784
1785 #[tokio::test]
1786 async fn test_fanout_publisher_integration() {
1787 let ep1 = Endpoint::new_memory("fanout_1", 10);
1788 let ep2 = Endpoint::new_memory("fanout_2", 10);
1789
1790 let chan1 = ep1.channel().unwrap();
1791 let chan2 = ep2.channel().unwrap();
1792 let fanout_ep = Endpoint::new(EndpointType::Fanout(vec![ep1, ep2]));
1793
1794 let publisher = create_publisher_from_route("test_fanout", &fanout_ep)
1795 .await
1796 .expect("Failed to create fanout publisher");
1797
1798 let msg = CanonicalMessage::new(b"fanout_payload".to_vec(), None);
1799 publisher.send(msg).await.expect("Failed to send message");
1800
1801 assert_eq!(chan1.len(), 1);
1802 assert_eq!(chan2.len(), 1);
1803
1804 let msg1 = chan1.drain_messages().pop().unwrap();
1805 let msg2 = chan2.drain_messages().pop().unwrap();
1806
1807 assert_eq!(msg1.payload, "fanout_payload".as_bytes());
1808 assert_eq!(msg2.payload, "fanout_payload".as_bytes());
1809 }
1810
1811 use crate::models::MemoryConfig;
1812 #[tokio::test]
1813 async fn test_factory_creates_memory_subscriber() {
1814 let endpoint = Endpoint {
1815 endpoint_type: EndpointType::Memory(
1816 MemoryConfig::new("mem".to_string(), None).with_subscribe(true),
1817 ),
1818 middlewares: vec![],
1819 handler: None,
1820 };
1821
1822 let consumer = create_consumer_from_route("test", &endpoint).await.unwrap();
1823 let is_subscriber = consumer
1825 .as_any()
1826 .is::<crate::endpoints::memory::MemoryConsumer>();
1827 assert!(is_subscriber, "Factory should create MemoryConsumer");
1828 }
1829
1830 #[cfg(feature = "websocket")]
1831 #[test]
1832 fn websocket_direct_route_support_requires_default_route_options() {
1833 let mut options = crate::models::RouteOptions::default();
1834 assert!(websocket_direct_route_options_allowed(&options));
1835
1836 options.batch_size = 128;
1837 assert!(!websocket_direct_route_options_allowed(&options));
1838 }
1839
1840 #[cfg(feature = "websocket")]
1841 #[test]
1842 fn websocket_direct_route_support_respects_execution_mode_and_output() {
1843 let input = Endpoint::new(EndpointType::WebSocket(
1844 crate::models::WebSocketConfig::new("127.0.0.1:0"),
1845 ));
1846 let response_route = crate::models::Route::new(input.clone(), Endpoint::new_response());
1847 assert!(matches!(
1848 websocket_direct_route_support(&response_route),
1849 WebSocketDirectRouteSupport::Supported
1850 ));
1851
1852 let memory_route = crate::models::Route::new(input.clone(), Endpoint::new_memory("ws", 1));
1853 assert!(matches!(
1854 websocket_direct_route_support(&memory_route),
1855 WebSocketDirectRouteSupport::Unsupported("output is not response")
1856 ));
1857
1858 let routed_input = Endpoint::new(EndpointType::WebSocket(
1859 crate::models::WebSocketConfig::new("127.0.0.1:0")
1860 .with_execution_mode(crate::models::WebSocketExecutionMode::Routed),
1861 ));
1862 let routed_route = crate::models::Route::new(routed_input, Endpoint::new_response());
1863 assert!(matches!(
1864 websocket_direct_route_support(&routed_route),
1865 WebSocketDirectRouteSupport::Unsupported("execution_mode is routed")
1866 ));
1867 }
1868
1869 #[test]
1870 fn test_endpoint_middleware_ordering_helpers() {
1871 let endpoint = Endpoint::new_memory("test", 10)
1872 .with_metrics()
1873 .with_dlq(crate::models::DeadLetterQueueMiddleware::default())
1874 .with_retry(crate::models::RetryMiddleware::default());
1875
1876 assert_eq!(endpoint.middlewares.len(), 3);
1878 assert!(matches!(endpoint.middlewares[0], Middleware::Retry(_)));
1879 assert!(matches!(endpoint.middlewares[1], Middleware::Dlq(_)));
1880 assert!(matches!(endpoint.middlewares[2], Middleware::Metrics(_)));
1881 }
1882
1883 #[cfg(feature = "http")]
1884 #[test]
1885 fn test_http_inline_fast_path_allows_simple_output_publisher_middlewares() {
1886 assert!(output_middlewares_allow_http_inline_fast_path(&[
1887 Middleware::Buffer(crate::models::BufferMiddleware {
1888 max_messages: 16,
1889 max_delay_ms: 0,
1890 }),
1891 Middleware::Delay(crate::models::DelayMiddleware { delay_ms: 0 }),
1892 Middleware::Limiter(crate::models::LimiterMiddleware {
1893 messages_per_second: 1_000_000.0,
1894 }),
1895 ]));
1896
1897 assert!(!output_middlewares_allow_http_inline_fast_path(&[
1898 Middleware::Retry(crate::models::RetryMiddleware::default()),
1899 ]));
1900 assert!(!output_middlewares_allow_http_inline_fast_path(&[
1901 Middleware::Dlq(Box::default()),
1902 ]));
1903 }
1904
1905 #[test]
1906 fn test_consumer_middleware_ordering() {
1907 let endpoint = Endpoint::new_memory("test", 10)
1908 .with_deduplication(crate::models::DeduplicationMiddleware {
1909 store: None,
1910 sled_path: Some("".into()),
1911 ttl_seconds: 10,
1912 })
1913 .with_consumer_metrics();
1914
1915 assert_eq!(endpoint.middlewares.len(), 2);
1919 assert!(matches!(endpoint.middlewares[0], Middleware::Metrics(_)));
1920 assert!(matches!(
1921 endpoint.middlewares[1],
1922 Middleware::Deduplication(_)
1923 ));
1924 }
1925
1926 #[test]
1927 fn test_check_consumer_invalid_config() {
1928 let config = crate::models::MemoryConfig {
1929 topic: "test".to_string(),
1930 request_reply: true, ..Default::default()
1932 };
1933 let endpoint = Endpoint::new(EndpointType::Memory(config));
1934
1935 let warnings = check_consumer("test_route", &endpoint, None).unwrap();
1936 assert!(warnings
1937 .iter()
1938 .any(|w| w.contains("request_reply") && w.contains("publisher-only")));
1939 }
1940}