Skip to main content

mq_bridge/endpoints/
mod.rs

1//  mq-bridge
2//  © Copyright 2025, by Marco Mengelkoch
3//  Licensed under MIT License, see License file for more details
4//  git clone https://github.com/marcomq/mq-bridge
5
6#[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    /// Creates a new in-memory endpoint with the specified topic and capacity.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use mq_bridge::models::Endpoint;
67    /// let endpoint = Endpoint::new_memory("my_topic", 100);
68    /// ```
69    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    ///
99    /// Returns a reference to the in-memory channel associated with this Endpoint.
100    /// This function will only succeed if the Endpoint is of type EndpointType::Memory.
101    /// If the Endpoint is not a memory endpoint, this function will return an error.
102    /// This function is primarily used for testing purposes where a Queue is needed.
103    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        // Retry should be inner to DLQ and Metrics.
115        // We insert it before any existing DLQ or Metrics middleware.
116        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        // DLQ should be outer to Retry, but inner to Metrics.
130        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        // Deduplication is consumer-only.
144        // We insert it at the beginning so it is applied last (innermost) for consumers,
145        // or second to last if metrics are at 0.
146        // List: [Dedup, ...] -> Consumer: ... ( Dedup ( base ) )
147        self.middlewares.insert(0, Middleware::Deduplication(dedup));
148        self
149    }
150
151    pub fn with_consumer_metrics(mut self) -> Self {
152        // For consumers, the first middleware in the list is the outermost (applied last).
153        // Inserting at 0 ensures it wraps everything else (Ingestion Metrics).
154        // List: [Metrics, Dedup] -> Consumer: Metrics ( Dedup ( base ) )
155        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        // Metrics should be outer to everything (last in the list for publishers).
168        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
207/// Validates the consumer configuration for a route.
208pub 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            // We need to check the referenced endpoint, but we don't need to merge middlewares
259            // for the check itself, as we just want to validate the core type.
260            // However, to be thorough, we recurse on the referenced endpoint.
261            // Note: This check ignores the middlewares on the 'ref' itself, which is acceptable for type checking.
262            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        // Merge middlewares: The ref's middlewares should be outer (applied last in the rev() loop).
524        // Since apply_middlewares_to_consumer iterates in reverse, we prepend the ref's middlewares.
525        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
534/// Creates a `MessageConsumer` based on the route's "in" configuration.
535pub 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        // The inline fast path applies to outputs that reply synchronously
554        // without the route worker/disposition pipeline: `response` (handler- or
555        // request-derived reply) and `static` (a fixed, pre-rendered reply).
556        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    // Helper to coerce concrete consumers to the trait object, fixing type inference issues in the match block.
729    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                // For persistent sessions, default client_id to route_name if not provided
768                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); // Remove events by default after 24 hours
813                }
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
837/// Validates the publisher configuration for a route.
838pub 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
1181/// Creates a `MessagePublisher` based on the route's "out" configuration.
1182pub 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            // Merge middlewares: The ref's middlewares should be outer (applied last).
1237            // Since apply_middlewares_to_publisher iterates forward, we append the ref's middlewares to the referenced ones.
1238            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/// Returns the active process-level rustls `CryptoProvider`, or a descriptive error if none
1441/// has been installed yet.
1442///
1443/// This is called by every endpoint that creates a rustls `ClientConfig` / `ServerConfig`.
1444/// As a library, mq-bridge never installs a provider itself; the choice belongs to the
1445/// application binary.  To resolve the error, either:
1446///
1447/// * Enable the **`rustls-ring`** or **`rustls-aws-lc`** feature of `mq-bridge`, or
1448/// * Call `rustls::crypto::CryptoProvider::install_default()` early in your `main()`.
1449#[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        // Check if it is a MemoryConsumer (MemorySubscriber was merged)
1506        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        // Expected order: Retry, Dlq, Metrics
1559        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        // Expected order in list: [Metrics, Dedup]
1597        // Consumer application (rev): Dedup -> Metrics.
1598        // Execution: Metrics( Dedup ( base ) ). Metrics is Outer.
1599        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, // Invalid for consumer
1612            ..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}