Skip to main content

mq_bridge/
models.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
6use serde::{
7    de::{MapAccess, Visitor},
8    Deserialize, Deserializer, Serialize,
9};
10use std::{
11    collections::HashMap,
12    sync::{atomic::AtomicUsize, Arc},
13};
14
15use crate::traits::Handler;
16use tracing::trace;
17
18/// The top-level configuration is a map of named routes.
19/// The key is the route name (e.g., "kafka_to_nats").
20///
21/// # Examples
22///
23/// Deserializing a complex configuration from YAML:
24///
25/// ```
26/// use mq_bridge::models::{Config, EndpointType, Middleware};
27///
28/// let yaml = r#"
29/// kafka_to_nats:
30///   concurrency: 10
31///   input:
32///     middlewares:
33///       - deduplication:
34///           sled_path: "/tmp/mq-bridge/dedup_db"
35///           ttl_seconds: 3600
36///       - metrics: {}
37///       - retry:
38///           max_attempts: 5
39///           initial_interval_ms: 200
40///       - random_panic:
41///           mode: nack
42///       - dlq:
43///           endpoint:
44///             nats:
45///               subject: "dlq-subject"
46///               url: "nats://localhost:4222"
47///     kafka:
48///       topic: "input-topic"
49///       url: "localhost:9092"
50///       group_id: "my-consumer-group"
51///       tls:
52///         required: true
53///         ca_file: "/path_to_ca"
54///         cert_file: "/path_to_cert"
55///         key_file: "/path_to_key"
56///         cert_password: "password"
57///         accept_invalid_certs: true
58///   output:
59///     middlewares:
60///       - metrics: {}
61///       - dlq:
62///           endpoint:
63///             file:
64///               path: "error.out"
65///     nats:
66///       subject: "output-subject"
67///       url: "nats://localhost:4222"
68/// "#;
69///
70/// let config: Config = serde_yaml_ng::from_str(yaml).unwrap();
71/// let route = config.get("kafka_to_nats").unwrap();
72///
73/// assert_eq!(route.options.concurrency, 10);
74/// // Check input middleware
75/// assert!(route.input.middlewares.iter().any(|m| matches!(m, Middleware::Deduplication(_))));
76/// // Check output endpoint
77/// assert!(matches!(route.output.endpoint_type, EndpointType::Nats(_)));
78/// ```
79pub type Config = HashMap<String, Route>;
80
81/// A configuration map for named publishers (endpoints).
82/// The key is the publisher name.
83pub type PublisherConfig = HashMap<String, Endpoint>;
84
85/// Defines a single message processing route from an input to an output.
86#[derive(Debug, Deserialize, Serialize, Clone)]
87#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
88#[cfg_attr(feature = "schema", schemars(transform = route_schema_transform))]
89#[serde(deny_unknown_fields)]
90pub struct Route {
91    /// The input/source endpoint for the route.
92    pub input: Endpoint,
93    /// The output/sink endpoint for the route.
94    #[serde(default = "default_output_endpoint")]
95    pub output: Endpoint,
96    /// (Optional) Fine-tuning options for the route's execution.
97    #[serde(flatten, default)]
98    pub options: RouteOptions,
99}
100
101impl Default for Route {
102    fn default() -> Self {
103        Self {
104            input: Endpoint::null(),
105            output: Endpoint::null(),
106            options: RouteOptions::default(),
107        }
108    }
109}
110
111/// Fine-tuning options for a route's execution.
112///
113/// These options control concurrency, batching, and commit behavior for message processing.
114///
115/// # Examples
116///
117/// ```
118/// use mq_bridge::models::RouteOptions;
119///
120/// let options = RouteOptions {
121///     description: "My Route".to_string(),
122///     concurrency: 10,
123///     batch_size: 5,
124///     commit_concurrency_limit: 1024,
125///     ..Default::default()
126/// };
127/// ```
128#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
129#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
130#[serde(deny_unknown_fields)]
131pub struct RouteOptions {
132    /// A human-readable description of the route's purpose. Defaults to an empty string.
133    #[serde(default, skip_serializing_if = "String::is_empty")]
134    pub description: String,
135    /// (Optional) Number of concurrent processing tasks for this route. While it improves throughput for high-latency
136    /// handlers, it adds synchronization overhead for ordered commits and may lead to out-of-order processing
137    /// in the handler. Defaults to 1.
138    #[serde(default = "default_concurrency")]
139    #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
140    pub concurrency: usize,
141    /// (Optional) Maximum number of messages to process in a single batch. The consumer waits for at least one message
142    /// and then attempts to fetch more if available. Increasing this improves throughput but also increases
143    /// the potential impact of a single batch processing failure. Defaults to 1.
144    #[serde(default = "default_batch_size")]
145    #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
146    pub batch_size: usize,
147    /// (Optional) The maximum number of in-flight commit requests queued for ordered sequencing.
148    /// Lower values apply backpressure earlier; higher values allow larger commit backlogs.
149    /// Defaults to 4096.
150    #[serde(default = "default_commit_concurrency_limit")]
151    #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
152    pub commit_concurrency_limit: usize,
153    /// Time to wait for a route to establish connections before startup fails. Defaults to 5000ms.
154    #[serde(default = "default_startup_timeout_ms")]
155    pub startup_timeout_ms: u64,
156    /// Time to wait before reconnecting after a transient route failure. Defaults to 5000ms.
157    #[serde(default = "default_reconnect_interval_ms")]
158    pub reconnect_interval_ms: u64,
159    /// Delay after an empty receive batch to avoid hot polling. Set to 0 to only yield. Defaults to 10ms.
160    #[serde(default = "default_empty_batch_delay_ms")]
161    pub empty_batch_delay_ms: u64,
162    /// Allows fault-injection middleware such as random_panic. Disabled by default.
163    #[serde(default = "default_false", skip_serializing_if = "is_false")]
164    #[cfg_attr(feature = "schema", schemars(default = "default_false"))]
165    pub allow_fault_injection: bool,
166}
167
168impl Default for RouteOptions {
169    fn default() -> Self {
170        Self {
171            description: String::new(),
172            concurrency: default_concurrency(),
173            batch_size: default_batch_size(),
174            commit_concurrency_limit: default_commit_concurrency_limit(),
175            startup_timeout_ms: default_startup_timeout_ms(),
176            reconnect_interval_ms: default_reconnect_interval_ms(),
177            empty_batch_delay_ms: default_empty_batch_delay_ms(),
178            allow_fault_injection: false,
179        }
180    }
181}
182
183impl RouteOptions {
184    pub fn validate(&self) -> anyhow::Result<()> {
185        if self.concurrency == 0 {
186            return Err(anyhow::anyhow!("route concurrency must be at least 1"));
187        }
188        if self.batch_size == 0 {
189            return Err(anyhow::anyhow!("route batch_size must be at least 1"));
190        }
191        if self.commit_concurrency_limit == 0 {
192            return Err(anyhow::anyhow!(
193                "route commit_concurrency_limit must be at least 1"
194            ));
195        }
196        Ok(())
197    }
198}
199
200pub(crate) fn default_concurrency() -> usize {
201    1
202}
203
204pub(crate) fn default_batch_size() -> usize {
205    1
206}
207
208pub(crate) fn default_commit_concurrency_limit() -> usize {
209    4096
210}
211
212pub(crate) fn default_startup_timeout_ms() -> u64 {
213    5000
214}
215
216pub(crate) fn default_reconnect_interval_ms() -> u64 {
217    5000
218}
219
220pub(crate) fn default_empty_batch_delay_ms() -> u64 {
221    10
222}
223
224fn is_false(value: &bool) -> bool {
225    !*value
226}
227
228fn default_false() -> bool {
229    false
230}
231
232#[cfg(feature = "schema")]
233fn default_inline_response_fast_path_schema() -> Option<bool> {
234    Some(true)
235}
236
237fn default_output_endpoint() -> Endpoint {
238    Endpoint::new(EndpointType::Null)
239}
240
241fn default_retry_attempts() -> usize {
242    3
243}
244fn default_initial_interval_ms() -> u64 {
245    100
246}
247fn default_max_interval_ms() -> u64 {
248    5000
249}
250fn default_multiplier() -> f64 {
251    2.0
252}
253fn default_clean_session() -> bool {
254    false
255}
256fn default_cookie_metadata_key() -> String {
257    "cookie".to_string()
258}
259fn default_set_cookie_metadata_key() -> String {
260    "set-cookie".to_string()
261}
262
263fn is_known_endpoint_name(name: &str) -> bool {
264    matches!(
265        name,
266        "aws"
267            | "kafka"
268            | "nats"
269            | "file"
270            | "static"
271            | "memory"
272            | "sled"
273            | "amqp"
274            | "mongodb"
275            | "mqtt"
276            | "http"
277            | "websocket"
278            | "ibmmq"
279            | "zeromq"
280            | "grpc"
281            | "fanout"
282            | "stream_buffer"
283            | "ref"
284            | "switch"
285            | "response"
286            | "reader"
287            | "null"
288            | "sqlx"
289    )
290}
291
292/// Represents a connection point for messages, which can be a source (input) or a sink (output).
293#[derive(Serialize, Clone, Default)]
294#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
295#[serde(deny_unknown_fields)]
296pub struct Endpoint {
297    /// (Optional) A list of middlewares to apply to the endpoint.
298    #[serde(default)]
299    pub middlewares: Vec<Middleware>,
300
301    /// The specific endpoint implementation, determined by the configuration key (e.g., "kafka", "nats").
302    #[serde(flatten)]
303    pub endpoint_type: EndpointType,
304
305    #[serde(skip_serializing)]
306    #[cfg_attr(feature = "schema", schemars(skip))]
307    /// Internal handler for processing messages (not serialized).
308    pub handler: Option<Arc<dyn Handler>>,
309}
310
311impl std::fmt::Debug for Endpoint {
312    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        f.debug_struct("Endpoint")
314            .field("middlewares", &self.middlewares)
315            .field("endpoint_type", &self.endpoint_type)
316            .field(
317                "handler",
318                &if self.handler.is_some() {
319                    "Some(<Handler>)"
320                } else {
321                    "None"
322                },
323            )
324            .finish()
325    }
326}
327
328impl<'de> Deserialize<'de> for Endpoint {
329    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
330    where
331        D: Deserializer<'de>,
332    {
333        struct EndpointVisitor;
334
335        impl<'de> Visitor<'de> for EndpointVisitor {
336            type Value = Endpoint;
337
338            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
339                formatter.write_str("a map representing an endpoint or null")
340            }
341
342            fn visit_unit<E>(self) -> Result<Self::Value, E>
343            where
344                E: serde::de::Error,
345            {
346                Ok(Endpoint::new(EndpointType::Null))
347            }
348
349            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
350            where
351                A: MapAccess<'de>,
352            {
353                // Buffer the map into a temporary serde_json::Map.
354                // This allows us to separate the `middlewares` field from the rest.
355                let mut temp_map = serde_json::Map::new();
356                let mut middlewares_val = None;
357
358                while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
359                    if key == "middlewares" {
360                        middlewares_val = Some(value);
361                    } else {
362                        temp_map.insert(key, value);
363                    }
364                }
365
366                // Deserialize the rest of the map into the flattened EndpointType.
367                let temp_val = serde_json::Value::Object(temp_map);
368                let endpoint_type: EndpointType = match serde_json::from_value(temp_val.clone()) {
369                    Ok(et) => et,
370                    Err(original_err) => {
371                        if let serde_json::Value::Object(map) = &temp_val {
372                            if map.len() == 1 {
373                                let (name, config) = map.iter().next().unwrap();
374                                if is_known_endpoint_name(name) {
375                                    return Err(serde::de::Error::custom(original_err));
376                                }
377                                trace!("Falling back to Custom endpoint for key: {}", name);
378                                EndpointType::Custom {
379                                    name: name.clone(),
380                                    config: config.clone(),
381                                }
382                            } else if map.is_empty() {
383                                EndpointType::Null
384                            } else {
385                                return Err(serde::de::Error::custom(
386                                    "Invalid endpoint configuration: multiple keys found or unknown endpoint type",
387                                ));
388                            }
389                        } else {
390                            return Err(serde::de::Error::custom("Invalid endpoint configuration"));
391                        }
392                    }
393                };
394
395                // Deserialize the extracted middlewares value using the existing helper logic.
396                let middlewares = match middlewares_val {
397                    Some(val) => {
398                        deserialize_middlewares_from_value(val).map_err(serde::de::Error::custom)?
399                    }
400                    None => Vec::new(),
401                };
402
403                Ok(Endpoint {
404                    middlewares,
405                    endpoint_type,
406                    handler: None,
407                })
408            }
409        }
410
411        deserializer.deserialize_any(EndpointVisitor)
412    }
413}
414
415fn is_known_middleware_name(name: &str) -> bool {
416    matches!(
417        name,
418        "deduplication"
419            | "metrics"
420            | "dlq"
421            | "retry"
422            | "random_panic"
423            | "delay"
424            | "weak_join"
425            | "limiter"
426            | "buffer"
427            | "cookie_jar"
428            | "custom"
429    )
430}
431
432/// Deserialize middlewares from a generic serde_json::Value.
433///
434/// This logic was extracted from `deserialize_middlewares_from_map_or_seq` to be reused by the custom `Endpoint` deserializer.
435fn deserialize_middlewares_from_value(value: serde_json::Value) -> anyhow::Result<Vec<Middleware>> {
436    let arr = match value {
437        serde_json::Value::Array(arr) => arr,
438        serde_json::Value::Object(map) => {
439            let mut middlewares: Vec<_> = map
440                .into_iter()
441                // The config crate can produce maps with numeric string keys ("0", "1", ...)
442                // from environment variables. We need to sort by these keys to maintain order.
443                .filter_map(|(key, value)| key.parse::<usize>().ok().map(|index| (index, value)))
444                .collect();
445            middlewares.sort_by_key(|(index, _)| *index);
446
447            middlewares.into_iter().map(|(_, value)| value).collect()
448        }
449        _ => return Err(anyhow::anyhow!("Expected an array or object")),
450    };
451
452    let mut middlewares = Vec::new();
453    for item in arr {
454        // Check if it is a map with a single key that matches a known middleware
455        let known_name = if let serde_json::Value::Object(map) = &item {
456            if map.len() == 1 {
457                let (name, _) = map.iter().next().unwrap();
458                if is_known_middleware_name(name) {
459                    Some(name.clone())
460                } else {
461                    None
462                }
463            } else {
464                None
465            }
466        } else {
467            None
468        };
469
470        if let Some(name) = known_name {
471            match serde_json::from_value::<Middleware>(item.clone()) {
472                Ok(m) => middlewares.push(m),
473                Err(e) => {
474                    return Err(anyhow::anyhow!(
475                        "Failed to deserialize known middleware '{}': {}",
476                        name,
477                        e
478                    ))
479                }
480            }
481        } else if let Ok(m) = serde_json::from_value::<Middleware>(item.clone()) {
482            middlewares.push(m);
483        } else if let serde_json::Value::Object(map) = &item {
484            if map.len() == 1 {
485                let (name, config) = map.iter().next().unwrap();
486                middlewares.push(Middleware::Custom {
487                    name: name.clone(),
488                    config: config.clone(),
489                });
490            } else {
491                return Err(anyhow::anyhow!(
492                    "Invalid middleware configuration: {:?}",
493                    item
494                ));
495            }
496        } else {
497            return Err(anyhow::anyhow!(
498                "Invalid middleware configuration: {:?}",
499                item
500            ));
501        }
502    }
503    Ok(middlewares)
504}
505
506/// Configuration for the `static` endpoint.
507///
508/// Accepts either a bare string (the response body, JSON-encoded for backward
509/// compatibility) or a map for full control:
510///
511/// ```yaml
512/// # bare string  -> body is JSON-encoded ("Hello" comes back quoted)
513/// static: "Hello, World!"
514///
515/// # map form -> raw body + custom metadata (HTTP maps metadata to headers)
516/// static:
517///   body: "Hello, World!"
518///   raw: true
519///   metadata:
520///     content-type: "text/plain"
521///     server: "mq-bridge"
522/// ```
523///
524/// When `raw` is true the body is sent verbatim; otherwise it is JSON-encoded as
525/// a string. Every entry in `metadata` is attached to the produced message; when
526/// this endpoint feeds an HTTP response, those entries become response headers
527/// (e.g. `content-type`), otherwise they are ordinary message metadata.
528#[derive(Debug, Clone, Default)]
529pub struct StaticConfig {
530    /// The static response body.
531    pub body: String,
532    /// Send the body verbatim instead of JSON-encoding it as a string.
533    pub raw: bool,
534    /// Extra metadata entries attached to the produced message.
535    pub metadata: std::collections::HashMap<String, String>,
536}
537
538// Hand-written schema: the `Deserialize` impl below accepts either a bare string
539// or a map where only `body` is required, so the derived all-fields-required
540// object schema would reject valid configs.
541#[cfg(feature = "schema")]
542impl schemars::JsonSchema for StaticConfig {
543    fn schema_name() -> std::borrow::Cow<'static, str> {
544        "StaticConfig".into()
545    }
546
547    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
548        schemars::json_schema!({
549            "description": "Configuration for the `static` endpoint. Accepts either a bare string (the response body, JSON-encoded for backward compatibility) or a map where only `body` is required and `raw` / `metadata` are optional.",
550            "oneOf": [
551                {
552                    "type": "string",
553                    "description": "The response body, JSON-encoded as a string."
554                },
555                {
556                    "type": "object",
557                    "properties": {
558                        "body": {
559                            "type": "string",
560                            "description": "The static response body."
561                        },
562                        "raw": {
563                            "type": "boolean",
564                            "description": "Send the body verbatim instead of JSON-encoding it as a string.",
565                            "default": false
566                        },
567                        "metadata": {
568                            "type": "object",
569                            "description": "Extra metadata entries attached to the produced message.",
570                            "additionalProperties": { "type": "string" }
571                        }
572                    },
573                    "required": ["body"],
574                    "additionalProperties": false
575                }
576            ]
577        })
578    }
579}
580
581impl Serialize for StaticConfig {
582    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
583    where
584        S: serde::Serializer,
585    {
586        // Backward-compatible: when no extra options are set, serialize as a bare
587        // string exactly like the historical `Static(String)` so configs written
588        // by this version remain readable by older versions.
589        if !self.raw && self.metadata.is_empty() {
590            return serializer.serialize_str(&self.body);
591        }
592        use serde::ser::SerializeStruct;
593        let mut state = serializer.serialize_struct("StaticConfig", 3)?;
594        state.serialize_field("body", &self.body)?;
595        state.serialize_field("raw", &self.raw)?;
596        state.serialize_field("metadata", &self.metadata)?;
597        state.end()
598    }
599}
600
601impl From<String> for StaticConfig {
602    fn from(body: String) -> Self {
603        StaticConfig {
604            body,
605            raw: false,
606            metadata: std::collections::HashMap::new(),
607        }
608    }
609}
610
611impl From<&str> for StaticConfig {
612    fn from(body: &str) -> Self {
613        StaticConfig::from(body.to_string())
614    }
615}
616
617impl<'de> Deserialize<'de> for StaticConfig {
618    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
619    where
620        D: serde::Deserializer<'de>,
621    {
622        #[derive(Deserialize)]
623        #[serde(untagged)]
624        enum Repr {
625            Str(String),
626            Map {
627                body: String,
628                #[serde(default)]
629                raw: bool,
630                #[serde(default)]
631                metadata: std::collections::HashMap<String, String>,
632            },
633        }
634        Ok(match Repr::deserialize(deserializer)? {
635            Repr::Str(body) => StaticConfig {
636                body,
637                raw: false,
638                metadata: std::collections::HashMap::new(),
639            },
640            Repr::Map {
641                body,
642                raw,
643                metadata,
644            } => StaticConfig {
645                body,
646                raw,
647                metadata,
648            },
649        })
650    }
651}
652
653/// An enumeration of all supported endpoint types.
654/// `#[serde(rename_all = "lowercase")]` ensures that the keys in the config (e.g., "kafka")
655/// match the enum variants.
656///
657/// # Examples
658///
659/// Configuring a Fanout endpoint in YAML:
660/// ```
661/// use mq_bridge::models::{Endpoint, EndpointType};
662///
663/// let yaml = r#"
664/// fanout:
665///   - memory: { topic: "out1" }
666///   - memory: { topic: "out2" }
667/// "#;
668///
669/// let endpoint: Endpoint = serde_yaml_ng::from_str(yaml).unwrap();
670/// if let EndpointType::Fanout(targets) = endpoint.endpoint_type {
671///     assert_eq!(targets.len(), 2);
672/// }
673/// ```
674#[derive(Debug, Deserialize, Serialize, Clone, Default)]
675#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
676#[serde(rename_all = "lowercase")]
677pub enum EndpointType {
678    Aws(AwsConfig),
679    Kafka(KafkaConfig),
680    Nats(NatsConfig),
681    File(FileConfig),
682    Static(StaticConfig),
683    Ref(String),
684    Memory(MemoryConfig),
685    Sled(SledConfig),
686    Amqp(AmqpConfig),
687    MongoDb(MongoDbConfig),
688    Mqtt(MqttConfig),
689    Http(HttpConfig),
690    WebSocket(WebSocketConfig),
691    IbmMq(IbmMqConfig),
692    ZeroMq(ZeroMqConfig),
693    Grpc(GrpcConfig),
694    Sqlx(SqlxConfig),
695    Fanout(Vec<Endpoint>),
696    #[serde(rename = "stream_buffer")]
697    StreamBuffer(StreamBufferConfig),
698    Switch(SwitchConfig),
699    Response(ResponseConfig),
700    Reader(Box<Endpoint>),
701    Custom {
702        name: String,
703        config: serde_json::Value,
704    },
705    #[default]
706    Null,
707}
708
709impl EndpointType {
710    pub fn name(&self) -> &'static str {
711        match self {
712            EndpointType::Aws(_) => "aws",
713            EndpointType::Kafka(_) => "kafka",
714            EndpointType::Nats(_) => "nats",
715            EndpointType::File(_) => "file",
716            EndpointType::Static(_) => "static",
717            EndpointType::Ref(_) => "ref",
718            EndpointType::Memory(_) => "memory",
719            EndpointType::Sled(_) => "sled",
720            EndpointType::Amqp(_) => "amqp",
721            EndpointType::MongoDb(_) => "mongodb",
722            EndpointType::Mqtt(_) => "mqtt",
723            EndpointType::Http(_) => "http",
724            EndpointType::WebSocket(_) => "websocket",
725            EndpointType::IbmMq(_) => "ibmmq",
726            EndpointType::ZeroMq(_) => "zeromq",
727            EndpointType::Grpc(_) => "grpc",
728            EndpointType::Sqlx(_) => "sqlx",
729            EndpointType::Fanout(_) => "fanout",
730            EndpointType::StreamBuffer(_) => "stream_buffer",
731            EndpointType::Switch(_) => "switch",
732            EndpointType::Response(_) => "response",
733            EndpointType::Reader(_) => "reader",
734            EndpointType::Custom { .. } => "custom",
735            EndpointType::Null => "null",
736        }
737    }
738
739    pub fn is_core(&self) -> bool {
740        matches!(
741            self,
742            EndpointType::File(_)
743                | EndpointType::Static(_)
744                | EndpointType::Ref(_)
745                | EndpointType::Memory(_)
746                | EndpointType::Fanout(_)
747                | EndpointType::StreamBuffer(_)
748                | EndpointType::Switch(_)
749                | EndpointType::Response(_)
750                | EndpointType::Reader(_)
751                | EndpointType::Custom { .. }
752                | EndpointType::Null
753        )
754    }
755}
756
757/// An enumeration of all supported middleware types.
758#[derive(Debug, Deserialize, Serialize, Clone)]
759#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
760#[serde(rename_all = "snake_case")]
761pub enum Middleware {
762    Deduplication(DeduplicationMiddleware),
763    Metrics(MetricsMiddleware),
764    Dlq(Box<DeadLetterQueueMiddleware>),
765    Retry(RetryMiddleware),
766    RandomPanic(RandomPanicMiddleware),
767    Delay(DelayMiddleware),
768    WeakJoin(WeakJoinMiddleware),
769    Limiter(LimiterMiddleware),
770    Buffer(BufferMiddleware),
771    CookieJar(CookieJarMiddleware),
772    Custom {
773        name: String,
774        config: serde_json::Value,
775    },
776}
777
778/// Deduplication middleware configuration.
779///
780/// Prevents duplicate messages from being processed using a Sled-backed database.
781/// Messages are identified by their deduplication key and removed after the TTL expires.
782#[derive(Debug, Deserialize, Serialize, Clone)]
783#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
784#[serde(deny_unknown_fields)]
785pub struct DeduplicationMiddleware {
786    /// Path to the Sled database directory.
787    pub sled_path: String,
788    /// Time-to-live for deduplication entries in seconds.
789    pub ttl_seconds: u64,
790}
791
792/// Metrics middleware configuration.
793///
794/// Enables collection and reporting of message processing metrics such as throughput,
795/// latency, and error rates. The presence of this middleware in the configuration
796/// enables metrics collection for the endpoint.
797///
798/// Metrics are typically exported via Prometheus or similar monitoring systems.
799#[derive(Debug, Deserialize, Serialize, Clone)]
800#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
801#[serde(deny_unknown_fields)]
802pub struct MetricsMiddleware {}
803
804/// Dead-Letter Queue (DLQ) middleware configuration.
805///
806/// Routes failed messages to a designated endpoint for later analysis and recovery.
807/// It is recommended to pair this with the Retry middleware to avoid message loss.
808///
809/// Failed messages are sent to the configured endpoint when they are exhausted after retry attempts.
810#[derive(Debug, Deserialize, Serialize, Clone, Default)]
811#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
812#[serde(deny_unknown_fields)]
813pub struct DeadLetterQueueMiddleware {
814    /// The endpoint to send failed messages to.
815    pub endpoint: Endpoint,
816}
817
818/// Retry middleware configuration.
819///
820/// Implements exponential backoff retry logic for failed message processing.
821/// Failed messages are automatically retried with increasing delays between attempts.
822#[derive(Debug, Deserialize, Serialize, Clone, Default)]
823#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
824#[serde(deny_unknown_fields)]
825pub struct RetryMiddleware {
826    /// Maximum number of retry attempts. Defaults to 3.
827    #[serde(default = "default_retry_attempts")]
828    pub max_attempts: usize,
829    /// Initial retry interval in milliseconds. Defaults to 100ms.
830    #[serde(default = "default_initial_interval_ms")]
831    pub initial_interval_ms: u64,
832    /// Maximum retry interval in milliseconds. Defaults to 5000ms.
833    #[serde(default = "default_max_interval_ms")]
834    pub max_interval_ms: u64,
835    /// Multiplier for exponential backoff. Defaults to 2.0.
836    #[serde(default = "default_multiplier")]
837    pub multiplier: f64,
838}
839
840/// Delay middleware configuration.
841///
842/// Introduces a fixed delay before processing each message.
843/// Useful for rate limiting, testing, or allowing time for dependent systems to become ready.
844#[derive(Debug, Deserialize, Serialize, Clone)]
845#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
846#[serde(deny_unknown_fields)]
847pub struct DelayMiddleware {
848    /// Delay duration in milliseconds.
849    pub delay_ms: u64,
850}
851
852/// Throughput limiter middleware configuration.
853///
854/// Applies a best-effort pacing delay so an endpoint does not exceed the configured
855/// message rate. For batch operations the limiter accounts for the number of messages
856/// in the batch, not just the batch count.
857#[derive(Debug, Deserialize, Serialize, Clone)]
858#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
859#[serde(deny_unknown_fields)]
860pub struct LimiterMiddleware {
861    /// Target throughput in messages per second. Must be greater than zero.
862    pub messages_per_second: f64,
863}
864
865/// Publisher-side buffer middleware configuration.
866///
867/// Buffers outbound messages briefly so multiple single-message sends can be
868/// forwarded as one `send_batch` call to the wrapped publisher.
869#[derive(Debug, Deserialize, Serialize, Clone)]
870#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
871#[serde(deny_unknown_fields)]
872pub struct BufferMiddleware {
873    /// Maximum number of messages to accumulate before flushing immediately.
874    pub max_messages: usize,
875    /// Maximum time to wait before flushing a non-full buffer.
876    pub max_delay_ms: u64,
877}
878
879/// Cookie/session jar middleware configuration.
880///
881/// Optimized for HTTP by default: it can read `cookie` and `set-cookie` metadata,
882/// persist session cookies, and inject them into later outgoing requests.
883///
884/// The middleware can also capture arbitrary metadata values into the same session store
885/// and optionally expose stored values back into message metadata.
886#[derive(Debug, Deserialize, Serialize, Clone)]
887#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
888#[serde(deny_unknown_fields)]
889pub struct CookieJarMiddleware {
890    /// Optional shared scope name. When set, middleware instances using the same scope
891    /// share one session store across endpoints/routes in the process.
892    #[serde(default)]
893    pub shared_scope: Option<String>,
894    /// Metadata key used to read/write HTTP Cookie headers. Defaults to `cookie`.
895    #[serde(default = "default_cookie_metadata_key")]
896    pub cookie_metadata_key: String,
897    /// Metadata key used to read HTTP Set-Cookie responses. Defaults to `set-cookie`.
898    #[serde(default = "default_set_cookie_metadata_key")]
899    pub set_cookie_metadata_key: String,
900    /// Additional metadata keys to persist into the session value store.
901    #[serde(default)]
902    pub capture_metadata_keys: Vec<String>,
903    /// Optional metadata prefix used to export stored values back onto each message.
904    ///
905    /// Exported keys use `PREFIXcookie.<name>` for cookies and `PREFIXvalue.<name>` for
906    /// captured generic values.
907    #[serde(default)]
908    pub export_metadata_prefix: Option<String>,
909    /// Optional mapping of outgoing metadata keys to stored session value names.
910    ///
911    /// Example: `{ "authorization": "access_token" }` copies the stored value
912    /// `access_token` into outgoing metadata key `authorization` when not already present.
913    #[serde(default)]
914    pub inject_metadata: HashMap<String, String>,
915}
916
917impl Default for CookieJarMiddleware {
918    fn default() -> Self {
919        Self {
920            shared_scope: None,
921            cookie_metadata_key: default_cookie_metadata_key(),
922            set_cookie_metadata_key: default_set_cookie_metadata_key(),
923            capture_metadata_keys: Vec::new(),
924            export_metadata_prefix: None,
925            inject_metadata: HashMap::new(),
926        }
927    }
928}
929
930/// Weak Join middleware configuration.
931///
932/// Groups and correlates messages based on a metadata key, waiting for a specified number
933/// of messages within a timeout window before processing them as a batch.
934/// Messages that exceed the timeout are processed individually.
935#[derive(Debug, Deserialize, Serialize, Clone)]
936#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
937#[serde(deny_unknown_fields)]
938pub struct WeakJoinMiddleware {
939    /// The metadata key to group messages by (e.g., "correlation_id").
940    pub group_by: String,
941    /// The number of messages to wait for.
942    pub expected_count: usize,
943    /// Timeout in milliseconds.
944    pub timeout_ms: u64,
945}
946
947/// Fault injection modes for testing error handling and recovery mechanisms.
948#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
949#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
950#[serde(rename_all = "snake_case")]
951pub enum FaultMode {
952    /// Trigger a thread panic.
953    #[default]
954    Panic,
955    /// Simulate a connection/network error (retryable).
956    Disconnect,
957    /// Simulate a timeout error (retryable).
958    Timeout,
959    /// Simulate a JSON format error (non-retryable).
960    JsonFormatError,
961    /// Return a negative acknowledgement (for handlers).
962    Nack,
963}
964
965impl std::fmt::Display for FaultMode {
966    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
967        match self {
968            FaultMode::Panic => write!(f, "panic"),
969            FaultMode::Disconnect => write!(f, "disconnect"),
970            FaultMode::Timeout => write!(f, "timeout"),
971            FaultMode::JsonFormatError => write!(f, "json_format_error"),
972            FaultMode::Nack => write!(f, "nack"),
973        }
974    }
975}
976
977/// Middleware for fault injection testing.
978///
979/// Allows testing error handling and recovery mechanisms by injecting faults
980/// at specific points in the message processing pipeline.
981///
982/// # Examples
983///
984/// ```yaml
985/// random_panic:
986///   mode: panic
987///   trigger_on_message: 3  # Trigger on the 3rd message
988/// ```
989#[derive(Debug, Clone, Serialize, Deserialize, Default)]
990#[serde(deny_unknown_fields)]
991#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
992pub struct RandomPanicMiddleware {
993    /// The type of fault to inject.
994    #[serde(default)]
995    pub mode: FaultMode,
996    /// Trigger the fault on the Nth message (1-indexed). None = trigger on every message.
997    #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
998    #[serde(default)]
999    pub trigger_on_message: Option<usize>,
1000    /// Enable/disable the fault injection without removing the configuration.
1001    #[serde(default = "default_true")]
1002    pub enabled: bool,
1003    #[serde(skip, default = "default_atomic_usize_arc")]
1004    #[cfg_attr(feature = "schema", schemars(skip))]
1005    pub message_count: Arc<AtomicUsize>,
1006}
1007
1008fn default_true() -> bool {
1009    true
1010}
1011
1012fn default_atomic_usize_arc() -> Arc<AtomicUsize> {
1013    Arc::new(AtomicUsize::new(0))
1014}
1015
1016fn deserialize_null_as_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
1017where
1018    D: Deserializer<'de>,
1019{
1020    let opt = Option::<bool>::deserialize(deserializer)?;
1021    Ok(opt.unwrap_or(false))
1022}
1023
1024// --- AWS Specific Configuration ---
1025#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1026#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1027#[serde(deny_unknown_fields)]
1028pub struct AwsConfig {
1029    /// The SQS queue URL. Required for Consumer. Optional for Publisher if `topic_arn` is set. If it contains userinfo, it will be treated as a secret.
1030    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1031    pub queue_url: Option<String>,
1032    /// (Publisher only) The SNS topic ARN.
1033    pub topic_arn: Option<String>,
1034    /// AWS Region (e.g., "us-east-1").
1035    pub region: Option<String>,
1036    /// Custom endpoint URL (e.g., for LocalStack).
1037    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1038    pub endpoint_url: Option<String>,
1039    /// AWS Access Key ID.
1040    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1041    pub access_key: Option<String>,
1042    /// AWS Secret Access Key.
1043    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1044    pub secret_key: Option<String>,
1045    /// AWS Session Token.
1046    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1047    pub session_token: Option<String>,
1048    /// (Consumer only) Maximum number of messages to receive in a batch (1-10).
1049    #[cfg_attr(feature = "schema", schemars(range(min = 1, max = 10)))]
1050    pub max_messages: Option<i32>,
1051    /// (Consumer only) Wait time for long polling in seconds (0-20).
1052    #[cfg_attr(feature = "schema", schemars(range(min = 0, max = 20)))]
1053    pub wait_time_seconds: Option<i32>,
1054    /// Use binary payloads in SQS/SNS messages.
1055    #[serde(default)]
1056    pub binary_payload_mode: bool,
1057}
1058
1059impl AwsConfig {
1060    /// Creates a new AWS configuration with default settings.
1061    pub fn new() -> Self {
1062        Self::default()
1063    }
1064
1065    pub fn with_queue_url(mut self, queue_url: impl Into<String>) -> Self {
1066        self.queue_url = Some(queue_url.into());
1067        self
1068    }
1069
1070    pub fn with_topic_arn(mut self, topic_arn: impl Into<String>) -> Self {
1071        self.topic_arn = Some(topic_arn.into());
1072        self
1073    }
1074
1075    pub fn with_region(mut self, region: impl Into<String>) -> Self {
1076        self.region = Some(region.into());
1077        self
1078    }
1079
1080    pub fn with_endpoint_url(mut self, endpoint_url: impl Into<String>) -> Self {
1081        self.endpoint_url = Some(endpoint_url.into());
1082        self
1083    }
1084
1085    pub fn with_credentials(
1086        mut self,
1087        access_key: impl Into<String>,
1088        secret_key: impl Into<String>,
1089    ) -> Self {
1090        self.access_key = Some(access_key.into());
1091        self.secret_key = Some(secret_key.into());
1092        self
1093    }
1094}
1095
1096// --- Kafka Specific Configuration ---
1097
1098/// General Kafka connection configuration.
1099#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1100#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1101#[serde(deny_unknown_fields)]
1102pub struct KafkaConfig {
1103    /// Comma-separated list of Kafka broker URLs. If it contains userinfo, it will be treated as a secret.
1104    #[serde(alias = "brokers")]
1105    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1106    pub url: String,
1107    /// The Kafka topic to produce to or consume from.
1108    pub topic: Option<String>,
1109    /// Optional username for SASL authentication.
1110    pub username: Option<String>,
1111    /// Optional password for SASL authentication.
1112    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1113    pub password: Option<String>,
1114    /// TLS configuration.
1115    #[serde(default)]
1116    pub tls: TlsConfig,
1117    /// (Consumer only) Consumer group ID.
1118    /// If not provided, the consumer acts in **Subscriber mode**: it generates a unique, ephemeral group ID and starts consuming from the latest offset.
1119    pub group_id: Option<String>,
1120    /// (Publisher only) If true, do not wait for an acknowledgement when sending to broker. Defaults to false.
1121    #[serde(default)]
1122    pub delayed_ack: bool,
1123    /// (Publisher only) Additional librdkafka producer configuration options (key-value pairs).
1124    #[serde(default)]
1125    pub producer_options: Option<Vec<(String, String)>>,
1126    /// (Consumer only) Additional librdkafka consumer configuration options (key-value pairs).
1127    #[serde(default)]
1128    pub consumer_options: Option<Vec<(String, String)>>,
1129}
1130
1131impl KafkaConfig {
1132    /// Creates a new Kafka configuration with the specified broker URL.
1133    pub fn new(url: impl Into<String>) -> Self {
1134        Self {
1135            url: url.into(),
1136            ..Default::default()
1137        }
1138    }
1139
1140    pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
1141        self.topic = Some(topic.into());
1142        self
1143    }
1144
1145    pub fn with_group_id(mut self, group_id: impl Into<String>) -> Self {
1146        self.group_id = Some(group_id.into());
1147        self
1148    }
1149
1150    pub fn with_credentials(
1151        mut self,
1152        username: impl Into<String>,
1153        password: impl Into<String>,
1154    ) -> Self {
1155        self.username = Some(username.into());
1156        self.password = Some(password.into());
1157        self
1158    }
1159
1160    pub fn with_producer_option(
1161        mut self,
1162        key: impl Into<String>,
1163        value: impl Into<String>,
1164    ) -> Self {
1165        let options = self.producer_options.get_or_insert_with(Vec::new);
1166        options.push((key.into(), value.into()));
1167        self
1168    }
1169
1170    pub fn with_consumer_option(
1171        mut self,
1172        key: impl Into<String>,
1173        value: impl Into<String>,
1174    ) -> Self {
1175        let options = self.consumer_options.get_or_insert_with(Vec::new);
1176        options.push((key.into(), value.into()));
1177        self
1178    }
1179}
1180
1181// --- Sled Specific Configuration ---
1182
1183/// General Sled database configuration
1184#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1185#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1186#[serde(deny_unknown_fields)]
1187pub struct SledConfig {
1188    /// Path to the Sled database directory.
1189    pub path: String,
1190    /// The tree name to use as a queue. Defaults to "default".
1191    pub tree: Option<String>,
1192    /// (Consumer only) If true, start reading from the beginning of the tree.
1193    #[serde(default)]
1194    pub read_from_start: bool,
1195    /// (Consumer only) If true, delete messages after processing (Queue mode).
1196    #[serde(default)]
1197    pub delete_after_read: bool,
1198}
1199
1200impl SledConfig {
1201    /// Creates a new Sled configuration with the specified database path.
1202    pub fn new(path: impl Into<String>) -> Self {
1203        Self {
1204            path: path.into(),
1205            ..Default::default()
1206        }
1207    }
1208
1209    pub fn with_tree(mut self, tree: impl Into<String>) -> Self {
1210        self.tree = Some(tree.into());
1211        self
1212    }
1213
1214    pub fn with_read_from_start(mut self, read_from_start: bool) -> Self {
1215        self.read_from_start = read_from_start;
1216        self
1217    }
1218}
1219
1220/// Format for messages written to or read from a file.
1221#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
1222#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1223#[serde(rename_all = "snake_case")]
1224pub enum FileFormat {
1225    /// The full `CanonicalMessage` is serialized to JSON. Payload is a byte array.
1226    #[default]
1227    Normal,
1228    /// The full `CanonicalMessage` is serialized to JSON. Payload is rendered as a JSON value if possible.
1229    Json,
1230    /// The full `CanonicalMessage` is serialized to JSON. Payload is rendered as a string if possible.
1231    Text,
1232    /// The raw payload of the message is written. For consumers, the line is read as raw bytes.
1233    Raw,
1234}
1235
1236// --- File Specific Configuration ---
1237
1238#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1240pub struct FileConfig {
1241    /// Path to the file.
1242    pub path: String,
1243    /// Optional delimiter for messages. Defaults to newline ("\n").
1244    /// Can be a string or a hex sequence (e.g. "0x00").
1245    /// Currently only single-byte delimiters are supported.
1246    pub delimiter: Option<String>,
1247    /// The consumption mode. If not specified, defaults to `consume`.
1248    /// For publishers, this setting is ignored.
1249    #[serde(flatten, default)]
1250    pub mode: Option<FileConsumerMode>,
1251    /// The format for writing messages to the file (Publisher) or interpreting them (Consumer). Defaults to `normal`.
1252    #[serde(default)]
1253    pub format: FileFormat,
1254}
1255
1256#[derive(Debug, Clone, Deserialize, Serialize)]
1257#[serde(tag = "mode", rename_all = "snake_case")]
1258#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1259pub enum FileConsumerMode {
1260    /// **Queue Mode**: Standard point-to-point consumption. Reads from the start
1261    /// of the file. If `delete` is true, processed lines are physically removed
1262    /// from the file once they are successfully acknowledged.
1263    Consume {
1264        #[serde(default)]
1265        delete: bool,
1266    },
1267    /// **Broadcast Mode**: Pub-sub style consumption. Tails the file by starting
1268    /// at the current end. If `delete` is true, lines are removed only after
1269    /// all local application subscribers for this specific file have acknowledged them.
1270    Subscribe {
1271        #[serde(default)]
1272        delete: bool,
1273    },
1274    /// **Persistent Mode**: Consumption with external offset tracking.
1275    /// Saves the last read byte position to a `.offset` file identified by the `group_id`.
1276    /// This allows the consumer to resume exactly where it left off after a restart
1277    /// without deleting data or requiring the bridge to stay running.
1278    GroupSubscribe {
1279        /// The consumer group ID that is used for offset tracking. Should be unique.
1280        group_id: String,
1281        /// If true, starts reading from the end of the file if no offset is stored.
1282        /// If false, starts reading from the beginning.
1283        #[serde(default)]
1284        read_from_tail: bool,
1285    },
1286}
1287
1288impl Default for FileConsumerMode {
1289    fn default() -> Self {
1290        Self::Consume { delete: false }
1291    }
1292}
1293
1294impl FileConfig {
1295    /// Creates a new File configuration with the specified path.
1296    pub fn new(path: impl Into<String>) -> Self {
1297        Self {
1298            path: path.into(),
1299            mode: Some(FileConsumerMode::default()),
1300            delimiter: None,
1301            format: FileFormat::default(),
1302        }
1303    }
1304
1305    pub fn with_mode(mut self, mode: FileConsumerMode) -> Self {
1306        self.mode = Some(mode);
1307        self
1308    }
1309
1310    /// Returns the effective consumer mode, defaulting to `Consume` if not set.
1311    pub fn effective_mode(&self) -> FileConsumerMode {
1312        self.mode.clone().unwrap_or_default()
1313    }
1314}
1315
1316// --- NATS Specific Configuration ---
1317
1318/// General NATS connection configuration.
1319#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1320#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1321#[serde(deny_unknown_fields)]
1322pub struct NatsConfig {
1323    /// Comma-separated list of NATS server URLs (e.g., "nats://localhost:4222,nats://localhost:4223"). If it contains userinfo, it will be treated as a secret.
1324    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1325    pub url: String,
1326    /// The NATS subject to publish to or subscribe to.
1327    pub subject: Option<String>,
1328    /// (Consumer only). The JetStream stream name. Required for Consumers.
1329    pub stream: Option<String>,
1330    /// Optional username for authentication.
1331    pub username: Option<String>,
1332    /// Optional password for authentication.
1333    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1334    pub password: Option<String>,
1335    /// TLS configuration.
1336    #[serde(default)]
1337    pub tls: TlsConfig,
1338    /// Optional token for authentication.
1339    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1340    pub token: Option<String>,
1341    /// (Publisher only) If true, the publisher uses the request-reply pattern.
1342    /// It sends a request and waits for a response (using `core_client.request_with_headers()`).
1343    /// Defaults to false.
1344    #[serde(default)]
1345    pub request_reply: bool,
1346    /// (Publisher only) Timeout for request-reply operations in milliseconds. Defaults to 30000ms.
1347    pub request_timeout_ms: Option<u64>,
1348    /// (Publisher only) If true, do not wait for an acknowledgement when sending to broker. Defaults to false.
1349    #[serde(default)]
1350    pub delayed_ack: bool,
1351    /// If no_jetstream: true, use Core NATS (fire-and-forget) instead of JetStream. Defaults to false.
1352    #[serde(default)]
1353    pub no_jetstream: bool,
1354    /// (Consumer only) If true, use ephemeral **Subscriber mode**. Defaults to false (durable consumer).
1355    #[serde(default)]
1356    pub subscriber_mode: bool,
1357    /// (Publisher only) Maximum number of messages in the stream (if created by the bridge). Defaults to 1,000,000.
1358    pub stream_max_messages: Option<i64>,
1359    /// (Consumer only) The delivery policy for the consumer. Defaults to "all".
1360    pub deliver_policy: Option<NatsDeliverPolicy>,
1361    /// (Publisher only) Maximum total bytes in the stream (if created by the bridge). Defaults to 1GB.
1362    pub stream_max_bytes: Option<i64>,
1363    /// (Consumer only) Number of messages to prefetch from the consumer. Defaults to 10000.
1364    pub prefetch_count: Option<usize>,
1365}
1366
1367#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
1368#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1369#[serde(rename_all = "snake_case")]
1370pub enum NatsDeliverPolicy {
1371    #[default]
1372    All,
1373    Last,
1374    New,
1375    LastPerSubject,
1376}
1377
1378impl NatsConfig {
1379    /// Creates a new NATS configuration with the specified server URL.
1380    pub fn new(url: impl Into<String>) -> Self {
1381        Self {
1382            url: url.into(),
1383            ..Default::default()
1384        }
1385    }
1386
1387    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
1388        self.subject = Some(subject.into());
1389        self
1390    }
1391
1392    pub fn with_stream(mut self, stream: impl Into<String>) -> Self {
1393        self.stream = Some(stream.into());
1394        self
1395    }
1396
1397    pub fn with_deliver_policy(mut self, policy: NatsDeliverPolicy) -> Self {
1398        self.deliver_policy = Some(policy);
1399        self
1400    }
1401
1402    pub fn with_credentials(
1403        mut self,
1404        username: impl Into<String>,
1405        password: impl Into<String>,
1406    ) -> Self {
1407        self.username = Some(username.into());
1408        self.password = Some(password.into());
1409        self
1410    }
1411}
1412
1413#[derive(Debug, Serialize, Clone, Default)]
1414#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1415#[cfg_attr(feature = "schema", schemars(transform = memory_config_schema_transform))]
1416#[serde(deny_unknown_fields)]
1417pub struct MemoryConfig {
1418    /// The topic name or transport URL. Can be:
1419    /// - Simple name: "my-topic" (defaults to memory://my-topic)
1420    /// - Memory URL: "memory://my-topic"
1421    /// - IPC URL: "ipc://my-queue" or "ipc:///path/to/socket"
1422    /// - Unix socket: "unix:///path/to/socket" (Unix only)
1423    /// - Named pipe: "pipe://my-pipe" (Windows only)
1424    ///
1425    /// Either `topic` or `url` can be specified (they are serde aliases).
1426    #[serde(default, skip_serializing_if = "String::is_empty", alias = "url")]
1427    pub topic: String,
1428    /// Transport URL (serde alias for `topic`). Use either `topic` or `url`.
1429    #[serde(skip)]
1430    pub url: Option<String>,
1431    /// The capacity of the channel. Defaults to 100.
1432    pub capacity: Option<usize>,
1433    /// (Publisher only) If true, send() waits for a response.
1434    #[serde(default)]
1435    pub request_reply: bool,
1436    /// (Publisher only) Timeout for request-reply operations in milliseconds. Defaults to 30000ms.
1437    pub request_timeout_ms: Option<u64>,
1438    /// (Consumer only) If true, act as a **Subscriber** (fan-out). Defaults to false (queue).
1439    #[serde(default)]
1440    pub subscribe_mode: bool,
1441    /// (Consumer only) If true, enables NACK support (re-queuing), which requires cloning messages.
1442    /// Defaults to false for memory:// transports, automatically true for IPC transports (ipc://, unix://, pipe://).
1443    #[serde(default)]
1444    pub enable_nack: bool,
1445    #[serde(skip)]
1446    pub enable_nack_overridden: bool,
1447}
1448
1449impl MemoryConfig {
1450    pub fn new(topic: impl Into<String>, capacity: Option<usize>) -> Self {
1451        Self {
1452            topic: topic.into(),
1453            url: None,
1454            capacity,
1455            ..Default::default()
1456        }
1457    }
1458
1459    pub fn new_with_url(url: impl Into<String>, capacity: Option<usize>) -> Self {
1460        let url = url.into();
1461        Self {
1462            topic: url.clone(),
1463            url: Some(url),
1464            capacity,
1465            ..Default::default()
1466        }
1467    }
1468
1469    pub fn with_subscribe(self, subscribe_mode: bool) -> Self {
1470        Self {
1471            subscribe_mode,
1472            ..self
1473        }
1474    }
1475
1476    pub fn with_request_reply(mut self, request_reply: bool) -> Self {
1477        self.request_reply = request_reply;
1478        self
1479    }
1480
1481    /// Gets the effective transport identifier.
1482    /// If topic contains ://, it's treated as a URL, otherwise as memory://topic.
1483    pub fn get_transport_identifier(&self) -> anyhow::Result<String> {
1484        let identifier = if !self.topic.is_empty() {
1485            &self.topic
1486        } else if let Some(url) = self.url.as_ref().filter(|url| !url.is_empty()) {
1487            url
1488        } else {
1489            return Err(anyhow::anyhow!(
1490                "MemoryConfig: 'topic' (or 'url' alias) is required."
1491            ));
1492        };
1493
1494        // If topic doesn't contain ://, treat it as memory://topic for backward compatibility
1495        if identifier.contains("://") {
1496            Ok(identifier.clone())
1497        } else {
1498            Ok(format!("memory://{}", identifier))
1499        }
1500    }
1501
1502    /// Check if the transport URL scheme suggests IPC (inter-process communication).
1503    /// IPC transports should enable nack by default for reliability.
1504    pub fn is_ipc_transport(&self) -> bool {
1505        if let Ok(identifier) = self.get_transport_identifier() {
1506            identifier.starts_with("ipc://")
1507                || identifier.starts_with("unix://")
1508                || identifier.starts_with("pipe://")
1509        } else {
1510            false
1511        }
1512    }
1513
1514    /// Apply smart defaults based on the transport type.
1515    /// For IPC transports, enable_nack defaults to true for reliability.
1516    pub fn with_smart_defaults(mut self) -> Self {
1517        if !self.enable_nack_overridden && self.is_ipc_transport() {
1518            self.enable_nack = true;
1519        }
1520        self
1521    }
1522}
1523
1524impl<'de> Deserialize<'de> for MemoryConfig {
1525    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1526    where
1527        D: Deserializer<'de>,
1528    {
1529        #[derive(Deserialize, Default)]
1530        #[serde(deny_unknown_fields)]
1531        struct MemoryConfigSerde {
1532            #[serde(default)]
1533            topic: String,
1534            #[serde(default)]
1535            url: Option<String>,
1536            capacity: Option<usize>,
1537            #[serde(default)]
1538            request_reply: bool,
1539            request_timeout_ms: Option<u64>,
1540            #[serde(default)]
1541            subscribe_mode: bool,
1542            #[serde(default)]
1543            enable_nack: Option<bool>,
1544        }
1545
1546        let raw = MemoryConfigSerde::deserialize(deserializer)?;
1547        if raw.topic.is_empty() && raw.url.as_deref().map_or(true, str::is_empty) {
1548            return Err(serde::de::Error::custom(
1549                "MemoryConfig: 'topic' (or 'url' alias) is required.",
1550            ));
1551        }
1552        let topic = if raw.topic.is_empty() {
1553            raw.url.clone().unwrap_or_default()
1554        } else {
1555            raw.topic
1556        };
1557        Ok(Self {
1558            topic,
1559            url: raw.url,
1560            capacity: raw.capacity,
1561            request_reply: raw.request_reply,
1562            request_timeout_ms: raw.request_timeout_ms,
1563            subscribe_mode: raw.subscribe_mode,
1564            enable_nack: raw.enable_nack.unwrap_or(false),
1565            enable_nack_overridden: raw.enable_nack.is_some(),
1566        })
1567    }
1568}
1569
1570#[cfg(feature = "schema")]
1571fn memory_config_schema_transform(schema: &mut schemars::Schema) {
1572    let Some(schema_obj) = schema.as_object_mut() else {
1573        return;
1574    };
1575
1576    let Some(properties) = schema_obj
1577        .get_mut("properties")
1578        .and_then(serde_json::Value::as_object_mut)
1579    else {
1580        return;
1581    };
1582
1583    properties.insert(
1584        "url".to_string(),
1585        serde_json::json!({
1586            "description": "Alias for `topic`. Use either `topic` or `url`.",
1587            "type": "string",
1588            "minLength": 1
1589        }),
1590    );
1591
1592    // Mirror the runtime check (see `MemoryConfig::deserialize`): an empty
1593    // `topic`/`url` is rejected, so the schema must require a non-empty value.
1594    if let Some(topic) = properties
1595        .get_mut("topic")
1596        .and_then(serde_json::Value::as_object_mut)
1597    {
1598        topic.insert("minLength".to_string(), serde_json::json!(1));
1599    }
1600
1601    schema_obj.insert(
1602        "anyOf".to_string(),
1603        serde_json::json!([
1604            { "required": ["topic"] },
1605            { "required": ["url"] }
1606        ]),
1607    );
1608}
1609
1610#[cfg(feature = "schema")]
1611fn route_schema_transform(schema: &mut schemars::Schema) {
1612    let Some(properties) = schema
1613        .as_object_mut()
1614        .and_then(|schema_obj| schema_obj.get_mut("properties"))
1615        .and_then(serde_json::Value::as_object_mut)
1616    else {
1617        return;
1618    };
1619
1620    let Some(allow_fault_injection) = properties
1621        .get_mut("allow_fault_injection")
1622        .and_then(serde_json::Value::as_object_mut)
1623    else {
1624        return;
1625    };
1626
1627    allow_fault_injection.insert("default".to_string(), serde_json::Value::Bool(false));
1628}
1629
1630/// Configuration for the correlated in-process stream response buffer.
1631#[derive(Debug, Serialize, Deserialize, Clone, Default)]
1632#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1633#[serde(deny_unknown_fields)]
1634pub struct StreamBufferConfig {
1635    /// Shared buffer topic used by both the publisher and correlated consumers.
1636    pub topic: String,
1637    /// Consumer-only correlation id partition to read from.
1638    ///
1639    /// Leave this unset for the publisher endpoint configured in
1640    /// `HttpConfig::stream_response_to`. Set it on consumers so a reader only
1641    /// receives messages belonging to one request or response stream.
1642    #[serde(default, skip_serializing_if = "Option::is_none")]
1643    pub correlation_id: Option<String>,
1644    /// Capacity of each correlation partition. Defaults to 100.
1645    #[serde(default, skip_serializing_if = "Option::is_none")]
1646    pub capacity: Option<usize>,
1647}
1648
1649impl StreamBufferConfig {
1650    /// Creates a `stream_buffer` config for the given topic.
1651    ///
1652    /// Add `with_correlation_id` when constructing a consumer for one stream.
1653    /// Leave the correlation id unset when constructing the publisher buffer
1654    /// used by `HttpConfig::stream_response_to`.
1655    pub fn new(topic: impl Into<String>) -> Self {
1656        Self {
1657            topic: topic.into(),
1658            ..Default::default()
1659        }
1660    }
1661
1662    /// Selects the response stream partition that a consumer should read.
1663    pub fn with_correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
1664        self.correlation_id = Some(correlation_id.into());
1665        self
1666    }
1667
1668    /// Sets the per-correlation partition capacity.
1669    pub fn with_capacity(mut self, capacity: usize) -> Self {
1670        self.capacity = Some(capacity);
1671        self
1672    }
1673}
1674
1675// --- AMQP Specific Configuration ---
1676
1677/// General AMQP connection configuration.
1678#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1679#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1680#[serde(deny_unknown_fields)]
1681pub struct AmqpConfig {
1682    /// AMQP connection URI. The `lapin` client connects to a single host specified in the URI. If it contains userinfo, it will be treated as a secret.
1683    /// For high availability, provide the address of a load balancer or use DNS resolution
1684    /// that points to multiple brokers. Example: "amqp://localhost:5672/vhost".
1685    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1686    pub url: String,
1687    /// The AMQP queue name.
1688    pub queue: Option<String>,
1689    /// (Consumer only) If true, act as a **Subscriber** (fan-out). Defaults to false.
1690    #[serde(default)]
1691    pub subscribe_mode: bool,
1692    /// Optional username for authentication.
1693    pub username: Option<String>,
1694    /// Optional password for authentication.
1695    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1696    pub password: Option<String>,
1697    /// TLS configuration.
1698    #[serde(default)]
1699    pub tls: TlsConfig,
1700    /// The exchange to publish to or bind the queue to.
1701    pub exchange: Option<String>,
1702    /// (Consumer only) Number of messages to prefetch. Defaults to 100.
1703    pub prefetch_count: Option<u16>,
1704    /// If true, declare queues as non-durable (transient). Defaults to false. Affects both Consumer (queue durability) and Publisher (message persistence).
1705    #[serde(default)]
1706    pub no_persistence: bool,
1707    /// (Publisher only) If true, do not attempt to declare the queue. Assumes the queue already exists. Defaults to false.
1708    #[serde(default)]
1709    pub no_declare_queue: bool,
1710    /// (Publisher only) If true, do not wait for an acknowledgement when sending to broker. Defaults to false.
1711    #[serde(default)]
1712    pub delayed_ack: bool,
1713}
1714
1715impl AmqpConfig {
1716    /// Creates a new AMQP configuration with the specified connection URL.
1717    pub fn new(url: impl Into<String>) -> Self {
1718        Self {
1719            url: url.into(),
1720            ..Default::default()
1721        }
1722    }
1723
1724    pub fn with_queue(mut self, queue: impl Into<String>) -> Self {
1725        self.queue = Some(queue.into());
1726        self
1727    }
1728
1729    pub fn with_exchange(mut self, exchange: impl Into<String>) -> Self {
1730        self.exchange = Some(exchange.into());
1731        self
1732    }
1733
1734    pub fn with_credentials(
1735        mut self,
1736        username: impl Into<String>,
1737        password: impl Into<String>,
1738    ) -> Self {
1739        self.username = Some(username.into());
1740        self.password = Some(password.into());
1741        self
1742    }
1743}
1744
1745/// MongoDB message storage format.
1746///
1747/// Determines how messages are stored and retrieved from MongoDB collections.
1748#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
1749#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1750#[serde(rename_all = "lowercase")]
1751pub enum MongoDbFormat {
1752    #[default]
1753    Normal,
1754    Json,
1755    Text,
1756    Raw,
1757}
1758
1759// --- MongoDB Specific Configuration ---
1760
1761/// General MongoDB connection configuration.
1762#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1763#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1764#[serde(deny_unknown_fields)]
1765pub struct MongoDbConfig {
1766    /// MongoDB connection string URI. Can contain a comma-separated list of hosts for a replica set. If it contains userinfo, it will be treated as a secret.
1767    /// Credentials provided via the separate `username` and `password` fields take precedence over any credentials embedded in the URL.
1768    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1769    pub url: String,
1770    /// The MongoDB collection name.
1771    pub collection: Option<String>,
1772    /// Optional username. Takes precedence over any credentials embedded in the `url`.
1773    /// Use embedded URL credentials for simple one-off connections but prefer explicit username/password fields (or environment-sourced secrets) for clarity and secret management in production.
1774    pub username: Option<String>,
1775    /// Optional password. Takes precedence over any credentials embedded in the `url`.
1776    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1777    /// Use embedded URL credentials for simple one-off connections but prefer explicit username/password fields (or environment-sourced secrets) for clarity and secret management in production.
1778    pub password: Option<String>,
1779    /// TLS configuration.
1780    #[serde(default)]
1781    pub tls: TlsConfig,
1782    /// The database name.
1783    pub database: String,
1784    /// (Consumer only) Polling interval in milliseconds for the consumer (when not using Change Streams). Defaults to 100ms.
1785    pub polling_interval_ms: Option<u64>,
1786    /// (Publisher only) Polling interval in milliseconds for the publisher when waiting for a reply. Defaults to 50ms.
1787    pub reply_polling_ms: Option<u64>,
1788    /// (Publisher only) If true, the publisher will wait for a response in a dedicated collection. Defaults to false.
1789    #[serde(default)]
1790    pub request_reply: bool,
1791    /// (Consumer only) If true, use Change Streams (**Subscriber mode**). Defaults to false (polling/consumer mode).
1792    #[serde(default)]
1793    pub change_stream: bool,
1794    /// (Publisher only) Timeout for request-reply operations in milliseconds. Defaults to 30000ms.
1795    pub request_timeout_ms: Option<u64>,
1796    /// (Publisher only) TTL in seconds for documents created by the publisher. If set, a TTL index is created.
1797    pub ttl_seconds: Option<u64>,
1798    /// (Publisher only) If set, creates a capped collection with this size in bytes.
1799    pub capped_size_bytes: Option<i64>,
1800    /// Format for storing messages. Defaults to Normal.
1801    #[serde(default)]
1802    pub format: MongoDbFormat,
1803    /// The ID used for the cursor in sequenced mode. If not provided, consumption starts from the current sequence (ephemeral).
1804    pub cursor_id: Option<String>,
1805    /// (Consumer only) Optional custom MongoDB query to filter messages. Provided as a JSON string (e.g., '{"type": "notification"}').
1806    pub receive_query: Option<String>,
1807    /// (Optional) Collection to store sequence counters and cursor positions. Defaults to the message collection if not set.
1808    pub meta_collection: Option<String>,
1809}
1810
1811impl MongoDbConfig {
1812    /// Creates a new MongoDB configuration with the specified URL and database name.
1813    pub fn new(url: impl Into<String>, database: impl Into<String>) -> Self {
1814        Self {
1815            url: url.into(),
1816            database: database.into(),
1817            ..Default::default()
1818        }
1819    }
1820
1821    pub fn with_collection(mut self, collection: impl Into<String>) -> Self {
1822        self.collection = Some(collection.into());
1823        self
1824    }
1825
1826    pub fn with_credentials(
1827        mut self,
1828        username: impl Into<String>,
1829        password: impl Into<String>,
1830    ) -> Self {
1831        self.username = Some(username.into());
1832        self.password = Some(password.into());
1833        self
1834    }
1835
1836    pub fn with_change_stream(mut self, change_stream: bool) -> Self {
1837        self.change_stream = change_stream;
1838        self
1839    }
1840}
1841
1842// --- MQTT Specific Configuration ---
1843
1844/// General MQTT connection configuration.
1845#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1846#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1847#[serde(deny_unknown_fields)]
1848pub struct MqttConfig {
1849    /// MQTT broker URL (e.g., "tcp://localhost:1883"). Does not support multiple hosts. If it contains userinfo, it will be treated as a secret.
1850    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1851    pub url: String,
1852    /// The MQTT topic.
1853    pub topic: Option<String>,
1854    /// Optional username for authentication.
1855    pub username: Option<String>,
1856    /// Optional password for authentication.
1857    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1858    pub password: Option<String>,
1859    /// TLS configuration.
1860    #[serde(default)]
1861    pub tls: TlsConfig,
1862    /// Optional client ID. If not provided, one is generated or derived from route name.
1863    pub client_id: Option<String>,
1864    /// Capacity of the internal channel for incoming messages. Defaults to 100.
1865    pub queue_capacity: Option<usize>,
1866    /// Maximum number of inflight messages.
1867    pub max_inflight: Option<u16>,
1868    /// Quality of Service level (0, 1, or 2). Defaults to 1.
1869    pub qos: Option<u8>,
1870    /// (Consumer only) If true, start with a clean session. Defaults to false (persistent session). Setting this to true effectively enables **Subscriber mode** (ephemeral).
1871    #[serde(default = "default_clean_session")]
1872    pub clean_session: bool,
1873    /// Keep-alive interval in seconds. Defaults to 20.
1874    pub keep_alive_seconds: Option<u64>,
1875    /// MQTT protocol version (V3 or V5). Defaults to V5.
1876    #[serde(default)]
1877    pub protocol: MqttProtocol,
1878    /// Session expiry interval in seconds (MQTT v5 only).
1879    pub session_expiry_interval: Option<u32>,
1880    /// (Consumer only) If true, messages are acknowledged immediately upon receipt (auto-ack).
1881    /// If false (default), messages are acknowledged after processing (manual-ack).
1882    /// Note: For QoS 1/2 the publisher always waits for end-to-end broker
1883    /// confirmation (PUBACK/PUBCOMP) before reporting success, independent of
1884    /// this setting; QoS 0 remains fire-and-forget.
1885    #[serde(default)]
1886    pub delayed_ack: bool,
1887}
1888
1889impl MqttConfig {
1890    /// Creates a new MQTT configuration with the specified broker URL.
1891    pub fn new(url: impl Into<String>) -> Self {
1892        Self {
1893            url: url.into(),
1894            ..Default::default()
1895        }
1896    }
1897
1898    pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
1899        self.topic = Some(topic.into());
1900        self
1901    }
1902
1903    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
1904        self.client_id = Some(client_id.into());
1905        self
1906    }
1907
1908    pub fn with_credentials(
1909        mut self,
1910        username: impl Into<String>,
1911        password: impl Into<String>,
1912    ) -> Self {
1913        self.username = Some(username.into());
1914        self.password = Some(password.into());
1915        self
1916    }
1917}
1918
1919/// MQTT protocol version.
1920///
1921/// Specifies which version of the MQTT protocol to use for connections.
1922#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
1923#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1924#[serde(rename_all = "lowercase")]
1925pub enum MqttProtocol {
1926    #[default]
1927    V5,
1928    V3,
1929}
1930
1931// --- ZeroMQ Specific Configuration ---
1932
1933#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1934#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1935#[serde(deny_unknown_fields)]
1936pub struct ZeroMqConfig {
1937    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1938    /// The ZeroMQ URL (e.g., "tcp://127.0.0.1:5555").
1939    pub url: String,
1940    /// The socket type (PUSH, PULL, PUB, SUB, REQ, REP).
1941    #[serde(default)]
1942    pub socket_type: Option<ZeroMqSocketType>,
1943    /// (Consumer only) The ZeroMQ topic (for SUB sockets).
1944    pub topic: Option<String>,
1945    /// If true, bind to the address. If false, connect.
1946    #[serde(default)]
1947    pub bind: bool,
1948    /// Internal buffer size for the channel. Defaults to 128.
1949    #[serde(default)]
1950    pub internal_buffer_size: Option<usize>,
1951}
1952
1953impl ZeroMqConfig {
1954    /// Creates a new ZeroMQ configuration with the specified URL.
1955    pub fn new(url: impl Into<String>) -> Self {
1956        Self {
1957            url: url.into(),
1958            ..Default::default()
1959        }
1960    }
1961
1962    pub fn with_socket_type(mut self, socket_type: ZeroMqSocketType) -> Self {
1963        self.socket_type = Some(socket_type);
1964        self
1965    }
1966
1967    pub fn with_bind(mut self, bind: bool) -> Self {
1968        self.bind = bind;
1969        self
1970    }
1971}
1972
1973/// ZeroMQ socket type.
1974///
1975/// Defines the messaging pattern for ZeroMQ connections.
1976/// Different patterns support different communication paradigms (request-reply, publish-subscribe, etc.).
1977#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
1978#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1979#[serde(rename_all = "lowercase")]
1980pub enum ZeroMqSocketType {
1981    Push,
1982    Pull,
1983    Pub,
1984    Sub,
1985    Req,
1986    Rep,
1987}
1988
1989// --- gRPC Specific Configuration ---
1990
1991#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1992#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1993#[serde(deny_unknown_fields)]
1994pub struct GrpcConfig {
1995    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1996    /// The gRPC server URL (e.g., "http://localhost:50051" for client or "0.0.0.0:50051" for server mode).
1997    pub url: String,
1998    /// Topic / subject used for both subscribe and publish paths.
1999    pub topic: Option<String>,
2000    /// Timeout in milliseconds.
2001    /// - Client mode: used as the connection timeout and per-request deadline.
2002    /// - Server mode: applied as the per-request deadline on the embedded server.
2003    pub timeout_ms: Option<u64>,
2004    /// TLS configuration.
2005    #[serde(default)]
2006    pub tls: TlsConfig,
2007    /// If `true`, start an embedded tonic gRPC server that accepts incoming `Publish` /
2008    /// `PublishBatch` RPCs. If `false` (the default), connect to a remote server as a client.
2009    #[serde(default)]
2010    pub server_mode: bool,
2011    /// HTTP/2 stream-level initial window size in bytes. **Server-mode only.**
2012    #[serde(default)]
2013    pub initial_stream_window_size: Option<u32>,
2014    /// HTTP/2 connection-level initial window size in bytes. **Server-mode only.**
2015    #[serde(default)]
2016    pub initial_connection_window_size: Option<u32>,
2017    /// Maximum number of concurrent requests handled per connection. **Server-mode only.**
2018    #[serde(default)]
2019    pub concurrency_limit_per_connection: Option<usize>,
2020    /// HTTP/2 keepalive ping interval in milliseconds. **Server-mode only.** Default disabled
2021    #[serde(default)]
2022    pub http2_keepalive_interval_ms: Option<u64>,
2023    /// Timeout for a keepalive ping acknowledgement in milliseconds. **Server-mode only.**
2024    #[serde(default)]
2025    pub http2_keepalive_timeout_ms: Option<u64>,
2026    /// Maximum size of a decoded incoming message in bytes. **Server-mode only.** Default 4 MiB.
2027    #[serde(default)]
2028    pub max_decoding_message_size: Option<usize>,
2029}
2030
2031impl GrpcConfig {
2032    /// Creates a new gRPC configuration with the specified server URL.
2033    pub fn new(url: impl Into<String>) -> Self {
2034        Self {
2035            url: url.into(),
2036            ..Default::default()
2037        }
2038    }
2039
2040    pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
2041        self.topic = Some(topic.into());
2042        self
2043    }
2044
2045    /// Enable or disable server mode for this gRPC endpoint.
2046    pub fn with_server_mode(mut self, server_mode: bool) -> Self {
2047        self.server_mode = server_mode;
2048        self
2049    }
2050}
2051
2052// --- HTTP Specific Configuration ---
2053
2054/// Supported inbound HTTP protocols for server listeners.
2055#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, Default)]
2056#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2057#[serde(rename_all = "snake_case")]
2058pub enum HttpServerProtocol {
2059    /// Accept both HTTP/1.1 and HTTP/2, matching the current default behavior.
2060    #[default]
2061    Auto,
2062    /// Accept only HTTP/1.x connections.
2063    Http1Only,
2064    /// Accept only HTTP/2 connections.
2065    Http2Only,
2066}
2067
2068/// WebSocket route execution strategy.
2069#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
2070#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2071#[serde(rename_all = "snake_case")]
2072pub enum WebSocketExecutionMode {
2073    /// Use direct per-connection handling for simple `websocket -> response` routes and fall back
2074    /// to the routed adapter with a warning when route semantics need the normal pipeline.
2075    #[default]
2076    Auto,
2077    /// Require direct per-connection handling. Startup fails if the route cannot run directly.
2078    DirectOnly,
2079    /// Always use the normal routed consumer/worker/disposition pipeline.
2080    Routed,
2081}
2082
2083/// General HTTP connection configuration.
2084#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2085#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2086#[serde(deny_unknown_fields)]
2087pub struct HttpConfig {
2088    /// For consumers, the listen address (e.g., "0.0.0.0:8080"). For publishers, the target URL.
2089    pub url: String,
2090    /// (Consumer only) Optional request path filter. If set, only requests whose URI path matches exactly are delivered to this consumer.
2091    pub path: Option<String>,
2092    /// (Optional) HTTP method. For publishers: the method to use (defaults to POST). For consumers: restrict to this method (others return 405).
2093    pub method: Option<String>,
2094    /// TLS configuration.
2095    #[serde(default)]
2096    pub tls: TlsConfig,
2097    /// (Consumer only) Number of worker threads to use. Defaults to 0 for unlimited.
2098    pub workers: Option<usize>,
2099    /// (Consumer only) Header key to extract the message ID from. Defaults to "message-id".
2100    pub message_id_header: Option<String>,
2101    /// Timeout for HTTP requests in milliseconds. For consumers, it's the request-reply timeout. For publishers, it's the timeout for each individual request. Defaults to 30000ms.
2102    pub request_timeout_ms: Option<u64>,
2103    /// (Consumer only) Internal buffer size for the channel. Defaults to 100.
2104    pub internal_buffer_size: Option<usize>,
2105    /// (Consumer only) If true, respond immediately with 202 Accepted without waiting for downstream processing. Defaults to false.
2106    #[serde(default)]
2107    pub fire_and_forget: bool,
2108    /// (Consumer only) If true, read request bodies as a stream and emit each received stream item as a separate message.
2109    #[serde(default)]
2110    pub receive_streamable: bool,
2111    /// (Consumer only) If true, compatible `http -> response` routes may bypass the normal route consumer/worker/disposition pipeline
2112    /// and reply inline for lower latency. Defaults to true. Set to false to force the normal route path.
2113    #[serde(default, skip_serializing_if = "Option::is_none")]
2114    #[cfg_attr(
2115        feature = "schema",
2116        schemars(default = "default_inline_response_fast_path_schema")
2117    )]
2118    pub inline_response_fast_path: Option<bool>,
2119    /// (Consumer only) Restrict which HTTP protocol versions a server listener accepts.
2120    /// Defaults to `auto` (HTTP/1.1 + HTTP/2). On cleartext listeners, `http2_only`
2121    /// means HTTP/2 prior-knowledge (h2c) only.
2122    #[serde(default)]
2123    pub server_protocol: HttpServerProtocol,
2124    /// (Publisher only) Optional endpoint that receives streamed HTTP response items as correlated messages.
2125    ///
2126    /// Use a `stream_buffer` endpoint here when callers need to read streamed
2127    /// response items later through a normal mq-bridge consumer. Each streamed
2128    /// item is published with `correlation_id`, `http_stream_id`,
2129    /// `http_stream_index`, `http_stream_format`, and `http_stream_end`
2130    /// metadata. If the request message has no `correlation_id`, the HTTP
2131    /// publisher uses `format!("{:032x}", request.message_id)` so callers can
2132    /// derive the consumer correlation id before calling `send`.
2133    #[serde(default, skip_serializing_if = "Option::is_none")]
2134    pub stream_response_to: Option<Box<Endpoint>>,
2135    /// (Publisher only) The number of concurrent HTTP requests to send in a batch. Defaults to 20.
2136    #[serde(default, skip_serializing_if = "Option::is_none")]
2137    pub batch_concurrency: Option<usize>,
2138    /// (Publisher only) TCP keepalive timeout for the underlying connection pool in milliseconds. Defaults to 60000ms.
2139    #[serde(default, skip_serializing_if = "Option::is_none")]
2140    pub tcp_keepalive_ms: Option<u64>,
2141    /// (Publisher only) Timeout for idle connections in the connection pool in milliseconds. Defaults to 90000ms.
2142    #[serde(default, skip_serializing_if = "Option::is_none")]
2143    pub pool_idle_timeout_ms: Option<u64>,
2144    /// Enable gzip compression for request/response bodies exceeding the threshold. Defaults to false.
2145    #[serde(default)]
2146    pub compression_enabled: bool,
2147    /// Minimum message size in bytes to compress. Messages smaller than this are sent uncompressed. Defaults to 1024 bytes.
2148    #[serde(default)]
2149    pub compression_threshold_bytes: Option<usize>,
2150    /// HTTP Basic Authentication credentials (username, password). For consumers: validates incoming requests. For publishers: adds Authorization header.
2151    /// (Consumer only) Maximum number of concurrent requests to handle. Defaults to 100.
2152    pub concurrency_limit: Option<usize>,
2153    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2154    #[serde(
2155        default,
2156        skip_serializing_if = "Option::is_none",
2157        deserialize_with = "deserialize_basic_auth"
2158    )]
2159    pub basic_auth: Option<(String, String)>,
2160    /// Custom headers as key-value pairs (e.g., {"X-API-Key": "token123"}). Added to outgoing HTTP headers for both consumers and publishers.
2161    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2162    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
2163    pub custom_headers: HashMap<String, String>,
2164}
2165
2166/// WebSocket connection configuration.
2167#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2168#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2169#[serde(deny_unknown_fields)]
2170pub struct WebSocketConfig {
2171    /// For consumers, the listen address (e.g. "0.0.0.0:9000"). For publishers, the target URL.
2172    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2173    pub url: String,
2174    /// (Consumer only) Optional request path filter. If set, only upgrade requests whose URI path matches exactly are delivered to this consumer.
2175    pub path: Option<String>,
2176    /// (Consumer only) Header key to extract the message ID from the WebSocket handshake. Defaults to "message-id".
2177    pub message_id_header: Option<String>,
2178    /// (Consumer only) Queue capacity for the routed adapter. Direct response routes do not use this queue. Defaults to 100.
2179    pub routed_queue_capacity: Option<usize>,
2180    /// (Consumer only) TCP listen backlog (pending-connection queue depth) for the accept socket.
2181    /// Raise this if high-concurrency handshake bursts are being dropped/reset before `accept()`
2182    /// can keep up. Defaults to 4096, which is higher than the OS/tokio default of 1024.
2183    pub backlog: Option<u32>,
2184    /// (Consumer only) Selects whether WebSocket routes run directly or through the routed pipeline.
2185    #[serde(default)]
2186    pub execution_mode: WebSocketExecutionMode,
2187}
2188
2189fn deserialize_basic_auth<'de, D>(deserializer: D) -> Result<Option<(String, String)>, D::Error>
2190where
2191    D: Deserializer<'de>,
2192{
2193    let val = serde_json::Value::deserialize(deserializer)?;
2194    match val {
2195        serde_json::Value::Null => Ok(None),
2196        serde_json::Value::Array(arr) => {
2197            if arr.len() != 2 {
2198                return Err(serde::de::Error::custom("basic_auth must have 2 elements"));
2199            }
2200            let u = arr[0]
2201                .as_str()
2202                .ok_or_else(|| serde::de::Error::custom("basic_auth[0] must be string"))?
2203                .to_string();
2204            let p = arr[1]
2205                .as_str()
2206                .ok_or_else(|| serde::de::Error::custom("basic_auth[1] must be string"))?
2207                .to_string();
2208            Ok(Some((u, p)))
2209        }
2210        serde_json::Value::Object(map) => {
2211            let u = map
2212                .get("0")
2213                .and_then(|v| v.as_str())
2214                .ok_or_else(|| serde::de::Error::custom("basic_auth map missing '0'"))?
2215                .to_string();
2216            let p = map
2217                .get("1")
2218                .and_then(|v| v.as_str())
2219                .ok_or_else(|| serde::de::Error::custom("basic_auth map missing '1'"))?
2220                .to_string();
2221            Ok(Some((u, p)))
2222        }
2223        _ => Err(serde::de::Error::custom("invalid type for basic_auth")),
2224    }
2225}
2226
2227impl HttpConfig {
2228    /// Creates a new HTTP configuration with the specified URL.
2229    pub fn new(url: impl Into<String>) -> Self {
2230        Self {
2231            url: url.into(),
2232            ..Default::default()
2233        }
2234    }
2235
2236    pub fn with_workers(mut self, workers: usize) -> Self {
2237        self.workers = Some(workers);
2238        self
2239    }
2240
2241    pub fn with_method(mut self, method: impl Into<String>) -> Self {
2242        self.method = Some(method.into());
2243        self
2244    }
2245
2246    pub fn with_path(mut self, path: impl Into<String>) -> Self {
2247        self.path = Some(path.into());
2248        self
2249    }
2250
2251    pub fn with_receive_streamable(mut self, receive_streamable: bool) -> Self {
2252        self.receive_streamable = receive_streamable;
2253        self
2254    }
2255
2256    pub fn with_inline_response_fast_path(mut self, inline_response_fast_path: bool) -> Self {
2257        self.inline_response_fast_path = Some(inline_response_fast_path);
2258        self
2259    }
2260
2261    pub fn with_server_protocol(mut self, server_protocol: HttpServerProtocol) -> Self {
2262        self.server_protocol = server_protocol;
2263        self
2264    }
2265
2266    pub fn inline_response_fast_path_enabled(&self) -> bool {
2267        self.inline_response_fast_path.unwrap_or(true)
2268    }
2269
2270    pub fn with_stream_response_to(mut self, endpoint: Endpoint) -> Self {
2271        self.stream_response_to = Some(Box::new(endpoint));
2272        self
2273    }
2274}
2275
2276impl WebSocketConfig {
2277    /// Creates a new WebSocket configuration with the specified URL.
2278    pub fn new(url: impl Into<String>) -> Self {
2279        Self {
2280            url: url.into(),
2281            ..Default::default()
2282        }
2283    }
2284
2285    pub fn with_path(mut self, path: impl Into<String>) -> Self {
2286        self.path = Some(path.into());
2287        self
2288    }
2289
2290    pub fn with_backlog(mut self, backlog: u32) -> Self {
2291        self.backlog = Some(backlog);
2292        self
2293    }
2294
2295    pub fn with_execution_mode(mut self, execution_mode: WebSocketExecutionMode) -> Self {
2296        self.execution_mode = execution_mode;
2297        self
2298    }
2299}
2300
2301// --- IBM MQ Specific Configuration ---
2302
2303/// Connection settings for the IBM MQ Queue Manager.
2304#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2305#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2306#[serde(deny_unknown_fields)]
2307pub struct IbmMqConfig {
2308    /// Required. Connection URL in `host(port)` format. Supports comma-separated list for failover (e.g., `host1(1414),host2(1414)`). If it contains userinfo, it will be treated as a secret.
2309    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2310    pub url: String,
2311    /// Target Queue name for point-to-point messaging. Optional if `topic` is set; defaults to route name if omitted.
2312    pub queue: Option<String>,
2313    /// Target Topic string for Publish/Subscribe. If set, enables **Subscriber mode** (Consumer) or publishes to a topic (Publisher). Optional if `queue` is set.
2314    pub topic: Option<String>,
2315    /// Required. Name of the Queue Manager to connect to (e.g., `QM1`).
2316    pub queue_manager: String,
2317    /// Required. Server Connection (SVRCONN) Channel name defined on the QM.
2318    pub channel: String,
2319    /// Username for authentication. Optional; required if the channel enforces authentication
2320    pub username: Option<String>,
2321    /// Password for authentication. Optional; required if the channel enforces authentication.
2322    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2323    pub password: Option<String>,
2324    /// TLS CipherSpec (e.g., `ANY_TLS12`). Optional; required for encrypted connections.
2325    pub cipher_spec: Option<String>,
2326    /// TLS configuration settings (e.g., keystore paths). Optional.
2327    #[serde(default)]
2328    pub tls: TlsConfig,
2329    /// Maximum message size in bytes (default: 4MB). Optional.
2330    #[serde(default = "default_max_message_size")]
2331    pub max_message_size: usize,
2332    /// (Consumer only) Polling timeout in milliseconds (default: 1000ms). Optional.
2333    #[serde(default = "default_wait_timeout_ms")]
2334    pub wait_timeout_ms: i32,
2335    /// Internal buffer size for the channel. Defaults to 100.
2336    #[serde(default)]
2337    pub internal_buffer_size: Option<usize>,
2338    /// If false, attempt to open the queue with INQUIRE permissions to fetch queue depth for status checks. Defaults to false.
2339    #[serde(default)]
2340    pub disable_status_inq: bool,
2341}
2342
2343impl IbmMqConfig {
2344    /// Creates a new IBM MQ configuration with the specified connection URL, queue manager, and channel.
2345    pub fn new(
2346        url: impl Into<String>,
2347        queue_manager: impl Into<String>,
2348        channel: impl Into<String>,
2349    ) -> Self {
2350        Self {
2351            url: url.into(),
2352            queue_manager: queue_manager.into(),
2353            channel: channel.into(),
2354            disable_status_inq: false,
2355            ..Default::default()
2356        }
2357    }
2358
2359    pub fn with_queue(mut self, queue: impl Into<String>) -> Self {
2360        self.queue = Some(queue.into());
2361        self
2362    }
2363
2364    pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
2365        self.topic = Some(topic.into());
2366        self
2367    }
2368
2369    pub fn with_credentials(
2370        mut self,
2371        username: impl Into<String>,
2372        password: impl Into<String>,
2373    ) -> Self {
2374        self.username = Some(username.into());
2375        self.password = Some(password.into());
2376        self
2377    }
2378}
2379
2380fn default_max_message_size() -> usize {
2381    4 * 1024 * 1024 // 4MB default
2382}
2383
2384fn default_wait_timeout_ms() -> i32 {
2385    1000 // 1 second default
2386}
2387
2388// --- Switch/Router Configuration ---
2389
2390#[derive(Debug, Deserialize, Serialize, Clone)]
2391#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2392#[serde(deny_unknown_fields)]
2393pub struct SwitchConfig {
2394    /// The metadata key to inspect for routing decisions.
2395    pub metadata_key: String,
2396    /// A map of values to endpoints.
2397    pub cases: HashMap<String, Endpoint>,
2398    /// The default endpoint if no case matches.
2399    pub default: Option<Box<Endpoint>>,
2400}
2401
2402// --- Response Endpoint Configuration ---
2403#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2404#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2405#[serde(deny_unknown_fields)]
2406pub struct ResponseConfig {
2407    // This struct is a marker and currently has no fields.
2408}
2409
2410// --- SQLx Specific Configuration ---
2411
2412/// General SQLx connection configuration.
2413#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2414#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2415#[serde(deny_unknown_fields)]
2416pub struct SqlxConfig {
2417    /// Database connection URL. If it contains userinfo, it will be treated as a secret.
2418    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2419    pub url: String,
2420    /// Optional username. Takes precedence over any credentials embedded in the `url`.
2421    #[serde(default)]
2422    pub username: Option<String>,
2423    /// Optional password. Takes precedence over any credentials embedded in the `url`.
2424    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2425    #[serde(default)]
2426    pub password: Option<String>,
2427    /// The table to interact with.
2428    pub table: String,
2429    /// (Publisher only) Optional. A custom SQL INSERT query. Use `?` as a placeholder for the payload.
2430    /// If not provided, a default `INSERT INTO {table} (payload) VALUES (?)` is used.
2431    pub insert_query: Option<String>,
2432    /// (Consumer only) Optional. A custom SQL SELECT query to fetch messages. This is only supported for PostgreSQL and Microsoft SQL Server.
2433    /// The query must include a placeholder for the batch size (`$1` for PostgreSQL, `@p1` for SQL Server).
2434    /// The bridge will bind the route's `batch_size` to this placeholder.
2435    pub select_query: Option<String>,
2436    /// (Consumer only) If true, delete messages after processing.
2437    #[serde(default)]
2438    pub delete_after_read: bool,
2439    /// (Publisher only) If true, automatically create the table and indexes if they don't exist. Defaults to false.
2440    #[serde(default)]
2441    pub auto_create_table: bool,
2442    /// (Consumer only) Polling interval in milliseconds. Defaults to 100ms.
2443    pub polling_interval_ms: Option<u64>,
2444    /// TLS configuration for the database connection.
2445    #[serde(default)]
2446    pub tls: TlsConfig,
2447    /// Maximum number of connections in the pool. Defaults to 10.
2448    pub max_connections: Option<u32>,
2449    /// Minimum number of connections to keep in the pool. Defaults to 0.
2450    pub min_connections: Option<u32>,
2451    /// Timeout for acquiring a connection from the pool in milliseconds. Defaults to 30000ms.
2452    pub acquire_timeout_ms: Option<u64>,
2453    /// Maximum idle time for a connection in milliseconds. Defaults to 600000ms (10 minutes).
2454    pub idle_timeout_ms: Option<u64>,
2455    /// Maximum lifetime of a connection in milliseconds. Defaults to 1800000ms (30 minutes).
2456    pub max_lifetime_ms: Option<u64>,
2457}
2458
2459// --- Common Configuration ---
2460
2461/// TLS configuration for secure connections.
2462///
2463/// Configures Transport Layer Security (TLS/SSL) for encrypted communication.
2464/// Supports both client certificate (mutual TLS) and server certificate validation.
2465///
2466/// # Examples
2467///
2468/// ```
2469/// use mq_bridge::models::TlsConfig;
2470///
2471/// let tls = TlsConfig {
2472///     required: true,
2473///     ca_file: Some("/path/to/ca.pem".to_string()),
2474///     cert_file: Some("/path/to/cert.pem".to_string()),
2475///     key_file: Some("/path/to/key.pem".to_string()),
2476///     ..Default::default()
2477/// };
2478/// ```
2479#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
2480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2481#[serde(deny_unknown_fields)]
2482pub struct TlsConfig {
2483    /// If true, enable TLS/SSL.
2484    #[serde(default, deserialize_with = "deserialize_null_as_false")]
2485    pub required: bool,
2486    /// Path to the CA certificate file.
2487    pub ca_file: Option<String>,
2488    /// Path to the client certificate file (PEM).
2489    pub cert_file: Option<String>,
2490    /// Path to the client private key file (PEM).
2491    pub key_file: Option<String>,
2492    /// Password for the private key (if encrypted).
2493    #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2494    pub cert_password: Option<String>,
2495    /// If true, disable server certificate verification (insecure).
2496    #[serde(default)]
2497    pub accept_invalid_certs: bool,
2498}
2499
2500impl TlsConfig {
2501    /// Creates a new TLS configuration with default settings (TLS not required).
2502    pub fn new() -> Self {
2503        Self::default()
2504    }
2505
2506    pub fn with_ca_file(mut self, ca_file: impl Into<String>) -> Self {
2507        self.ca_file = Some(ca_file.into());
2508        self.required = true;
2509        self
2510    }
2511
2512    pub fn with_client_cert(
2513        mut self,
2514        cert_file: impl Into<String>,
2515        key_file: impl Into<String>,
2516    ) -> Self {
2517        self.cert_file = Some(cert_file.into());
2518        self.key_file = Some(key_file.into());
2519        self.required = true;
2520        self
2521    }
2522
2523    pub fn with_insecure(mut self, accept_invalid_certs: bool) -> Self {
2524        self.accept_invalid_certs = accept_invalid_certs;
2525        self
2526    }
2527
2528    /// Checks if mutual TLS (mTLS) client authentication is configured.
2529    pub fn is_mtls_client_configured(&self) -> bool {
2530        self.required && self.cert_file.is_some() && self.key_file.is_some()
2531    }
2532
2533    /// Checks if TLS server certificate authentication is configured.
2534    pub fn is_tls_server_configured(&self) -> bool {
2535        self.required && self.cert_file.is_some() && self.key_file.is_some()
2536    }
2537
2538    /// Checks if the TLS configuration is sufficient to make a TLS client connection.
2539    pub fn is_tls_client_configured(&self) -> bool {
2540        self.required
2541            || self.ca_file.is_some()
2542            || (self.cert_file.is_some() && self.key_file.is_some())
2543    }
2544
2545    /// Helper to normalize a URL by adding the appropriate scheme prefix (http:// or https://) if missing.
2546    pub fn normalize_url(&self, url: &str) -> String {
2547        if url
2548            .get(..7)
2549            .is_some_and(|prefix| prefix.eq_ignore_ascii_case("http://"))
2550            || url
2551                .get(..8)
2552                .is_some_and(|prefix| prefix.eq_ignore_ascii_case("https://"))
2553        {
2554            url.to_string()
2555        } else {
2556            let is_tls = self.required;
2557            let scheme = if is_tls { "https" } else { "http" };
2558            format!("{}://{}", scheme, url)
2559        }
2560    }
2561}
2562
2563/// Trait for extracting secrets from configuration structures.
2564pub trait SecretExtractor {
2565    /// Extracts secrets into the provided map using the given prefix, and clears them from self.
2566    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>);
2567}
2568
2569fn extract_sensitive_string_map_entries(
2570    values: &mut HashMap<String, String>,
2571    prefix: &str,
2572    field_name: &str,
2573    secrets: &mut HashMap<String, String>,
2574) {
2575    let secret_keys = values
2576        .keys()
2577        .filter(|key| {
2578            let key = key.to_ascii_lowercase();
2579            key.contains("key") || key.contains("token") || key.contains("auth")
2580        })
2581        .cloned()
2582        .collect::<Vec<_>>();
2583
2584    for key in secret_keys {
2585        if let Some(value) = values.remove(&key) {
2586            secrets.insert(
2587                sanitize_secret_key(&format!("{}__{}__{}", prefix, field_name, key)),
2588                value,
2589            );
2590        }
2591    }
2592}
2593
2594fn url_has_userinfo(url: &str) -> bool {
2595    let Some(authority_start) = url.find("://").map(|idx| idx + 3) else {
2596        return false;
2597    };
2598    let authority_end = url[authority_start..]
2599        .find(['/', '?', '#'])
2600        .map(|idx| authority_start + idx)
2601        .unwrap_or(url.len());
2602    url[authority_start..authority_end].contains('@')
2603}
2604
2605fn sanitize_secret_key(key: &str) -> String {
2606    key.chars()
2607        .map(|ch| {
2608            let ch = ch.to_ascii_uppercase();
2609            if ch.is_ascii_alphanumeric() || ch == '_' {
2610                ch
2611            } else {
2612                '_'
2613            }
2614        })
2615        .collect()
2616}
2617
2618fn extract_sensitive_url(
2619    url: &mut String,
2620    prefix: &str,
2621    field_name: &str,
2622    secrets: &mut HashMap<String, String>,
2623) {
2624    if !url.is_empty() && url_has_userinfo(url) {
2625        secrets.insert(
2626            sanitize_secret_key(&format!("{}__{}", prefix, field_name)),
2627            std::mem::take(url),
2628        );
2629    }
2630}
2631
2632fn extract_sensitive_optional_url(
2633    url: &mut Option<String>,
2634    prefix: &str,
2635    field_name: &str,
2636    secrets: &mut HashMap<String, String>,
2637) {
2638    if url.as_ref().is_some_and(|url| url_has_userinfo(url)) {
2639        if let Some(url) = url.take() {
2640            secrets.insert(
2641                sanitize_secret_key(&format!("{}__{}", prefix, field_name)),
2642                url,
2643            );
2644        }
2645    }
2646}
2647
2648impl SecretExtractor for Route {
2649    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2650        self.input
2651            .extract_secrets(&format!("{}__{}", prefix, "INPUT"), secrets);
2652        self.output
2653            .extract_secrets(&format!("{}__{}", prefix, "OUTPUT"), secrets);
2654    }
2655}
2656
2657impl SecretExtractor for Endpoint {
2658    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2659        for (i, middleware) in self.middlewares.iter_mut().enumerate() {
2660            middleware.extract_secrets(&format!("{}__{}__{}", prefix, "MIDDLEWARES", i), secrets);
2661        }
2662        self.endpoint_type.extract_secrets(prefix, secrets);
2663    }
2664}
2665
2666impl SecretExtractor for EndpointType {
2667    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2668        match self {
2669            EndpointType::Aws(cfg) => {
2670                cfg.extract_secrets(&format!("{}__{}", prefix, "AWS"), secrets)
2671            }
2672            EndpointType::Kafka(cfg) => {
2673                cfg.extract_secrets(&format!("{}__{}", prefix, "KAFKA"), secrets)
2674            }
2675            EndpointType::Nats(cfg) => {
2676                cfg.extract_secrets(&format!("{}__{}", prefix, "NATS"), secrets)
2677            }
2678            EndpointType::Amqp(cfg) => {
2679                cfg.extract_secrets(&format!("{}__{}", prefix, "AMQP"), secrets)
2680            }
2681            EndpointType::MongoDb(cfg) => {
2682                cfg.extract_secrets(&format!("{}__{}", prefix, "MONGODB"), secrets)
2683            }
2684            EndpointType::Mqtt(cfg) => {
2685                cfg.extract_secrets(&format!("{}__{}", prefix, "MQTT"), secrets)
2686            }
2687            EndpointType::Http(cfg) => {
2688                cfg.extract_secrets(&format!("{}__{}", prefix, "HTTP"), secrets)
2689            }
2690            EndpointType::WebSocket(cfg) => {
2691                cfg.extract_secrets(&format!("{}__{}", prefix, "WEBSOCKET"), secrets)
2692            }
2693            EndpointType::IbmMq(cfg) => {
2694                cfg.extract_secrets(&format!("{}__{}", prefix, "IBMMQ"), secrets)
2695            }
2696            EndpointType::ZeroMq(cfg) => {
2697                cfg.extract_secrets(&format!("{}__{}", prefix, "ZEROMQ"), secrets)
2698            }
2699            EndpointType::Sqlx(cfg) => {
2700                cfg.extract_secrets(&format!("{}__{}", prefix, "SQLX"), secrets)
2701            }
2702            EndpointType::Grpc(cfg) => {
2703                cfg.extract_secrets(&format!("{}__{}", prefix, "GRPC"), secrets)
2704            }
2705            EndpointType::Fanout(endpoints) => {
2706                for (i, ep) in endpoints.iter_mut().enumerate() {
2707                    ep.extract_secrets(&format!("{}__{}__{}", prefix, "FANOUT", i), secrets);
2708                }
2709            }
2710            EndpointType::Switch(cfg) => {
2711                for (key, ep) in cfg.cases.iter_mut() {
2712                    ep.extract_secrets(
2713                        &format!(
2714                            "{}__{}__{}",
2715                            prefix,
2716                            "SWITCH__CASES",
2717                            sanitize_secret_key(key)
2718                        ),
2719                        secrets,
2720                    );
2721                }
2722                if let Some(default) = &mut cfg.default {
2723                    default.extract_secrets(&format!("{}__{}", prefix, "SWITCH__DEFAULT"), secrets);
2724                }
2725            }
2726            EndpointType::Reader(ep) => {
2727                ep.extract_secrets(&format!("{}__{}", prefix, "READER"), secrets)
2728            }
2729            _ => {}
2730        }
2731    }
2732}
2733
2734impl SecretExtractor for Middleware {
2735    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2736        if let Middleware::Dlq(cfg) = self {
2737            cfg.endpoint
2738                .extract_secrets(&format!("{}__{}__{}", prefix, "DLQ", "ENDPOINT"), secrets);
2739        }
2740    }
2741}
2742
2743impl SecretExtractor for AwsConfig {
2744    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2745        if let Some(val) = self.access_key.take() {
2746            secrets.insert(format!("{}__{}", prefix, "ACCESS_KEY"), val);
2747        }
2748        if let Some(val) = self.secret_key.take() {
2749            secrets.insert(format!("{}__{}", prefix, "SECRET_KEY"), val);
2750        }
2751        if let Some(val) = self.session_token.take() {
2752            secrets.insert(format!("{}__{}", prefix, "SESSION_TOKEN"), val);
2753        }
2754        extract_sensitive_optional_url(&mut self.queue_url, prefix, "QUEUE_URL", secrets);
2755        extract_sensitive_optional_url(&mut self.endpoint_url, prefix, "ENDPOINT_URL", secrets);
2756    }
2757}
2758
2759impl SecretExtractor for KafkaConfig {
2760    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2761        extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2762        if let Some(val) = self.username.take() {
2763            secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2764        }
2765        if let Some(val) = self.password.take() {
2766            secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2767        }
2768        self.tls
2769            .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2770    }
2771}
2772
2773impl SecretExtractor for NatsConfig {
2774    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2775        extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2776        if let Some(val) = self.username.take() {
2777            secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2778        }
2779        if let Some(val) = self.password.take() {
2780            secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2781        }
2782        if let Some(val) = self.token.take() {
2783            secrets.insert(format!("{}__{}", prefix, "TOKEN"), val);
2784        }
2785        self.tls
2786            .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2787    }
2788}
2789
2790impl SecretExtractor for AmqpConfig {
2791    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2792        extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2793        if let Some(val) = self.username.take() {
2794            secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2795        }
2796        if let Some(val) = self.password.take() {
2797            secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2798        }
2799        self.tls
2800            .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2801    }
2802}
2803
2804impl SecretExtractor for MongoDbConfig {
2805    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2806        extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2807        if let Some(val) = self.username.take() {
2808            secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2809        }
2810        if let Some(val) = self.password.take() {
2811            secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2812        }
2813        self.tls
2814            .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2815    }
2816}
2817
2818impl SecretExtractor for MqttConfig {
2819    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2820        extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2821        if let Some(val) = self.username.take() {
2822            secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2823        }
2824        if let Some(val) = self.password.take() {
2825            secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2826        }
2827        self.tls
2828            .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2829    }
2830}
2831
2832impl SecretExtractor for HttpConfig {
2833    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2834        extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2835        if let Some((u, p)) = self.basic_auth.take() {
2836            secrets.insert(format!("{}__{}__{}", prefix, "BASIC_AUTH", 0), u);
2837            secrets.insert(format!("{}__{}__{}", prefix, "BASIC_AUTH", 1), p);
2838        }
2839        extract_sensitive_string_map_entries(
2840            &mut self.custom_headers,
2841            prefix,
2842            "CUSTOM_HEADERS",
2843            secrets,
2844        );
2845        self.tls
2846            .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2847        if let Some(endpoint) = &mut self.stream_response_to {
2848            endpoint.extract_secrets(&format!("{}__{}", prefix, "STREAM_RESPONSE_TO"), secrets);
2849        }
2850    }
2851}
2852
2853impl SecretExtractor for WebSocketConfig {
2854    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2855        extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2856    }
2857}
2858
2859impl SecretExtractor for IbmMqConfig {
2860    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2861        extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2862        if let Some(val) = self.username.take() {
2863            secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2864        }
2865        if let Some(val) = self.password.take() {
2866            secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2867        }
2868        self.tls
2869            .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2870    }
2871}
2872
2873impl SecretExtractor for ZeroMqConfig {
2874    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2875        extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2876    }
2877}
2878
2879impl SecretExtractor for SqlxConfig {
2880    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2881        extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2882        if let Some(val) = self.username.take() {
2883            secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2884        }
2885        if let Some(val) = self.password.take() {
2886            secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2887        }
2888        self.tls
2889            .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2890    }
2891}
2892
2893impl SecretExtractor for GrpcConfig {
2894    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2895        extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2896        self.tls
2897            .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2898    }
2899}
2900
2901impl SecretExtractor for TlsConfig {
2902    fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2903        if let Some(val) = self.cert_password.take() {
2904            secrets.insert(format!("{}__{}", prefix, "CERT_PASSWORD"), val);
2905        }
2906    }
2907}
2908
2909/// Extracts sensitive values (passwords, keys, tokens) from the configuration
2910/// and returns them as a map of environment variables (key-value pairs).
2911/// The extracted fields in the configuration are set to `None`.
2912///
2913/// The keys in the returned map follow the `MQB__{ROUTE}__{ENDPOINT}__{FIELD}` pattern
2914/// compatible with the `config` crate's environment variable override mechanism.
2915pub fn extract_config_secrets(config: &mut Config) -> HashMap<String, String> {
2916    let mut secrets = HashMap::new();
2917    for (route_name, route) in config.iter_mut() {
2918        let prefix = sanitize_secret_key(&format!("MQB__{}", route_name));
2919        route.extract_secrets(&prefix, &mut secrets);
2920    }
2921    secrets
2922}
2923
2924#[cfg(test)]
2925mod tests {
2926    use super::*;
2927    use config::{Config as ConfigBuilder, Environment};
2928
2929    const TEST_YAML: &str = r#"
2930kafka_to_nats:
2931  concurrency: 10
2932  input:
2933    middlewares:
2934      - deduplication:
2935          sled_path: "/tmp/mq-bridge/dedup_db"
2936          ttl_seconds: 3600
2937      - metrics: {}
2938      - retry:
2939          max_attempts: 5
2940          initial_interval_ms: 200
2941      - random_panic:
2942          mode: nack
2943      - dlq:
2944          endpoint:
2945            nats:
2946              subject: "dlq-subject"
2947              url: "nats://localhost:4222"
2948    kafka:
2949      topic: "input-topic"
2950      url: "localhost:9092"
2951      group_id: "my-consumer-group"
2952      tls:
2953        required: true
2954        ca_file: "/path_to_ca"
2955        cert_file: "/path_to_cert"
2956        key_file: "/path_to_key"
2957        cert_password: "password"
2958        accept_invalid_certs: true
2959  output:
2960    middlewares:
2961      - metrics: {}
2962      - dlq:
2963          endpoint:
2964            file:
2965              path: "error.out"
2966    nats:
2967      subject: "output-subject"
2968      url: "nats://localhost:4222"
2969"#;
2970
2971    fn assert_config_values(config: &Config) {
2972        assert_eq!(config.len(), 1);
2973        let route = config.get("kafka_to_nats").expect("Route should exist");
2974
2975        assert_eq!(route.options.concurrency, 10);
2976
2977        // --- Assert Input ---
2978        let input = &route.input;
2979        assert_eq!(input.middlewares.len(), 5);
2980
2981        let mut has_dedup = false;
2982        let mut has_metrics = false;
2983        let mut has_dlq = false;
2984        let mut has_retry = false;
2985        let mut has_random_panic = false;
2986        for middleware in &input.middlewares {
2987            match middleware {
2988                Middleware::Deduplication(dedup) => {
2989                    assert_eq!(dedup.sled_path, "/tmp/mq-bridge/dedup_db");
2990                    assert_eq!(dedup.ttl_seconds, 3600);
2991                    has_dedup = true;
2992                }
2993                Middleware::Metrics(_) => {
2994                    has_metrics = true;
2995                }
2996                Middleware::Custom { .. } => {}
2997                Middleware::Dlq(dlq) => {
2998                    assert!(dlq.endpoint.middlewares.is_empty());
2999                    if let EndpointType::Nats(nats_cfg) = &dlq.endpoint.endpoint_type {
3000                        assert_eq!(nats_cfg.subject, Some("dlq-subject".to_string()));
3001                        assert_eq!(nats_cfg.url, "nats://localhost:4222");
3002                    }
3003                    has_dlq = true;
3004                }
3005                Middleware::Retry(retry) => {
3006                    assert_eq!(retry.max_attempts, 5);
3007                    assert_eq!(retry.initial_interval_ms, 200);
3008                    has_retry = true;
3009                }
3010                Middleware::RandomPanic(rp) => {
3011                    assert!(rp.mode == FaultMode::Nack);
3012                    has_random_panic = true;
3013                }
3014                Middleware::Delay(_) => {}
3015                Middleware::WeakJoin(_) => {}
3016                Middleware::Limiter(_) => {}
3017                Middleware::Buffer(_) => {}
3018                Middleware::CookieJar(_) => {}
3019            }
3020        }
3021
3022        if let EndpointType::Kafka(kafka) = &input.endpoint_type {
3023            assert_eq!(kafka.topic, Some("input-topic".to_string()));
3024            assert_eq!(kafka.url, "localhost:9092");
3025            assert_eq!(kafka.group_id, Some("my-consumer-group".to_string()));
3026            let tls = &kafka.tls;
3027            assert!(tls.required);
3028            assert_eq!(tls.ca_file.as_deref(), Some("/path_to_ca"));
3029            assert!(tls.accept_invalid_certs);
3030        } else {
3031            panic!("Input endpoint should be Kafka");
3032        }
3033        assert!(has_dedup);
3034        assert!(has_metrics);
3035        assert!(has_dlq);
3036        assert!(has_retry);
3037        assert!(has_random_panic);
3038
3039        // --- Assert Output ---
3040        let output = &route.output;
3041        assert_eq!(output.middlewares.len(), 2);
3042        assert!(matches!(output.middlewares[0], Middleware::Metrics(_)));
3043
3044        if let EndpointType::Nats(nats) = &output.endpoint_type {
3045            assert_eq!(nats.subject, Some("output-subject".to_string()));
3046            assert_eq!(nats.url, "nats://localhost:4222");
3047        } else {
3048            panic!("Output endpoint should be NATS");
3049        }
3050    }
3051
3052    #[test]
3053    fn test_deserialize_from_yaml() {
3054        // We use serde_yaml directly here because the `config` crate's processing
3055        // can interfere with complex deserialization logic.
3056        let result: Result<Config, _> = serde_yaml_ng::from_str(TEST_YAML);
3057        println!("Deserialized from YAML: {:#?}", result);
3058        let config = result.expect("Failed to deserialize TEST_YAML");
3059        assert_config_values(&config);
3060    }
3061
3062    #[test]
3063    fn test_deserialize_from_env() {
3064        // Set environment variables based on README
3065        unsafe {
3066            std::env::set_var("MQB__KAFKA_TO_NATS__CONCURRENCY", "10");
3067            std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__TOPIC", "input-topic");
3068            std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__URL", "localhost:9092");
3069            std::env::set_var(
3070                "MQB__KAFKA_TO_NATS__INPUT__KAFKA__GROUP_ID",
3071                "my-consumer-group",
3072            );
3073            std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__REQUIRED", "true");
3074            std::env::set_var(
3075                "MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__CA_FILE",
3076                "/path_to_ca",
3077            );
3078            std::env::set_var(
3079                "MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__ACCEPT_INVALID_CERTS",
3080                "true",
3081            );
3082            std::env::set_var(
3083                "MQB__KAFKA_TO_NATS__OUTPUT__NATS__SUBJECT",
3084                "output-subject",
3085            );
3086            std::env::set_var(
3087                "MQB__KAFKA_TO_NATS__OUTPUT__NATS__URL",
3088                "nats://localhost:4222",
3089            );
3090            std::env::set_var(
3091                "MQB__KAFKA_TO_NATS__INPUT__MIDDLEWARES__0__DLQ__ENDPOINT__NATS__SUBJECT",
3092                "dlq-subject",
3093            );
3094            std::env::set_var(
3095                "MQB__KAFKA_TO_NATS__INPUT__MIDDLEWARES__0__DLQ__ENDPOINT__NATS__URL",
3096                "nats://localhost:4222",
3097            );
3098        }
3099
3100        let builder = ConfigBuilder::builder()
3101            // Enable automatic type parsing for values from environment variables.
3102            .add_source(
3103                Environment::with_prefix("MQB")
3104                    .separator("__")
3105                    .try_parsing(true),
3106            );
3107
3108        let config: Config = builder
3109            .build()
3110            .expect("Failed to build config")
3111            .try_deserialize()
3112            .expect("Failed to deserialize config");
3113
3114        // We can't test all values from env, but we can check the ones we set.
3115        assert_eq!(config.get("kafka_to_nats").unwrap().options.concurrency, 10);
3116        if let EndpointType::Kafka(k) = &config.get("kafka_to_nats").unwrap().input.endpoint_type {
3117            assert_eq!(k.topic, Some("input-topic".to_string()));
3118            assert!(k.tls.required);
3119        } else {
3120            panic!("Expected Kafka endpoint");
3121        }
3122
3123        let input = &config.get("kafka_to_nats").unwrap().input;
3124        assert_eq!(input.middlewares.len(), 1);
3125        if let Middleware::Dlq(_) = &input.middlewares[0] {
3126            // Correctly parsed
3127        } else {
3128            panic!("Expected DLQ middleware");
3129        }
3130    }
3131
3132    #[test]
3133    fn test_extract_secrets() {
3134        let mut config = Config::new();
3135        let mut route = Route::default();
3136
3137        // Setup Kafka with secrets
3138        let mut kafka_config = KafkaConfig::new("kafka://user:pass@localhost:9092");
3139        kafka_config.username = Some("user".to_string());
3140        kafka_config.password = Some("pass".to_string());
3141        kafka_config.tls.cert_password = Some("certpass".to_string());
3142
3143        route.input = Endpoint {
3144            endpoint_type: EndpointType::Kafka(kafka_config),
3145            middlewares: vec![],
3146            handler: None,
3147        };
3148
3149        // Setup HTTP with basic auth
3150        let mut http_config = HttpConfig::new("http://httpuser:httppass@localhost");
3151        http_config.basic_auth = Some(("httpuser".to_string(), "httppass".to_string()));
3152        http_config
3153            .custom_headers
3154            .insert("X-API-Key".to_string(), "http-api-key".to_string());
3155        http_config.custom_headers.insert(
3156            "X-Access-Token".to_string(),
3157            "http-access-token".to_string(),
3158        );
3159        http_config.custom_headers.insert(
3160            "X-Authentication".to_string(),
3161            "http-authentication".to_string(),
3162        );
3163        http_config.custom_headers.insert(
3164            "Authorization".to_string(),
3165            "Bearer secret-token".to_string(),
3166        );
3167        http_config
3168            .custom_headers
3169            .insert("X-Trace-Id".to_string(), "trace-value".to_string());
3170
3171        route.output = Endpoint {
3172            endpoint_type: EndpointType::Http(http_config),
3173            middlewares: vec![],
3174            handler: None,
3175        };
3176
3177        config.insert("test_route".to_string(), route);
3178
3179        let secrets = extract_config_secrets(&mut config);
3180
3181        // Verify secrets extracted
3182        assert_eq!(
3183            secrets
3184                .get("MQB__TEST_ROUTE__INPUT__KAFKA__URL")
3185                .map(|s| s.as_str()),
3186            Some("kafka://user:pass@localhost:9092")
3187        );
3188        assert_eq!(
3189            secrets
3190                .get("MQB__TEST_ROUTE__INPUT__KAFKA__USERNAME")
3191                .map(|s| s.as_str()),
3192            Some("user")
3193        );
3194        assert_eq!(
3195            secrets
3196                .get("MQB__TEST_ROUTE__INPUT__KAFKA__PASSWORD")
3197                .map(|s| s.as_str()),
3198            Some("pass")
3199        );
3200        assert_eq!(
3201            secrets
3202                .get("MQB__TEST_ROUTE__INPUT__KAFKA__TLS__CERT_PASSWORD")
3203                .map(|s| s.as_str()),
3204            Some("certpass")
3205        );
3206        assert_eq!(
3207            secrets
3208                .get("MQB__TEST_ROUTE__OUTPUT__HTTP__URL")
3209                .map(|s| s.as_str()),
3210            Some("http://httpuser:httppass@localhost")
3211        );
3212        assert_eq!(
3213            secrets
3214                .get("MQB__TEST_ROUTE__OUTPUT__HTTP__BASIC_AUTH__0")
3215                .map(|s| s.as_str()),
3216            Some("httpuser")
3217        );
3218        assert_eq!(
3219            secrets
3220                .get("MQB__TEST_ROUTE__OUTPUT__HTTP__BASIC_AUTH__1")
3221                .map(|s| s.as_str()),
3222            Some("httppass")
3223        );
3224        assert_eq!(
3225            secrets
3226                .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_API_KEY")
3227                .map(|s| s.as_str()),
3228            Some("http-api-key")
3229        );
3230        assert_eq!(
3231            secrets
3232                .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_ACCESS_TOKEN")
3233                .map(|s| s.as_str()),
3234            Some("http-access-token")
3235        );
3236        assert_eq!(
3237            secrets
3238                .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_AUTHENTICATION")
3239                .map(|s| s.as_str()),
3240            Some("http-authentication")
3241        );
3242        assert_eq!(
3243            secrets
3244                .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__AUTHORIZATION")
3245                .map(|s| s.as_str()),
3246            Some("Bearer secret-token")
3247        );
3248
3249        // Verify config cleared
3250        let route = config.get("test_route").unwrap();
3251        if let EndpointType::Kafka(k) = &route.input.endpoint_type {
3252            assert!(k.url.is_empty());
3253            assert!(k.username.is_none());
3254            assert!(k.password.is_none());
3255            assert!(k.tls.cert_password.is_none());
3256        }
3257        if let EndpointType::Http(h) = &route.output.endpoint_type {
3258            assert!(h.url.is_empty());
3259            assert!(h.basic_auth.is_none());
3260            assert!(!h.custom_headers.contains_key("X-API-Key"));
3261            assert!(!h.custom_headers.contains_key("X-Access-Token"));
3262            assert!(!h.custom_headers.contains_key("X-Authentication"));
3263            assert!(!h.custom_headers.contains_key("Authorization"));
3264            assert_eq!(
3265                h.custom_headers.get("X-Trace-Id").map(|s| s.as_str()),
3266                Some("trace-value")
3267            );
3268        }
3269    }
3270
3271    #[test]
3272    fn test_extract_sensitive_url_only_strips_authority_credentials() {
3273        let mut config = Config::new();
3274        let path_at_route = Route {
3275            output: Endpoint {
3276                endpoint_type: EndpointType::Http(HttpConfig::new(
3277                    "https://example.com/path/user@example.com?email=a@b.test",
3278                )),
3279                middlewares: vec![],
3280                handler: None,
3281            },
3282            ..Default::default()
3283        };
3284        config.insert("path_at_route".to_string(), path_at_route);
3285
3286        let credential_route = Route {
3287            output: Endpoint {
3288                endpoint_type: EndpointType::Http(HttpConfig::new(
3289                    "https://user:pass@example.com/path",
3290                )),
3291                middlewares: vec![],
3292                handler: None,
3293            },
3294            ..Default::default()
3295        };
3296        config.insert("credential_route".to_string(), credential_route);
3297
3298        let query_at_route = Route {
3299            output: Endpoint {
3300                endpoint_type: EndpointType::Http(HttpConfig::new(
3301                    "https://example.com?next=a@b.test",
3302                )),
3303                middlewares: vec![],
3304                handler: None,
3305            },
3306            ..Default::default()
3307        };
3308        config.insert("query_at_route".to_string(), query_at_route);
3309
3310        let fragment_at_route = Route {
3311            output: Endpoint {
3312                endpoint_type: EndpointType::Http(HttpConfig::new(
3313                    "https://example.com#user@example.com",
3314                )),
3315                middlewares: vec![],
3316                handler: None,
3317            },
3318            ..Default::default()
3319        };
3320        config.insert("fragment_at_route".to_string(), fragment_at_route);
3321
3322        let secrets = extract_config_secrets(&mut config);
3323
3324        if let EndpointType::Http(http) = &config.get("path_at_route").unwrap().output.endpoint_type
3325        {
3326            assert_eq!(
3327                http.url,
3328                "https://example.com/path/user@example.com?email=a@b.test"
3329            );
3330        }
3331        if let EndpointType::Http(http) =
3332            &config.get("query_at_route").unwrap().output.endpoint_type
3333        {
3334            assert_eq!(http.url, "https://example.com?next=a@b.test");
3335        }
3336        if let EndpointType::Http(http) = &config
3337            .get("fragment_at_route")
3338            .unwrap()
3339            .output
3340            .endpoint_type
3341        {
3342            assert_eq!(http.url, "https://example.com#user@example.com");
3343        }
3344        if let EndpointType::Http(http) =
3345            &config.get("credential_route").unwrap().output.endpoint_type
3346        {
3347            assert!(http.url.is_empty());
3348        }
3349        assert_eq!(
3350            secrets
3351                .get("MQB__CREDENTIAL_ROUTE__OUTPUT__HTTP__URL")
3352                .map(String::as_str),
3353            Some("https://user:pass@example.com/path")
3354        );
3355        assert!(!secrets.contains_key("MQB__PATH_AT_ROUTE__OUTPUT__HTTP__URL"));
3356        assert!(!secrets.contains_key("MQB__QUERY_AT_ROUTE__OUTPUT__HTTP__URL"));
3357        assert!(!secrets.contains_key("MQB__FRAGMENT_AT_ROUTE__OUTPUT__HTTP__URL"));
3358    }
3359
3360    #[test]
3361    fn test_memory_config_requires_topic_or_url() {
3362        let err = serde_yaml_ng::from_str::<MemoryConfig>("{}").unwrap_err();
3363        assert!(err
3364            .to_string()
3365            .contains("MemoryConfig: 'topic' (or 'url' alias) is required."));
3366    }
3367
3368    #[test]
3369    fn test_file_config_inference() {
3370        let yaml = r#"
3371mode: group_subscribe
3372path: "/tmp/test"
3373group_id: "my_group"
3374"#;
3375        let config: FileConfig = serde_yaml_ng::from_str(yaml).unwrap();
3376        match config.mode {
3377            Some(FileConsumerMode::GroupSubscribe { group_id, .. }) => {
3378                assert_eq!(group_id, "my_group")
3379            }
3380            _ => panic!("Expected GroupSubscribe"),
3381        }
3382
3383        let yaml_queue = r#"
3384mode: consume
3385path: "/tmp/test"
3386"#;
3387        let config_queue: FileConfig = serde_yaml_ng::from_str(yaml_queue).unwrap();
3388        match config_queue.mode {
3389            Some(FileConsumerMode::Consume { delete }) => assert!(!delete),
3390            _ => panic!("Expected Consume"),
3391        }
3392    }
3393}
3394
3395#[cfg(all(test, feature = "schema"))]
3396mod schema_tests {
3397    use super::*;
3398
3399    #[test]
3400    fn generate_json_schema() {
3401        let schema = schemars::schema_for!(Config);
3402        let schema_json = serde_json::to_string_pretty(&schema).unwrap();
3403
3404        let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
3405        path.push("mq-bridge.schema.json");
3406        std::fs::write(path, schema_json).expect("Failed to write schema file");
3407    }
3408}