1use 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
18pub type Config = HashMap<String, Route>;
80
81pub type PublisherConfig = HashMap<String, Endpoint>;
84
85#[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 pub input: Endpoint,
93 #[serde(default = "default_output_endpoint")]
95 pub output: Endpoint,
96 #[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#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
129#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
130#[serde(deny_unknown_fields)]
131pub struct RouteOptions {
132 #[serde(default, skip_serializing_if = "String::is_empty")]
134 pub description: String,
135 #[serde(default = "default_concurrency")]
139 #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
140 pub concurrency: usize,
141 #[serde(default = "default_batch_size")]
145 #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
146 pub batch_size: usize,
147 #[serde(default = "default_commit_concurrency_limit")]
151 #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
152 pub commit_concurrency_limit: usize,
153 #[serde(default = "default_startup_timeout_ms")]
155 pub startup_timeout_ms: u64,
156 #[serde(default = "default_reconnect_interval_ms")]
158 pub reconnect_interval_ms: u64,
159 #[serde(default = "default_empty_batch_delay_ms")]
161 pub empty_batch_delay_ms: u64,
162 #[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 #[serde(default = "default_false", skip_serializing_if = "is_false")]
169 #[cfg_attr(feature = "schema", schemars(default = "default_false"))]
170 pub exit_on_empty: bool,
171}
172
173impl Default for RouteOptions {
174 fn default() -> Self {
175 Self {
176 description: String::new(),
177 concurrency: default_concurrency(),
178 batch_size: default_batch_size(),
179 commit_concurrency_limit: default_commit_concurrency_limit(),
180 startup_timeout_ms: default_startup_timeout_ms(),
181 reconnect_interval_ms: default_reconnect_interval_ms(),
182 empty_batch_delay_ms: default_empty_batch_delay_ms(),
183 allow_fault_injection: false,
184 exit_on_empty: false,
185 }
186 }
187}
188
189impl RouteOptions {
190 pub fn validate(&self) -> anyhow::Result<()> {
191 if self.concurrency == 0 {
192 return Err(anyhow::anyhow!("route concurrency must be at least 1"));
193 }
194 if self.batch_size == 0 {
195 return Err(anyhow::anyhow!("route batch_size must be at least 1"));
196 }
197 if self.commit_concurrency_limit == 0 {
198 return Err(anyhow::anyhow!(
199 "route commit_concurrency_limit must be at least 1"
200 ));
201 }
202 Ok(())
203 }
204}
205
206pub(crate) fn default_concurrency() -> usize {
207 1
208}
209
210pub(crate) fn default_batch_size() -> usize {
211 1
212}
213
214pub(crate) fn default_commit_concurrency_limit() -> usize {
215 4096
216}
217
218pub(crate) fn default_startup_timeout_ms() -> u64 {
219 5000
220}
221
222pub(crate) fn default_reconnect_interval_ms() -> u64 {
223 5000
224}
225
226pub(crate) fn default_empty_batch_delay_ms() -> u64 {
227 10
228}
229
230fn is_false(value: &bool) -> bool {
231 !*value
232}
233
234fn default_false() -> bool {
235 false
236}
237
238#[cfg(feature = "schema")]
239fn default_inline_response_fast_path_schema() -> Option<bool> {
240 Some(true)
241}
242
243#[cfg(feature = "schema")]
245fn default_shared_schema() -> Option<bool> {
246 Some(true)
247}
248
249#[cfg(feature = "schema")]
251fn default_kafka_partitions_schema() -> Option<i32> {
252 Some(DEFAULT_KAFKA_PARTITIONS)
253}
254
255pub const DEFAULT_KAFKA_PARTITIONS: i32 = 6;
258
259fn default_output_endpoint() -> Endpoint {
260 Endpoint::new(EndpointType::Null)
261}
262
263fn default_retry_attempts() -> usize {
264 3
265}
266fn default_initial_interval_ms() -> u64 {
267 100
268}
269fn default_max_interval_ms() -> u64 {
270 5000
271}
272fn default_multiplier() -> f64 {
273 2.0
274}
275fn default_clean_session() -> bool {
276 false
277}
278fn default_cookie_metadata_key() -> String {
279 "cookie".to_string()
280}
281fn default_set_cookie_metadata_key() -> String {
282 "set-cookie".to_string()
283}
284
285fn is_known_endpoint_name(name: &str) -> bool {
286 matches!(
287 name,
288 "aws"
289 | "kafka"
290 | "nats"
291 | "file"
292 | "static"
293 | "memory"
294 | "sled"
295 | "amqp"
296 | "mongodb"
297 | "mqtt"
298 | "http"
299 | "websocket"
300 | "ibmmq"
301 | "zeromq"
302 | "grpc"
303 | "fanout"
304 | "stream_buffer"
305 | "ref"
306 | "switch"
307 | "response"
308 | "reader"
309 | "null"
310 | "sqlx"
311 )
312}
313
314#[derive(Serialize, Clone, Default)]
316#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
317#[serde(deny_unknown_fields)]
318pub struct Endpoint {
319 #[serde(default)]
321 pub middlewares: Vec<Middleware>,
322
323 #[serde(flatten)]
325 pub endpoint_type: EndpointType,
326
327 #[serde(skip_serializing)]
328 #[cfg_attr(feature = "schema", schemars(skip))]
329 pub handler: Option<Arc<dyn Handler>>,
331}
332
333impl std::fmt::Debug for Endpoint {
334 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335 f.debug_struct("Endpoint")
336 .field("middlewares", &self.middlewares)
337 .field("endpoint_type", &self.endpoint_type)
338 .field(
339 "handler",
340 &if self.handler.is_some() {
341 "Some(<Handler>)"
342 } else {
343 "None"
344 },
345 )
346 .finish()
347 }
348}
349
350impl<'de> Deserialize<'de> for Endpoint {
351 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
352 where
353 D: Deserializer<'de>,
354 {
355 struct EndpointVisitor;
356
357 impl<'de> Visitor<'de> for EndpointVisitor {
358 type Value = Endpoint;
359
360 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
361 formatter.write_str("a map representing an endpoint, the string \"null\", or null")
362 }
363
364 fn visit_unit<E>(self) -> Result<Self::Value, E>
365 where
366 E: serde::de::Error,
367 {
368 Ok(Endpoint::new(EndpointType::Null))
369 }
370
371 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
374 where
375 E: serde::de::Error,
376 {
377 if value == "null" {
378 Ok(Endpoint::new(EndpointType::Null))
379 } else {
380 Err(serde::de::Error::unknown_variant(value, &["null"]))
381 }
382 }
383
384 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
385 where
386 E: serde::de::Error,
387 {
388 self.visit_str(&value)
389 }
390
391 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
392 where
393 A: MapAccess<'de>,
394 {
395 let mut temp_map = serde_json::Map::new();
398 let mut middlewares_val = None;
399
400 while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
401 if key == "middlewares" {
402 middlewares_val = Some(value);
403 } else {
404 temp_map.insert(key, value);
405 }
406 }
407
408 let temp_val = serde_json::Value::Object(temp_map);
410 let endpoint_type: EndpointType = match serde_json::from_value(temp_val.clone()) {
411 Ok(et) => et,
412 Err(original_err) => {
413 if let serde_json::Value::Object(map) = &temp_val {
414 if map.len() == 1 {
415 let (name, config) = map.iter().next().unwrap();
416 if is_known_endpoint_name(name) {
417 return Err(serde::de::Error::custom(original_err));
418 }
419 trace!("Falling back to Custom endpoint for key: {}", name);
420 EndpointType::Custom {
421 name: name.clone(),
422 config: config.clone(),
423 }
424 } else if map.is_empty() {
425 EndpointType::Null
426 } else {
427 return Err(serde::de::Error::custom(
428 "Invalid endpoint configuration: multiple keys found or unknown endpoint type",
429 ));
430 }
431 } else {
432 return Err(serde::de::Error::custom("Invalid endpoint configuration"));
433 }
434 }
435 };
436
437 let middlewares = match middlewares_val {
439 Some(val) => {
440 deserialize_middlewares_from_value(val).map_err(serde::de::Error::custom)?
441 }
442 None => Vec::new(),
443 };
444
445 Ok(Endpoint {
446 middlewares,
447 endpoint_type,
448 handler: None,
449 })
450 }
451 }
452
453 deserializer.deserialize_any(EndpointVisitor)
454 }
455}
456
457fn is_known_middleware_name(name: &str) -> bool {
458 matches!(
459 name,
460 "deduplication"
461 | "metrics"
462 | "dlq"
463 | "retry"
464 | "random_panic"
465 | "delay"
466 | "weak_join"
467 | "limiter"
468 | "buffer"
469 | "cookie_jar"
470 | "custom"
471 )
472}
473
474fn deserialize_middlewares_from_value(value: serde_json::Value) -> anyhow::Result<Vec<Middleware>> {
478 let arr = match value {
479 serde_json::Value::Array(arr) => arr,
480 serde_json::Value::Object(map) => {
481 let mut middlewares: Vec<_> = map
482 .into_iter()
483 .filter_map(|(key, value)| key.parse::<usize>().ok().map(|index| (index, value)))
486 .collect();
487 middlewares.sort_by_key(|(index, _)| *index);
488
489 middlewares.into_iter().map(|(_, value)| value).collect()
490 }
491 _ => return Err(anyhow::anyhow!("Expected an array or object")),
492 };
493
494 let mut middlewares = Vec::new();
495 for item in arr {
496 let known_name = if let serde_json::Value::Object(map) = &item {
498 if map.len() == 1 {
499 let (name, _) = map.iter().next().unwrap();
500 if is_known_middleware_name(name) {
501 Some(name.clone())
502 } else {
503 None
504 }
505 } else {
506 None
507 }
508 } else {
509 None
510 };
511
512 if let Some(name) = known_name {
513 match serde_json::from_value::<Middleware>(item.clone()) {
514 Ok(m) => middlewares.push(m),
515 Err(e) => {
516 return Err(anyhow::anyhow!(
517 "Failed to deserialize known middleware '{}': {}",
518 name,
519 e
520 ))
521 }
522 }
523 } else if let Ok(m) = serde_json::from_value::<Middleware>(item.clone()) {
524 middlewares.push(m);
525 } else if let serde_json::Value::Object(map) = &item {
526 if map.len() == 1 {
527 let (name, config) = map.iter().next().unwrap();
528 middlewares.push(Middleware::Custom {
529 name: name.clone(),
530 config: config.clone(),
531 });
532 } else {
533 return Err(anyhow::anyhow!(
534 "Invalid middleware configuration: {:?}",
535 item
536 ));
537 }
538 } else {
539 return Err(anyhow::anyhow!(
540 "Invalid middleware configuration: {:?}",
541 item
542 ));
543 }
544 }
545 Ok(middlewares)
546}
547
548#[derive(Debug, Clone, Default)]
578pub struct StaticConfig {
579 pub body: String,
581 pub raw: bool,
583 pub metadata: std::collections::HashMap<String, String>,
585}
586
587#[cfg(feature = "schema")]
591impl schemars::JsonSchema for StaticConfig {
592 fn schema_name() -> std::borrow::Cow<'static, str> {
593 "StaticConfig".into()
594 }
595
596 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
597 schemars::json_schema!({
598 "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.",
599 "oneOf": [
600 {
601 "type": "string",
602 "description": "The response body, JSON-encoded as a string."
603 },
604 {
605 "type": "object",
606 "properties": {
607 "body": {
608 "type": "string",
609 "description": "The static response body."
610 },
611 "raw": {
612 "type": "boolean",
613 "description": "Send the body verbatim instead of JSON-encoding it as a string.",
614 "default": false
615 },
616 "metadata": {
617 "type": "object",
618 "description": "Extra metadata entries attached to the produced message.",
619 "additionalProperties": { "type": "string" }
620 }
621 },
622 "required": ["body"],
623 "additionalProperties": false
624 }
625 ]
626 })
627 }
628}
629
630impl Serialize for StaticConfig {
631 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
632 where
633 S: serde::Serializer,
634 {
635 if !self.raw && self.metadata.is_empty() {
639 return serializer.serialize_str(&self.body);
640 }
641 use serde::ser::SerializeStruct;
642 let mut state = serializer.serialize_struct("StaticConfig", 3)?;
643 state.serialize_field("body", &self.body)?;
644 state.serialize_field("raw", &self.raw)?;
645 state.serialize_field("metadata", &self.metadata)?;
646 state.end()
647 }
648}
649
650impl From<String> for StaticConfig {
651 fn from(body: String) -> Self {
652 StaticConfig {
653 body,
654 raw: false,
655 metadata: std::collections::HashMap::new(),
656 }
657 }
658}
659
660impl From<&str> for StaticConfig {
661 fn from(body: &str) -> Self {
662 StaticConfig::from(body.to_string())
663 }
664}
665
666impl<'de> Deserialize<'de> for StaticConfig {
667 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
668 where
669 D: serde::Deserializer<'de>,
670 {
671 #[derive(Deserialize)]
672 #[serde(untagged)]
673 enum Repr {
674 Str(String),
675 Map {
676 body: String,
677 #[serde(default)]
678 raw: bool,
679 #[serde(default)]
680 metadata: std::collections::HashMap<String, String>,
681 },
682 }
683 Ok(match Repr::deserialize(deserializer)? {
684 Repr::Str(body) => StaticConfig {
685 body,
686 raw: false,
687 metadata: std::collections::HashMap::new(),
688 },
689 Repr::Map {
690 body,
691 raw,
692 metadata,
693 } => StaticConfig {
694 body,
695 raw,
696 metadata,
697 },
698 })
699 }
700}
701
702#[derive(Debug, Deserialize, Serialize, Clone, Default)]
724#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
725#[serde(rename_all = "lowercase")]
726pub enum EndpointType {
727 Aws(AwsConfig),
728 Kafka(KafkaConfig),
729 Nats(NatsConfig),
730 File(FileConfig),
731 #[serde(rename = "object_store", alias = "objectstore", alias = "s3")]
732 ObjectStore(ObjectStoreConfig),
733 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
734 Static(StaticConfig),
735 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
736 Ref(String),
737 Memory(MemoryConfig),
738 Sled(SledConfig),
739 Amqp(AmqpConfig),
740 MongoDb(MongoDbConfig),
741 Mqtt(MqttConfig),
742 Http(HttpConfig),
743 WebSocket(WebSocketConfig),
744 IbmMq(IbmMqConfig),
745 ZeroMq(ZeroMqConfig),
746 #[serde(rename = "redis_streams", alias = "redis")]
747 RedisStreams(RedisStreamsConfig),
748 Grpc(GrpcConfig),
749 Sqlx(SqlxConfig),
750 #[serde(rename = "clickhouse", alias = "click_house")]
751 ClickHouse(ClickHouseConfig),
752 #[serde(rename = "postgres_cdc", alias = "postgres-cdc")]
753 PostgresCdc(PostgresCdcConfig),
754 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
755 Fanout(Vec<Endpoint>),
756 #[serde(rename = "stream_buffer")]
757 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
758 StreamBuffer(StreamBufferConfig),
759 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
760 Switch(SwitchConfig),
761 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
762 Response(ResponseConfig),
763 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
764 Reader(Box<Endpoint>),
765 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
766 Request(RequestForwardConfig),
767 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
768 Custom {
769 name: String,
770 config: serde_json::Value,
771 },
772 #[default]
773 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
774 Null,
775}
776
777impl EndpointType {
778 pub fn name(&self) -> &'static str {
779 match self {
780 EndpointType::Aws(_) => "aws",
781 EndpointType::Kafka(_) => "kafka",
782 EndpointType::Nats(_) => "nats",
783 EndpointType::File(_) => "file",
784 EndpointType::ObjectStore(_) => "object_store",
785 EndpointType::Static(_) => "static",
786 EndpointType::Ref(_) => "ref",
787 EndpointType::Memory(_) => "memory",
788 EndpointType::Sled(_) => "sled",
789 EndpointType::Amqp(_) => "amqp",
790 EndpointType::MongoDb(_) => "mongodb",
791 EndpointType::Mqtt(_) => "mqtt",
792 EndpointType::Http(_) => "http",
793 EndpointType::WebSocket(_) => "websocket",
794 EndpointType::IbmMq(_) => "ibmmq",
795 EndpointType::ZeroMq(_) => "zeromq",
796 EndpointType::RedisStreams(_) => "redis_streams",
797 EndpointType::Grpc(_) => "grpc",
798 EndpointType::Sqlx(_) => "sqlx",
799 EndpointType::ClickHouse(_) => "clickhouse",
800 EndpointType::PostgresCdc(_) => "postgres_cdc",
801 EndpointType::Fanout(_) => "fanout",
802 EndpointType::StreamBuffer(_) => "stream_buffer",
803 EndpointType::Switch(_) => "switch",
804 EndpointType::Response(_) => "response",
805 EndpointType::Reader(_) => "reader",
806 EndpointType::Request(_) => "request",
807 EndpointType::Custom { .. } => "custom",
808 EndpointType::Null => "null",
809 }
810 }
811
812 pub fn is_core(&self) -> bool {
813 matches!(
814 self,
815 EndpointType::File(_)
816 | EndpointType::Static(_)
817 | EndpointType::Ref(_)
818 | EndpointType::Memory(_)
819 | EndpointType::Fanout(_)
820 | EndpointType::StreamBuffer(_)
821 | EndpointType::Switch(_)
822 | EndpointType::Response(_)
823 | EndpointType::Reader(_)
824 | EndpointType::Request(_)
825 | EndpointType::Custom { .. }
826 | EndpointType::Null
827 )
828 }
829}
830
831#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
833#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
834#[serde(rename_all = "snake_case")]
835pub enum CipherKind {
836 #[default]
838 Xchacha20poly1305,
839 Aes256gcm,
841}
842
843#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
847#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
848#[serde(deny_unknown_fields)]
849pub struct EncryptionConfig {
850 #[serde(default)]
852 pub cipher: CipherKind,
853 #[serde(default = "default_encryption_key_id")]
855 pub key_id: String,
856 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
858 pub key: String,
859 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
861 #[cfg_attr(feature = "schema", schemars(extend("format" = "password")))]
862 pub decrypt_keys: HashMap<String, String>,
863}
864
865fn default_encryption_key_id() -> String {
866 "default".to_string()
867}
868
869impl Default for EncryptionConfig {
870 fn default() -> Self {
871 Self {
872 cipher: CipherKind::default(),
873 key_id: default_encryption_key_id(),
874 key: String::new(),
875 decrypt_keys: HashMap::new(),
876 }
877 }
878}
879
880#[derive(Debug, Deserialize, Serialize, Clone)]
882#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
883#[serde(rename_all = "snake_case")]
884pub enum Middleware {
885 Deduplication(DeduplicationMiddleware),
886 Metrics(MetricsMiddleware),
887 Dlq(Box<DeadLetterQueueMiddleware>),
888 Retry(RetryMiddleware),
889 RandomPanic(RandomPanicMiddleware),
890 Delay(DelayMiddleware),
891 WeakJoin(WeakJoinMiddleware),
892 Limiter(LimiterMiddleware),
893 Buffer(BufferMiddleware),
894 CookieJar(CookieJarMiddleware),
895 Transform(TransformMiddleware),
896 Encryption(EncryptionConfig),
897 Compression(CompressionMiddleware),
898 Custom {
899 name: String,
900 config: serde_json::Value,
901 },
902}
903
904#[derive(Debug, Deserialize, Serialize, Clone)]
909#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
910#[serde(deny_unknown_fields)]
911pub struct DeduplicationMiddleware {
912 #[serde(default)]
914 pub store: Option<String>,
915 #[serde(default)]
917 pub sled_path: Option<String>,
918 pub ttl_seconds: u64,
920}
921
922#[derive(Debug, Deserialize, Serialize, Clone)]
930#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
931#[serde(deny_unknown_fields)]
932pub struct MetricsMiddleware {}
933
934#[derive(Debug, Deserialize, Serialize, Clone, Default)]
941#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
942#[serde(deny_unknown_fields)]
943pub struct DeadLetterQueueMiddleware {
944 pub endpoint: Endpoint,
946}
947
948#[derive(Debug, Deserialize, Serialize, Clone, Default)]
953#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
954#[serde(deny_unknown_fields)]
955pub struct RetryMiddleware {
956 #[serde(default = "default_retry_attempts")]
958 pub max_attempts: usize,
959 #[serde(default = "default_initial_interval_ms")]
961 pub initial_interval_ms: u64,
962 #[serde(default = "default_max_interval_ms")]
964 pub max_interval_ms: u64,
965 #[serde(default = "default_multiplier")]
967 pub multiplier: f64,
968}
969
970#[derive(Debug, Deserialize, Serialize, Clone)]
975#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
976#[serde(deny_unknown_fields)]
977pub struct DelayMiddleware {
978 pub delay_ms: u64,
980}
981
982#[derive(Debug, Deserialize, Serialize, Clone)]
988#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
989#[serde(deny_unknown_fields)]
990pub struct LimiterMiddleware {
991 pub messages_per_second: f64,
993}
994
995#[derive(Debug, Deserialize, Serialize, Clone)]
1000#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1001#[serde(deny_unknown_fields)]
1002pub struct BufferMiddleware {
1003 pub max_messages: usize,
1005 pub max_delay_ms: u64,
1007}
1008
1009#[derive(Debug, Deserialize, Serialize, Clone)]
1017#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1018#[serde(deny_unknown_fields)]
1019pub struct CookieJarMiddleware {
1020 #[serde(default)]
1023 pub shared_scope: Option<String>,
1024 #[serde(default = "default_cookie_metadata_key")]
1026 pub cookie_metadata_key: String,
1027 #[serde(default = "default_set_cookie_metadata_key")]
1029 pub set_cookie_metadata_key: String,
1030 #[serde(default)]
1032 pub capture_metadata_keys: Vec<String>,
1033 #[serde(default)]
1038 pub export_metadata_prefix: Option<String>,
1039 #[serde(default)]
1044 pub inject_metadata: HashMap<String, String>,
1045}
1046
1047impl Default for CookieJarMiddleware {
1048 fn default() -> Self {
1049 Self {
1050 shared_scope: None,
1051 cookie_metadata_key: default_cookie_metadata_key(),
1052 set_cookie_metadata_key: default_set_cookie_metadata_key(),
1053 capture_metadata_keys: Vec::new(),
1054 export_metadata_prefix: None,
1055 inject_metadata: HashMap::new(),
1056 }
1057 }
1058}
1059
1060#[derive(Debug, Deserialize, Serialize, Clone)]
1066#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1067#[serde(deny_unknown_fields)]
1068pub struct WeakJoinMiddleware {
1069 pub group_by: String,
1071 pub expected_count: usize,
1073 pub timeout_ms: u64,
1075 #[serde(default)]
1077 pub branch_by: Option<String>,
1078 #[serde(default)]
1080 pub required: Vec<String>,
1081 #[serde(default)]
1083 pub on_timeout: WeakJoinTimeout,
1084}
1085
1086#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
1088#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1089#[serde(rename_all = "snake_case")]
1090pub enum WeakJoinTimeout {
1091 #[default]
1093 Fire,
1094 Discard,
1096}
1097
1098#[derive(Debug, Deserialize, Serialize, Clone)]
1111#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1112#[cfg_attr(feature = "schema", schemars(transform = transform_middleware_schema_transform))]
1113#[serde(deny_unknown_fields)]
1114pub struct TransformMiddleware {
1115 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1117 pub mapping: HashMap<String, MappingRule>,
1118 #[serde(default, skip_serializing_if = "Option::is_none")]
1120 pub schema: Option<serde_json::Value>,
1121 #[serde(default, skip_serializing_if = "Option::is_none")]
1123 pub schema_file: Option<String>,
1124 #[serde(default = "default_true")]
1126 pub coerce: bool,
1127 #[serde(default = "default_true")]
1129 pub apply_defaults: bool,
1130 #[serde(default)]
1132 pub on_error: TransformErrorPolicy,
1133}
1134
1135impl Default for TransformMiddleware {
1140 fn default() -> Self {
1141 Self {
1142 mapping: HashMap::new(),
1143 schema: None,
1144 schema_file: None,
1145 coerce: default_true(),
1146 apply_defaults: default_true(),
1147 on_error: TransformErrorPolicy::default(),
1148 }
1149 }
1150}
1151
1152#[derive(Debug, Deserialize, Serialize, Clone)]
1157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1158#[serde(untagged)]
1159pub enum MappingRule {
1160 Path(String),
1162 Detailed(DetailedMappingRule),
1164}
1165
1166#[derive(Debug, Deserialize, Serialize, Clone)]
1168#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1169#[serde(deny_unknown_fields)]
1170pub struct DetailedMappingRule {
1171 pub path: String,
1173 #[serde(default, skip_serializing_if = "Option::is_none")]
1175 pub default: Option<serde_json::Value>,
1176 #[serde(default)]
1178 pub required: bool,
1179}
1180
1181impl MappingRule {
1182 pub fn path(&self) -> &str {
1184 match self {
1185 MappingRule::Path(p) => p,
1186 MappingRule::Detailed(d) => &d.path,
1187 }
1188 }
1189}
1190
1191#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
1193#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1194#[serde(rename_all = "snake_case")]
1195pub enum TransformErrorPolicy {
1196 #[default]
1198 Reject,
1199 PassThrough,
1201}
1202
1203#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1205#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1206#[serde(rename_all = "snake_case")]
1207pub enum FaultMode {
1208 #[default]
1210 Panic,
1211 Disconnect,
1213 Timeout,
1215 JsonFormatError,
1217 Nack,
1219}
1220
1221impl std::fmt::Display for FaultMode {
1222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1223 match self {
1224 FaultMode::Panic => write!(f, "panic"),
1225 FaultMode::Disconnect => write!(f, "disconnect"),
1226 FaultMode::Timeout => write!(f, "timeout"),
1227 FaultMode::JsonFormatError => write!(f, "json_format_error"),
1228 FaultMode::Nack => write!(f, "nack"),
1229 }
1230 }
1231}
1232
1233#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1246#[serde(deny_unknown_fields)]
1247#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1248pub struct RandomPanicMiddleware {
1249 #[serde(default)]
1251 pub mode: FaultMode,
1252 #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
1254 #[serde(default)]
1255 pub trigger_on_message: Option<usize>,
1256 #[serde(default = "default_true")]
1258 pub enabled: bool,
1259 #[serde(skip, default = "default_atomic_usize_arc")]
1260 #[cfg_attr(feature = "schema", schemars(skip))]
1261 pub message_count: Arc<AtomicUsize>,
1262}
1263
1264fn default_true() -> bool {
1265 true
1266}
1267
1268fn default_atomic_usize_arc() -> Arc<AtomicUsize> {
1269 Arc::new(AtomicUsize::new(0))
1270}
1271
1272fn deserialize_null_as_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
1273where
1274 D: Deserializer<'de>,
1275{
1276 let opt = Option::<bool>::deserialize(deserializer)?;
1277 Ok(opt.unwrap_or(false))
1278}
1279
1280#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1282#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1283#[serde(deny_unknown_fields)]
1284pub struct AwsConfig {
1285 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1287 pub queue_url: Option<String>,
1288 pub topic_arn: Option<String>,
1290 pub region: Option<String>,
1292 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1294 pub endpoint_url: Option<String>,
1295 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1297 pub access_key: Option<String>,
1298 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1300 pub secret_key: Option<String>,
1301 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1303 pub session_token: Option<String>,
1304 #[cfg_attr(feature = "schema", schemars(range(min = 1, max = 10)))]
1306 pub max_messages: Option<i32>,
1307 #[cfg_attr(feature = "schema", schemars(range(min = 0, max = 20)))]
1309 pub wait_time_seconds: Option<i32>,
1310 #[serde(default)]
1312 pub binary_payload_mode: bool,
1313}
1314
1315impl AwsConfig {
1316 pub fn new() -> Self {
1318 Self::default()
1319 }
1320
1321 pub fn with_queue_url(mut self, queue_url: impl Into<String>) -> Self {
1322 self.queue_url = Some(queue_url.into());
1323 self
1324 }
1325
1326 pub fn with_topic_arn(mut self, topic_arn: impl Into<String>) -> Self {
1327 self.topic_arn = Some(topic_arn.into());
1328 self
1329 }
1330
1331 pub fn with_region(mut self, region: impl Into<String>) -> Self {
1332 self.region = Some(region.into());
1333 self
1334 }
1335
1336 pub fn with_endpoint_url(mut self, endpoint_url: impl Into<String>) -> Self {
1337 self.endpoint_url = Some(endpoint_url.into());
1338 self
1339 }
1340
1341 pub fn with_credentials(
1342 mut self,
1343 access_key: impl Into<String>,
1344 secret_key: impl Into<String>,
1345 ) -> Self {
1346 self.access_key = Some(access_key.into());
1347 self.secret_key = Some(secret_key.into());
1348 self
1349 }
1350}
1351
1352#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1356#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1357#[serde(deny_unknown_fields)]
1358pub struct KafkaConfig {
1359 #[serde(alias = "brokers")]
1361 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1362 pub url: String,
1363 pub topic: Option<String>,
1365 pub username: Option<String>,
1367 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1369 pub password: Option<String>,
1370 #[serde(default)]
1372 pub tls: TlsConfig,
1373 pub group_id: Option<String>,
1376 #[serde(default)]
1378 pub delayed_ack: bool,
1379 #[serde(default)]
1381 pub producer_options: Option<Vec<(String, String)>>,
1382 #[serde(default)]
1384 pub consumer_options: Option<Vec<(String, String)>>,
1385 #[serde(default)]
1387 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
1388 pub shared: Option<bool>,
1389 #[serde(default)]
1393 #[cfg_attr(
1394 feature = "schema",
1395 schemars(default = "default_kafka_partitions_schema", range(min = 1))
1396 )]
1397 pub partitions: Option<i32>,
1398 #[serde(default)]
1402 pub partition_key: Option<String>,
1403}
1404
1405impl KafkaConfig {
1406 pub fn new(url: impl Into<String>) -> Self {
1408 Self {
1409 url: url.into(),
1410 ..Default::default()
1411 }
1412 }
1413
1414 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
1415 self.topic = Some(topic.into());
1416 self
1417 }
1418
1419 pub fn with_group_id(mut self, group_id: impl Into<String>) -> Self {
1420 self.group_id = Some(group_id.into());
1421 self
1422 }
1423
1424 pub fn with_credentials(
1425 mut self,
1426 username: impl Into<String>,
1427 password: impl Into<String>,
1428 ) -> Self {
1429 self.username = Some(username.into());
1430 self.password = Some(password.into());
1431 self
1432 }
1433
1434 pub fn with_producer_option(
1435 mut self,
1436 key: impl Into<String>,
1437 value: impl Into<String>,
1438 ) -> Self {
1439 let options = self.producer_options.get_or_insert_with(Vec::new);
1440 options.push((key.into(), value.into()));
1441 self
1442 }
1443
1444 pub fn with_consumer_option(
1445 mut self,
1446 key: impl Into<String>,
1447 value: impl Into<String>,
1448 ) -> Self {
1449 let options = self.consumer_options.get_or_insert_with(Vec::new);
1450 options.push((key.into(), value.into()));
1451 self
1452 }
1453}
1454
1455#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1459#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1460#[serde(deny_unknown_fields)]
1461pub struct SledConfig {
1462 pub path: String,
1464 pub tree: Option<String>,
1466 #[serde(default)]
1468 pub read_from_start: bool,
1469 #[serde(default)]
1471 pub delete_after_read: bool,
1472}
1473
1474impl SledConfig {
1475 pub fn new(path: impl Into<String>) -> Self {
1477 Self {
1478 path: path.into(),
1479 ..Default::default()
1480 }
1481 }
1482
1483 pub fn with_tree(mut self, tree: impl Into<String>) -> Self {
1484 self.tree = Some(tree.into());
1485 self
1486 }
1487
1488 pub fn with_read_from_start(mut self, read_from_start: bool) -> Self {
1489 self.read_from_start = read_from_start;
1490 self
1491 }
1492}
1493
1494#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
1496#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1497#[serde(rename_all = "snake_case")]
1498pub enum FileFormat {
1499 #[default]
1501 Normal,
1502 Json,
1504 Text,
1506 Raw,
1508 Csv,
1510}
1511
1512#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
1515#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1516#[serde(rename_all = "snake_case")]
1517pub enum Compression {
1518 #[default]
1520 None,
1521 Gzip,
1523 Lz4,
1525 Zstd,
1528}
1529
1530fn default_compression_algorithm() -> Compression {
1531 Compression::Zstd
1532}
1533
1534#[derive(Debug, Deserialize, Serialize, Clone)]
1541#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1542#[serde(deny_unknown_fields)]
1543pub struct CompressionMiddleware {
1544 #[serde(default = "default_compression_algorithm")]
1546 pub algorithm: Compression,
1547 #[serde(default)]
1550 pub max_decompressed_bytes: Option<u64>,
1551}
1552
1553impl Default for CompressionMiddleware {
1554 fn default() -> Self {
1555 Self {
1556 algorithm: default_compression_algorithm(),
1557 max_decompressed_bytes: None,
1558 }
1559 }
1560}
1561
1562#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1565#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1566pub struct FileConfig {
1567 pub path: String,
1569 pub delimiter: Option<String>,
1573 #[serde(flatten, default)]
1576 pub mode: Option<FileConsumerMode>,
1577 #[serde(default)]
1579 pub format: FileFormat,
1580 #[serde(default)]
1582 pub compression: Compression,
1583 #[serde(default)]
1585 pub encryption: Option<EncryptionConfig>,
1586}
1587
1588#[derive(Debug, Clone, Deserialize, Serialize)]
1589#[serde(tag = "mode", rename_all = "snake_case")]
1590#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1591pub enum FileConsumerMode {
1592 Consume {
1596 #[serde(default)]
1599 delete: bool,
1600 },
1601 Subscribe {
1605 #[serde(default)]
1608 delete: bool,
1609 },
1610 GroupSubscribe {
1615 group_id: String,
1617 #[serde(default)]
1620 read_from_tail: bool,
1621 },
1622}
1623
1624impl Default for FileConsumerMode {
1625 fn default() -> Self {
1626 Self::Consume { delete: false }
1627 }
1628}
1629
1630impl FileConfig {
1631 pub fn new(path: impl Into<String>) -> Self {
1633 Self {
1634 path: path.into(),
1635 mode: Some(FileConsumerMode::default()),
1636 delimiter: None,
1637 format: FileFormat::default(),
1638 compression: Compression::default(),
1639 encryption: None,
1640 }
1641 }
1642
1643 pub fn with_mode(mut self, mode: FileConsumerMode) -> Self {
1644 self.mode = Some(mode);
1645 self
1646 }
1647
1648 pub fn effective_mode(&self) -> FileConsumerMode {
1650 self.mode.clone().unwrap_or_default()
1651 }
1652}
1653
1654#[derive(Debug, Clone, Serialize, Deserialize)]
1664#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1665pub struct ObjectStoreConfig {
1666 pub url: String,
1671 #[serde(default)]
1674 pub format: FileFormat,
1675 pub delimiter: Option<String>,
1678 pub checkpoint_store: Option<String>,
1682 pub cursor_id: Option<String>,
1684 pub polling_interval_ms: Option<u64>,
1687 pub max_object_bytes: Option<u64>,
1691 #[serde(default = "default_true")]
1694 pub date_partition: bool,
1695 pub extension: Option<String>,
1700 #[serde(default)]
1702 pub compression: Compression,
1703 #[serde(default)]
1705 pub encryption: Option<EncryptionConfig>,
1706}
1707
1708impl Default for ObjectStoreConfig {
1709 fn default() -> Self {
1710 Self {
1711 url: String::new(),
1712 format: FileFormat::default(),
1713 delimiter: None,
1714 checkpoint_store: None,
1715 cursor_id: None,
1716 polling_interval_ms: None,
1717 max_object_bytes: None,
1718 date_partition: true,
1719 extension: None,
1720 compression: Compression::default(),
1721 encryption: None,
1722 }
1723 }
1724}
1725
1726#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1730#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1731#[serde(deny_unknown_fields)]
1732pub struct NatsConfig {
1733 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1735 pub url: String,
1736 pub subject: Option<String>,
1739 pub stream: Option<String>,
1742 pub username: Option<String>,
1744 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1746 pub password: Option<String>,
1747 #[serde(default)]
1749 pub tls: TlsConfig,
1750 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1752 pub token: Option<String>,
1753 #[serde(default)]
1757 pub request_reply: bool,
1758 pub request_timeout_ms: Option<u64>,
1760 #[serde(default)]
1762 pub delayed_ack: bool,
1763 #[serde(default)]
1766 pub deduplicate: bool,
1767 #[serde(default)]
1769 pub no_jetstream: bool,
1770 #[serde(default)]
1772 pub subscriber_mode: bool,
1773 pub stream_max_messages: Option<i64>,
1775 pub deliver_policy: Option<NatsDeliverPolicy>,
1777 pub stream_max_bytes: Option<i64>,
1779 pub prefetch_count: Option<usize>,
1781 #[serde(default)]
1783 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
1784 pub shared: Option<bool>,
1785}
1786
1787#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
1788#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1789#[serde(rename_all = "snake_case")]
1790pub enum NatsDeliverPolicy {
1791 #[default]
1792 All,
1793 Last,
1794 New,
1795 LastPerSubject,
1796}
1797
1798impl NatsConfig {
1799 pub fn new(url: impl Into<String>) -> Self {
1801 Self {
1802 url: url.into(),
1803 ..Default::default()
1804 }
1805 }
1806
1807 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
1808 self.subject = Some(subject.into());
1809 self
1810 }
1811
1812 pub fn with_stream(mut self, stream: impl Into<String>) -> Self {
1813 self.stream = Some(stream.into());
1814 self
1815 }
1816
1817 pub fn with_deliver_policy(mut self, policy: NatsDeliverPolicy) -> Self {
1818 self.deliver_policy = Some(policy);
1819 self
1820 }
1821
1822 pub fn with_credentials(
1823 mut self,
1824 username: impl Into<String>,
1825 password: impl Into<String>,
1826 ) -> Self {
1827 self.username = Some(username.into());
1828 self.password = Some(password.into());
1829 self
1830 }
1831}
1832
1833#[derive(Debug, Serialize, Clone, Default)]
1834#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1835#[cfg_attr(feature = "schema", schemars(transform = memory_config_schema_transform))]
1836#[serde(deny_unknown_fields)]
1837pub struct MemoryConfig {
1838 #[serde(default, skip_serializing_if = "String::is_empty", alias = "url")]
1847 pub topic: String,
1848 #[serde(skip)]
1850 pub url: Option<String>,
1851 pub capacity: Option<usize>,
1853 #[serde(default)]
1855 pub request_reply: bool,
1856 pub request_timeout_ms: Option<u64>,
1858 #[serde(default)]
1860 pub subscribe_mode: bool,
1861 #[serde(default)]
1864 pub enable_nack: bool,
1865 #[serde(skip)]
1866 pub enable_nack_overridden: bool,
1867}
1868
1869impl MemoryConfig {
1870 pub fn new(topic: impl Into<String>, capacity: Option<usize>) -> Self {
1871 Self {
1872 topic: topic.into(),
1873 url: None,
1874 capacity,
1875 ..Default::default()
1876 }
1877 }
1878
1879 pub fn new_with_url(url: impl Into<String>, capacity: Option<usize>) -> Self {
1880 let url = url.into();
1881 Self {
1882 topic: url.clone(),
1883 url: Some(url),
1884 capacity,
1885 ..Default::default()
1886 }
1887 }
1888
1889 pub fn with_subscribe(self, subscribe_mode: bool) -> Self {
1890 Self {
1891 subscribe_mode,
1892 ..self
1893 }
1894 }
1895
1896 pub fn with_request_reply(mut self, request_reply: bool) -> Self {
1897 self.request_reply = request_reply;
1898 self
1899 }
1900
1901 pub fn get_transport_identifier(&self) -> anyhow::Result<String> {
1904 let identifier = if !self.topic.is_empty() {
1905 &self.topic
1906 } else if let Some(url) = self.url.as_ref().filter(|url| !url.is_empty()) {
1907 url
1908 } else {
1909 return Err(anyhow::anyhow!(
1910 "MemoryConfig: 'topic' (or 'url' alias) is required."
1911 ));
1912 };
1913
1914 if identifier.contains("://") {
1916 Ok(identifier.clone())
1917 } else {
1918 Ok(format!("memory://{}", identifier))
1919 }
1920 }
1921
1922 pub fn is_ipc_transport(&self) -> bool {
1925 if let Ok(identifier) = self.get_transport_identifier() {
1926 identifier.starts_with("ipc://")
1927 || identifier.starts_with("unix://")
1928 || identifier.starts_with("pipe://")
1929 } else {
1930 false
1931 }
1932 }
1933
1934 pub fn with_smart_defaults(mut self) -> Self {
1937 if !self.enable_nack_overridden && self.is_ipc_transport() {
1938 self.enable_nack = true;
1939 }
1940 self
1941 }
1942}
1943
1944impl<'de> Deserialize<'de> for MemoryConfig {
1945 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1946 where
1947 D: Deserializer<'de>,
1948 {
1949 #[derive(Deserialize, Default)]
1950 #[serde(deny_unknown_fields)]
1951 struct MemoryConfigSerde {
1952 #[serde(default)]
1953 topic: String,
1954 #[serde(default)]
1955 url: Option<String>,
1956 capacity: Option<usize>,
1957 #[serde(default)]
1958 request_reply: bool,
1959 request_timeout_ms: Option<u64>,
1960 #[serde(default)]
1961 subscribe_mode: bool,
1962 #[serde(default)]
1963 enable_nack: Option<bool>,
1964 }
1965
1966 let raw = MemoryConfigSerde::deserialize(deserializer)?;
1967 if raw.topic.is_empty() && raw.url.as_deref().is_none_or(str::is_empty) {
1968 return Err(serde::de::Error::custom(
1969 "MemoryConfig: 'topic' (or 'url' alias) is required.",
1970 ));
1971 }
1972 let topic = if raw.topic.is_empty() {
1973 raw.url.clone().unwrap_or_default()
1974 } else {
1975 raw.topic
1976 };
1977 Ok(Self {
1978 topic,
1979 url: raw.url,
1980 capacity: raw.capacity,
1981 request_reply: raw.request_reply,
1982 request_timeout_ms: raw.request_timeout_ms,
1983 subscribe_mode: raw.subscribe_mode,
1984 enable_nack: raw.enable_nack.unwrap_or(false),
1985 enable_nack_overridden: raw.enable_nack.is_some(),
1986 })
1987 }
1988}
1989
1990#[cfg(feature = "schema")]
1991fn memory_config_schema_transform(schema: &mut schemars::Schema) {
1992 let Some(schema_obj) = schema.as_object_mut() else {
1993 return;
1994 };
1995
1996 let Some(properties) = schema_obj
1997 .get_mut("properties")
1998 .and_then(serde_json::Value::as_object_mut)
1999 else {
2000 return;
2001 };
2002
2003 properties.insert(
2004 "url".to_string(),
2005 serde_json::json!({
2006 "description": "Alias for `topic`. Use either `topic` or `url`.",
2007 "type": "string",
2008 "minLength": 1
2009 }),
2010 );
2011
2012 if let Some(topic) = properties
2015 .get_mut("topic")
2016 .and_then(serde_json::Value::as_object_mut)
2017 {
2018 topic.insert("minLength".to_string(), serde_json::json!(1));
2019 }
2020
2021 schema_obj.insert(
2022 "anyOf".to_string(),
2023 serde_json::json!([
2024 { "required": ["topic"] },
2025 { "required": ["url"] }
2026 ]),
2027 );
2028}
2029
2030#[cfg(feature = "schema")]
2031fn route_schema_transform(schema: &mut schemars::Schema) {
2032 let Some(properties) = schema
2033 .as_object_mut()
2034 .and_then(|schema_obj| schema_obj.get_mut("properties"))
2035 .and_then(serde_json::Value::as_object_mut)
2036 else {
2037 return;
2038 };
2039
2040 if let Some(output) = properties
2043 .get_mut("output")
2044 .and_then(serde_json::Value::as_object_mut)
2045 {
2046 let reference = output.remove("$ref");
2047 let default = output.remove("default");
2048 let description = output.remove("description");
2049 output.clear();
2050 let mut any_of = Vec::new();
2051 if let Some(reference) = reference {
2052 any_of.push(serde_json::json!({ "$ref": reference }));
2053 }
2054 any_of.push(serde_json::json!({ "type": "null" }));
2055 output.insert("anyOf".to_string(), serde_json::Value::Array(any_of));
2056 if let Some(description) = description {
2057 output.insert("description".to_string(), description);
2058 }
2059 if let Some(default) = default {
2060 output.insert("default".to_string(), default);
2061 }
2062 }
2063
2064 let Some(allow_fault_injection) = properties
2065 .get_mut("allow_fault_injection")
2066 .and_then(serde_json::Value::as_object_mut)
2067 else {
2068 return;
2069 };
2070
2071 allow_fault_injection.insert("default".to_string(), serde_json::Value::Bool(false));
2072}
2073
2074#[cfg(feature = "schema")]
2076fn transform_middleware_schema_transform(schema: &mut schemars::Schema) {
2077 if let Some(schema_obj) = schema.as_object_mut() {
2078 schema_obj.insert(
2081 "not".to_string(),
2082 serde_json::json!({
2083 "required": ["schema", "schema_file"],
2084 "properties": { "schema_file": { "type": "string" } }
2085 }),
2086 );
2087 }
2088}
2089
2090#[derive(Debug, Serialize, Deserialize, Clone, Default)]
2092#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2093#[serde(deny_unknown_fields)]
2094pub struct StreamBufferConfig {
2095 pub topic: String,
2097 #[serde(default, skip_serializing_if = "Option::is_none")]
2103 pub correlation_id: Option<String>,
2104 #[serde(default, skip_serializing_if = "Option::is_none")]
2106 pub capacity: Option<usize>,
2107}
2108
2109impl StreamBufferConfig {
2110 pub fn new(topic: impl Into<String>) -> Self {
2116 Self {
2117 topic: topic.into(),
2118 ..Default::default()
2119 }
2120 }
2121
2122 pub fn with_correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
2124 self.correlation_id = Some(correlation_id.into());
2125 self
2126 }
2127
2128 pub fn with_capacity(mut self, capacity: usize) -> Self {
2130 self.capacity = Some(capacity);
2131 self
2132 }
2133}
2134
2135#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2139#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2140#[serde(deny_unknown_fields)]
2141pub struct AmqpConfig {
2142 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2146 pub url: String,
2147 pub queue: Option<String>,
2149 #[serde(default)]
2151 pub subscribe_mode: bool,
2152 pub username: Option<String>,
2154 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2156 pub password: Option<String>,
2157 #[serde(default)]
2159 pub tls: TlsConfig,
2160 pub exchange: Option<String>,
2162 pub prefetch_count: Option<u16>,
2164 #[serde(default)]
2166 pub no_persistence: bool,
2167 #[serde(default)]
2169 pub no_declare_queue: bool,
2170 #[serde(default)]
2172 pub delayed_ack: bool,
2173}
2174
2175impl AmqpConfig {
2176 pub fn new(url: impl Into<String>) -> Self {
2178 Self {
2179 url: url.into(),
2180 ..Default::default()
2181 }
2182 }
2183
2184 pub fn with_queue(mut self, queue: impl Into<String>) -> Self {
2185 self.queue = Some(queue.into());
2186 self
2187 }
2188
2189 pub fn with_exchange(mut self, exchange: impl Into<String>) -> Self {
2190 self.exchange = Some(exchange.into());
2191 self
2192 }
2193
2194 pub fn with_credentials(
2195 mut self,
2196 username: impl Into<String>,
2197 password: impl Into<String>,
2198 ) -> Self {
2199 self.username = Some(username.into());
2200 self.password = Some(password.into());
2201 self
2202 }
2203}
2204
2205#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
2209#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2210#[serde(rename_all = "lowercase")]
2211pub enum MongoDbFormat {
2212 #[default]
2213 Normal,
2214 Json,
2215 Text,
2216 Raw,
2217}
2218
2219#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
2222#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2223#[serde(rename_all = "snake_case")]
2224pub enum MongoConsume {
2225 #[default]
2228 Consumer,
2229 Subscriber,
2231 CaptureNew,
2234 CaptureAll,
2237}
2238
2239#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2243#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2244#[serde(deny_unknown_fields)]
2245pub struct MongoDbConfig {
2246 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2249 pub url: String,
2250 pub collection: Option<String>,
2252 pub username: Option<String>,
2255 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2257 pub password: Option<String>,
2259 #[serde(default)]
2261 pub tls: TlsConfig,
2262 pub database: String,
2264 pub polling_interval_ms: Option<u64>,
2266 pub reply_polling_ms: Option<u64>,
2268 #[serde(default)]
2270 pub request_reply: bool,
2271 pub consume: Option<MongoConsume>,
2278 pub receive_query: Option<String>,
2280 #[serde(default)]
2282 pub change_stream: bool,
2283 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2295 pub checkpoint_store: Option<String>,
2296 pub request_timeout_ms: Option<u64>,
2298 pub ttl_seconds: Option<u64>,
2300 pub capped_size_bytes: Option<i64>,
2302 #[serde(default)]
2304 pub format: MongoDbFormat,
2305 pub id_field: Option<String>,
2308 #[serde(default)]
2311 pub report_outcome: bool,
2312 pub cursor_id: Option<String>,
2314 pub meta_collection: Option<String>,
2316 #[serde(default)]
2318 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
2319 pub shared: Option<bool>,
2320}
2321
2322impl MongoDbConfig {
2323 pub fn new(url: impl Into<String>, database: impl Into<String>) -> Self {
2325 Self {
2326 url: url.into(),
2327 database: database.into(),
2328 ..Default::default()
2329 }
2330 }
2331
2332 pub fn with_collection(mut self, collection: impl Into<String>) -> Self {
2333 self.collection = Some(collection.into());
2334 self
2335 }
2336
2337 pub fn with_credentials(
2338 mut self,
2339 username: impl Into<String>,
2340 password: impl Into<String>,
2341 ) -> Self {
2342 self.username = Some(username.into());
2343 self.password = Some(password.into());
2344 self
2345 }
2346
2347 pub fn with_change_stream(mut self, change_stream: bool) -> Self {
2348 self.change_stream = change_stream;
2349 self
2350 }
2351
2352 pub fn resolved_consume(&self) -> MongoConsume {
2355 if let Some(mode) = self.consume {
2356 return mode;
2357 }
2358 if self.change_stream {
2359 MongoConsume::Subscriber
2360 } else {
2361 MongoConsume::Consumer
2362 }
2363 }
2364}
2365
2366#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2370#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2371#[serde(deny_unknown_fields)]
2372pub struct MqttConfig {
2373 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2375 pub url: String,
2376 pub topic: Option<String>,
2378 pub username: Option<String>,
2380 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2382 pub password: Option<String>,
2383 #[serde(default)]
2385 pub tls: TlsConfig,
2386 pub client_id: Option<String>,
2388 pub queue_capacity: Option<usize>,
2390 pub max_inflight: Option<u16>,
2392 pub qos: Option<u8>,
2394 #[serde(default = "default_clean_session")]
2396 pub clean_session: bool,
2397 pub keep_alive_seconds: Option<u64>,
2399 #[serde(default)]
2401 pub protocol: MqttProtocol,
2402 pub session_expiry_interval: Option<u32>,
2404 #[serde(default)]
2410 pub delayed_ack: bool,
2411}
2412
2413impl MqttConfig {
2414 pub fn new(url: impl Into<String>) -> Self {
2416 Self {
2417 url: url.into(),
2418 ..Default::default()
2419 }
2420 }
2421
2422 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
2423 self.topic = Some(topic.into());
2424 self
2425 }
2426
2427 pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
2428 self.client_id = Some(client_id.into());
2429 self
2430 }
2431
2432 pub fn with_credentials(
2433 mut self,
2434 username: impl Into<String>,
2435 password: impl Into<String>,
2436 ) -> Self {
2437 self.username = Some(username.into());
2438 self.password = Some(password.into());
2439 self
2440 }
2441}
2442
2443#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
2447#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2448#[serde(rename_all = "lowercase")]
2449pub enum MqttProtocol {
2450 #[default]
2451 V5,
2452 V3,
2453}
2454
2455#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2458#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2459#[serde(deny_unknown_fields)]
2460pub struct ZeroMqConfig {
2461 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2462 pub url: String,
2464 #[serde(default)]
2466 pub socket_type: Option<ZeroMqSocketType>,
2467 pub topic: Option<String>,
2469 #[serde(default)]
2471 pub bind: bool,
2472 #[serde(default)]
2474 pub internal_buffer_size: Option<usize>,
2475 #[serde(default)]
2477 pub format: ZeroMqFormat,
2478 #[serde(default)]
2480 pub backend: ZeroMqBackend,
2481 #[serde(default)]
2483 pub request_timeout_ms: Option<u64>,
2484}
2485
2486impl ZeroMqConfig {
2487 pub fn new(url: impl Into<String>) -> Self {
2489 Self {
2490 url: url.into(),
2491 ..Default::default()
2492 }
2493 }
2494
2495 pub fn with_socket_type(mut self, socket_type: ZeroMqSocketType) -> Self {
2496 self.socket_type = Some(socket_type);
2497 self
2498 }
2499
2500 pub fn with_bind(mut self, bind: bool) -> Self {
2501 self.bind = bind;
2502 self
2503 }
2504}
2505
2506#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
2514#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2515#[serde(rename_all = "snake_case")]
2516pub enum ZeroMqFormat {
2517 #[default]
2518 Json,
2519 Raw,
2520 RawFramed,
2521}
2522
2523#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
2528#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2529#[serde(rename_all = "lowercase")]
2530pub enum ZeroMqSocketType {
2531 Push,
2532 Pull,
2533 Pub,
2534 Sub,
2535 Req,
2536 Rep,
2537}
2538
2539#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
2546#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2547#[serde(rename_all = "lowercase")]
2548pub enum ZeroMqBackend {
2549 #[default]
2550 Zmq,
2551 Omq,
2552}
2553
2554#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2562#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2563#[serde(deny_unknown_fields)]
2564pub struct RedisStreamsConfig {
2565 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2567 pub url: String,
2568 pub stream: Option<String>,
2570 pub group: Option<String>,
2572 pub consumer_name: Option<String>,
2574 #[serde(default)]
2576 pub subscriber_mode: bool,
2577 pub block_ms: Option<u64>,
2579 #[serde(default)]
2581 pub read_from_start: bool,
2582 pub redelivery_timeout_ms: Option<u64>,
2584 pub maxlen: Option<usize>,
2586 pub approx_trim: Option<bool>,
2588 pub username: Option<String>,
2590 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2592 pub password: Option<String>,
2593 pub internal_buffer_size: Option<usize>,
2595 pub reader_connections: Option<usize>,
2598}
2599
2600impl RedisStreamsConfig {
2601 pub fn new(url: impl Into<String>) -> Self {
2603 Self {
2604 url: url.into(),
2605 ..Default::default()
2606 }
2607 }
2608
2609 pub fn with_stream(mut self, stream: impl Into<String>) -> Self {
2610 self.stream = Some(stream.into());
2611 self
2612 }
2613
2614 pub fn with_group(mut self, group: impl Into<String>) -> Self {
2615 self.group = Some(group.into());
2616 self
2617 }
2618
2619 pub fn with_subscriber(mut self, subscriber: bool) -> Self {
2620 self.subscriber_mode = subscriber;
2621 self
2622 }
2623
2624 pub fn with_reader_connections(mut self, connections: usize) -> Self {
2625 self.reader_connections = Some(connections);
2626 self
2627 }
2628}
2629
2630#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2633#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2634#[serde(deny_unknown_fields)]
2635pub struct GrpcConfig {
2636 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2637 pub url: String,
2639 pub topic: Option<String>,
2641 pub timeout_ms: Option<u64>,
2645 #[serde(default)]
2647 pub tls: TlsConfig,
2648 #[serde(default)]
2651 pub server_mode: bool,
2652 #[serde(default)]
2654 pub initial_stream_window_size: Option<u32>,
2655 #[serde(default)]
2657 pub initial_connection_window_size: Option<u32>,
2658 #[serde(default)]
2660 pub concurrency_limit_per_connection: Option<usize>,
2661 #[serde(default)]
2663 pub http2_keepalive_interval_ms: Option<u64>,
2664 #[serde(default)]
2666 pub http2_keepalive_timeout_ms: Option<u64>,
2667 #[serde(default)]
2669 pub max_decoding_message_size: Option<usize>,
2670 #[serde(default)]
2672 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
2673 pub shared: Option<bool>,
2674}
2675
2676impl GrpcConfig {
2677 pub fn new(url: impl Into<String>) -> Self {
2679 Self {
2680 url: url.into(),
2681 ..Default::default()
2682 }
2683 }
2684
2685 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
2686 self.topic = Some(topic.into());
2687 self
2688 }
2689
2690 pub fn with_server_mode(mut self, server_mode: bool) -> Self {
2692 self.server_mode = server_mode;
2693 self
2694 }
2695}
2696
2697#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, Default)]
2701#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2702#[serde(rename_all = "snake_case")]
2703pub enum HttpServerProtocol {
2704 #[default]
2706 Auto,
2707 Http1Only,
2709 Http2Only,
2711}
2712
2713#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
2715#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2716#[serde(rename_all = "snake_case")]
2717pub enum WebSocketExecutionMode {
2718 #[default]
2721 Auto,
2722 DirectOnly,
2724 Routed,
2726}
2727
2728#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2730#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2731#[serde(deny_unknown_fields)]
2732pub struct HttpConfig {
2733 pub url: String,
2735 pub path: Option<String>,
2737 pub method: Option<String>,
2739 #[serde(default)]
2741 pub tls: TlsConfig,
2742 pub workers: Option<usize>,
2744 pub message_id_header: Option<String>,
2746 pub request_timeout_ms: Option<u64>,
2748 pub internal_buffer_size: Option<usize>,
2750 #[serde(default)]
2752 pub fire_and_forget: bool,
2753 #[serde(default)]
2755 pub receive_streamable: bool,
2756 #[serde(default, skip_serializing_if = "Option::is_none")]
2759 #[cfg_attr(
2760 feature = "schema",
2761 schemars(default = "default_inline_response_fast_path_schema")
2762 )]
2763 pub inline_response_fast_path: Option<bool>,
2764 #[serde(default)]
2768 pub server_protocol: HttpServerProtocol,
2769 #[serde(default, skip_serializing_if = "Option::is_none")]
2779 pub stream_response_to: Option<Box<Endpoint>>,
2780 #[serde(default, skip_serializing_if = "Option::is_none")]
2782 pub batch_concurrency: Option<usize>,
2783 #[serde(default, skip_serializing_if = "Option::is_none")]
2785 pub tcp_keepalive_ms: Option<u64>,
2786 #[serde(default, skip_serializing_if = "Option::is_none")]
2788 pub pool_idle_timeout_ms: Option<u64>,
2789 #[serde(default)]
2793 pub compression: Compression,
2794 #[serde(default, skip_serializing_if = "Option::is_none")]
2798 pub compression_enabled: Option<bool>,
2799 #[serde(default)]
2801 pub compression_threshold_bytes: Option<usize>,
2802 pub concurrency_limit: Option<usize>,
2804 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2806 #[serde(
2807 default,
2808 skip_serializing_if = "Option::is_none",
2809 deserialize_with = "deserialize_basic_auth"
2810 )]
2811 pub basic_auth: Option<(String, String)>,
2812 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2814 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
2815 pub custom_headers: HashMap<String, String>,
2816 #[serde(default)]
2818 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
2819 pub shared: Option<bool>,
2820}
2821
2822#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2824#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2825#[serde(deny_unknown_fields)]
2826pub struct WebSocketConfig {
2827 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2829 pub url: String,
2830 pub path: Option<String>,
2832 pub message_id_header: Option<String>,
2834 pub routed_queue_capacity: Option<usize>,
2836 pub backlog: Option<u32>,
2840 #[serde(default)]
2842 pub execution_mode: WebSocketExecutionMode,
2843}
2844
2845fn deserialize_basic_auth<'de, D>(deserializer: D) -> Result<Option<(String, String)>, D::Error>
2846where
2847 D: Deserializer<'de>,
2848{
2849 let val = serde_json::Value::deserialize(deserializer)?;
2850 match val {
2851 serde_json::Value::Null => Ok(None),
2852 serde_json::Value::Array(arr) => {
2853 if arr.len() != 2 {
2854 return Err(serde::de::Error::custom("basic_auth must have 2 elements"));
2855 }
2856 let u = arr[0]
2857 .as_str()
2858 .ok_or_else(|| serde::de::Error::custom("basic_auth[0] must be string"))?
2859 .to_string();
2860 let p = arr[1]
2861 .as_str()
2862 .ok_or_else(|| serde::de::Error::custom("basic_auth[1] must be string"))?
2863 .to_string();
2864 Ok(Some((u, p)))
2865 }
2866 serde_json::Value::Object(map) => {
2867 let u = map
2868 .get("0")
2869 .and_then(|v| v.as_str())
2870 .ok_or_else(|| serde::de::Error::custom("basic_auth map missing '0'"))?
2871 .to_string();
2872 let p = map
2873 .get("1")
2874 .and_then(|v| v.as_str())
2875 .ok_or_else(|| serde::de::Error::custom("basic_auth map missing '1'"))?
2876 .to_string();
2877 Ok(Some((u, p)))
2878 }
2879 _ => Err(serde::de::Error::custom("invalid type for basic_auth")),
2880 }
2881}
2882
2883impl HttpConfig {
2884 pub fn new(url: impl Into<String>) -> Self {
2886 Self {
2887 url: url.into(),
2888 ..Default::default()
2889 }
2890 }
2891
2892 pub fn with_workers(mut self, workers: usize) -> Self {
2893 self.workers = Some(workers);
2894 self
2895 }
2896
2897 pub fn with_method(mut self, method: impl Into<String>) -> Self {
2898 self.method = Some(method.into());
2899 self
2900 }
2901
2902 pub fn with_path(mut self, path: impl Into<String>) -> Self {
2903 self.path = Some(path.into());
2904 self
2905 }
2906
2907 pub fn with_receive_streamable(mut self, receive_streamable: bool) -> Self {
2908 self.receive_streamable = receive_streamable;
2909 self
2910 }
2911
2912 pub fn with_inline_response_fast_path(mut self, inline_response_fast_path: bool) -> Self {
2913 self.inline_response_fast_path = Some(inline_response_fast_path);
2914 self
2915 }
2916
2917 pub fn with_server_protocol(mut self, server_protocol: HttpServerProtocol) -> Self {
2918 self.server_protocol = server_protocol;
2919 self
2920 }
2921
2922 pub fn inline_response_fast_path_enabled(&self) -> bool {
2923 self.inline_response_fast_path.unwrap_or(true)
2924 }
2925
2926 pub fn publisher_compression(&self) -> Compression {
2929 match self.compression {
2930 Compression::None if self.compression_enabled == Some(true) => Compression::Gzip,
2931 other => other,
2932 }
2933 }
2934
2935 pub fn consumer_compression_enabled(&self) -> bool {
2938 self.compression_enabled == Some(true)
2939 }
2940
2941 pub fn with_stream_response_to(mut self, endpoint: Endpoint) -> Self {
2942 self.stream_response_to = Some(Box::new(endpoint));
2943 self
2944 }
2945}
2946
2947impl WebSocketConfig {
2948 pub fn new(url: impl Into<String>) -> Self {
2950 Self {
2951 url: url.into(),
2952 ..Default::default()
2953 }
2954 }
2955
2956 pub fn with_path(mut self, path: impl Into<String>) -> Self {
2957 self.path = Some(path.into());
2958 self
2959 }
2960
2961 pub fn with_backlog(mut self, backlog: u32) -> Self {
2962 self.backlog = Some(backlog);
2963 self
2964 }
2965
2966 pub fn with_execution_mode(mut self, execution_mode: WebSocketExecutionMode) -> Self {
2967 self.execution_mode = execution_mode;
2968 self
2969 }
2970}
2971
2972#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
2979#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2980#[cfg_attr(feature = "schema", schemars(transform = ibm_tls_config_schema_transform))]
2981#[serde(deny_unknown_fields)]
2982pub struct IbmTlsConfig {
2983 #[serde(default, deserialize_with = "deserialize_null_as_false")]
2985 pub required: bool,
2986 pub cipher_spec: Option<String>,
2988 #[serde(rename = "cert_file", alias = "key_repository")]
2992 pub key_repository: Option<String>,
2993 #[serde(rename = "cert_password", alias = "key_repository_password")]
2996 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2997 pub key_repository_password: Option<String>,
2998 #[serde(default)]
3000 pub accept_invalid_certs: bool,
3001}
3002
3003#[cfg(feature = "schema")]
3007fn ibm_tls_config_schema_transform(schema: &mut schemars::Schema) {
3008 let Some(properties) = schema
3009 .as_object_mut()
3010 .and_then(|schema_obj| schema_obj.get_mut("properties"))
3011 .and_then(serde_json::Value::as_object_mut)
3012 else {
3013 return;
3014 };
3015
3016 properties.insert(
3017 "key_repository".to_string(),
3018 serde_json::json!({
3019 "description": "MQ-native alias for `cert_file`: the CMS key repository stem \
3020 (e.g. `/path/to/tls` for `tls.kdb`/`tls.sth`).",
3021 "type": ["string", "null"]
3022 }),
3023 );
3024
3025 properties.insert(
3026 "key_repository_password".to_string(),
3027 serde_json::json!({
3028 "description": "MQ-native alias for `cert_password`: password unlocking the key \
3029 repository. Requires an IBM MQ client/server at 9.3.0.0+.",
3030 "type": ["string", "null"],
3031 "format": "password"
3032 }),
3033 );
3034}
3035
3036#[derive(Debug, Deserialize, Serialize, Clone)]
3041#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3042#[serde(deny_unknown_fields)]
3043pub struct IbmMqConfig {
3044 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3046 pub url: String,
3047 pub queue: Option<String>,
3049 pub topic: Option<String>,
3051 pub queue_manager: String,
3053 pub channel: String,
3055 pub username: Option<String>,
3057 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3059 pub password: Option<String>,
3060 #[serde(default)]
3062 pub tls: IbmTlsConfig,
3063 #[serde(default = "default_max_message_size")]
3065 pub max_message_size: usize,
3066 #[serde(default = "default_wait_timeout_ms")]
3068 pub wait_timeout_ms: i32,
3069 #[serde(default)]
3071 pub internal_buffer_size: Option<usize>,
3072 #[serde(default)]
3074 pub disable_status_inq: bool,
3075}
3076
3077impl IbmMqConfig {
3078 pub fn new(
3080 url: impl Into<String>,
3081 queue_manager: impl Into<String>,
3082 channel: impl Into<String>,
3083 ) -> Self {
3084 Self {
3085 url: url.into(),
3086 queue_manager: queue_manager.into(),
3087 channel: channel.into(),
3088 disable_status_inq: false,
3089 ..Default::default()
3090 }
3091 }
3092
3093 pub fn with_queue(mut self, queue: impl Into<String>) -> Self {
3094 self.queue = Some(queue.into());
3095 self
3096 }
3097
3098 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
3099 self.topic = Some(topic.into());
3100 self
3101 }
3102
3103 pub fn with_credentials(
3104 mut self,
3105 username: impl Into<String>,
3106 password: impl Into<String>,
3107 ) -> Self {
3108 self.username = Some(username.into());
3109 self.password = Some(password.into());
3110 self
3111 }
3112}
3113
3114impl Default for IbmMqConfig {
3115 fn default() -> Self {
3116 Self {
3117 url: String::new(),
3118 queue: None,
3119 topic: None,
3120 queue_manager: String::new(),
3121 channel: String::new(),
3122 username: None,
3123 password: None,
3124 tls: IbmTlsConfig::default(),
3125 max_message_size: default_max_message_size(),
3126 wait_timeout_ms: default_wait_timeout_ms(),
3127 internal_buffer_size: None,
3128 disable_status_inq: false,
3129 }
3130 }
3131}
3132
3133fn default_max_message_size() -> usize {
3134 4 * 1024 * 1024 }
3136
3137fn default_wait_timeout_ms() -> i32 {
3138 1000 }
3140
3141#[derive(Debug, Deserialize, Serialize, Clone)]
3144#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3145#[serde(deny_unknown_fields)]
3146pub struct SwitchConfig {
3147 pub metadata_key: String,
3149 pub cases: HashMap<String, Endpoint>,
3151 pub default: Option<Box<Endpoint>>,
3153}
3154
3155#[derive(Debug, Deserialize, Serialize, Clone, Default)]
3157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3158#[serde(deny_unknown_fields)]
3159pub struct ResponseConfig {
3160 }
3162
3163#[derive(Debug, Deserialize, Serialize, Clone)]
3174#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3175#[serde(deny_unknown_fields)]
3176pub struct RequestForwardConfig {
3177 pub to: Box<Endpoint>,
3179 pub forward_to: Box<Endpoint>,
3181}
3182
3183#[derive(Debug, Deserialize, Serialize, Clone, Default)]
3187#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3188#[serde(deny_unknown_fields)]
3189pub struct PostgresCdcConfig {
3190 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3192 pub url: String,
3193 pub publication: String,
3195 #[serde(default = "default_pg_cdc_slot")]
3197 pub slot_name: String,
3198 #[serde(default = "default_true")]
3200 pub create_slot: bool,
3201 #[serde(default)]
3204 pub create_publication: bool,
3205 #[serde(default)]
3208 pub publication_tables: Vec<String>,
3209 #[serde(default)]
3211 pub temporary_slot: bool,
3212 pub cursor_id: Option<String>,
3214 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3216 pub checkpoint_store: Option<String>,
3217 #[serde(default = "default_pg_cdc_status_interval_ms")]
3219 pub status_interval_ms: u64,
3220 #[serde(default)]
3222 pub tls: TlsConfig,
3223}
3224
3225fn default_pg_cdc_slot() -> String {
3226 "mq_bridge_slot".to_string()
3227}
3228
3229fn default_pg_cdc_status_interval_ms() -> u64 {
3230 10_000
3231}
3232
3233#[derive(Debug, Deserialize, Serialize, Clone, Default)]
3237#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3238#[serde(deny_unknown_fields)]
3239pub struct SqlxConfig {
3240 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3242 pub url: String,
3243 #[serde(default)]
3245 pub username: Option<String>,
3246 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3248 #[serde(default)]
3249 pub password: Option<String>,
3250 pub table: String,
3252 pub insert_query: Option<String>,
3269 pub select_query: Option<String>,
3273 #[serde(default)]
3275 pub delete_after_read: bool,
3276 pub cursor_column: Option<String>,
3281 pub cursor_id: Option<String>,
3285 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3297 pub checkpoint_store: Option<String>,
3298 #[serde(default)]
3300 pub auto_create_table: bool,
3301 #[serde(default)]
3303 pub bulk_copy: bool,
3304 pub polling_interval_ms: Option<u64>,
3306 pub max_polling_interval_ms: Option<u64>,
3309 pub publication: Option<String>,
3313 pub slot_name: Option<String>,
3315 #[serde(default)]
3318 pub create_publication: bool,
3319 #[serde(default)]
3321 pub tls: TlsConfig,
3322 pub max_connections: Option<u32>,
3324 pub min_connections: Option<u32>,
3326 pub acquire_timeout_ms: Option<u64>,
3328 pub idle_timeout_ms: Option<u64>,
3330 pub max_lifetime_ms: Option<u64>,
3332 #[serde(default)]
3334 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
3335 pub shared: Option<bool>,
3336}
3337
3338#[derive(Debug, Deserialize, Serialize, Clone, Default)]
3348#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3349#[serde(deny_unknown_fields)]
3350pub struct ClickHouseConfig {
3351 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3354 pub url: String,
3355 #[serde(default)]
3357 pub username: Option<String>,
3358 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3360 #[serde(default)]
3361 pub password: Option<String>,
3362 pub database: Option<String>,
3364 pub table: String,
3366 pub columns: Option<std::collections::BTreeMap<String, String>>,
3371 #[serde(default)]
3374 pub async_insert: bool,
3375 #[serde(default)]
3378 pub wait_for_async_insert: Option<bool>,
3379 pub cursor_column: Option<String>,
3383 pub cursor_id: Option<String>,
3386 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3395 pub checkpoint_store: Option<String>,
3396 pub select_columns: Option<String>,
3398 pub polling_interval_ms: Option<u64>,
3400 pub max_polling_interval_ms: Option<u64>,
3403 pub request_timeout_ms: Option<u64>,
3406 pub connect_timeout_ms: Option<u64>,
3408 #[serde(default)]
3410 pub tls: TlsConfig,
3411 #[serde(default = "default_gzip_compression")]
3415 pub compression: Compression,
3416}
3417
3418fn default_gzip_compression() -> Compression {
3419 Compression::Gzip
3420}
3421
3422#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
3443#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3444#[serde(deny_unknown_fields)]
3445pub struct TlsConfig {
3446 #[serde(default, deserialize_with = "deserialize_null_as_false")]
3448 pub required: bool,
3449 pub ca_file: Option<String>,
3451 pub cert_file: Option<String>,
3453 pub key_file: Option<String>,
3455 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3457 pub cert_password: Option<String>,
3458 #[serde(default)]
3460 pub accept_invalid_certs: bool,
3461}
3462
3463impl TlsConfig {
3464 pub fn new() -> Self {
3466 Self::default()
3467 }
3468
3469 pub fn with_ca_file(mut self, ca_file: impl Into<String>) -> Self {
3470 self.ca_file = Some(ca_file.into());
3471 self.required = true;
3472 self
3473 }
3474
3475 pub fn with_client_cert(
3476 mut self,
3477 cert_file: impl Into<String>,
3478 key_file: impl Into<String>,
3479 ) -> Self {
3480 self.cert_file = Some(cert_file.into());
3481 self.key_file = Some(key_file.into());
3482 self.required = true;
3483 self
3484 }
3485
3486 pub fn with_insecure(mut self, accept_invalid_certs: bool) -> Self {
3487 self.accept_invalid_certs = accept_invalid_certs;
3488 self
3489 }
3490
3491 pub fn is_mtls_client_configured(&self) -> bool {
3493 self.required && self.cert_file.is_some() && self.key_file.is_some()
3494 }
3495
3496 pub fn is_tls_server_configured(&self) -> bool {
3498 self.required && self.cert_file.is_some() && self.key_file.is_some()
3499 }
3500
3501 pub fn is_tls_client_configured(&self) -> bool {
3503 self.required
3504 || self.ca_file.is_some()
3505 || (self.cert_file.is_some() && self.key_file.is_some())
3506 }
3507
3508 pub fn normalize_url(&self, url: &str) -> String {
3510 if url
3511 .get(..7)
3512 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("http://"))
3513 || url
3514 .get(..8)
3515 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("https://"))
3516 {
3517 url.to_string()
3518 } else {
3519 let is_tls = self.required;
3520 let scheme = if is_tls { "https" } else { "http" };
3521 format!("{}://{}", scheme, url)
3522 }
3523 }
3524}
3525
3526pub trait SecretExtractor {
3528 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>);
3530}
3531
3532fn extract_sensitive_string_map_entries(
3533 values: &mut HashMap<String, String>,
3534 prefix: &str,
3535 field_name: &str,
3536 secrets: &mut HashMap<String, String>,
3537) {
3538 let secret_keys = values
3539 .keys()
3540 .filter(|key| {
3541 let key = key.to_ascii_lowercase();
3542 key.contains("key") || key.contains("token") || key.contains("auth")
3543 })
3544 .cloned()
3545 .collect::<Vec<_>>();
3546
3547 for key in secret_keys {
3548 if let Some(value) = values.remove(&key) {
3549 secrets.insert(
3550 sanitize_secret_key(&format!("{}__{}__{}", prefix, field_name, key)),
3551 value,
3552 );
3553 }
3554 }
3555}
3556
3557fn url_has_userinfo(url: &str) -> bool {
3558 let Some(authority_start) = url.find("://").map(|idx| idx + 3) else {
3559 return false;
3560 };
3561 let authority_end = url[authority_start..]
3562 .find(['/', '?', '#'])
3563 .map(|idx| authority_start + idx)
3564 .unwrap_or(url.len());
3565 url[authority_start..authority_end].contains('@')
3566}
3567
3568fn sanitize_secret_key(key: &str) -> String {
3569 key.chars()
3570 .map(|ch| {
3571 let ch = ch.to_ascii_uppercase();
3572 if ch.is_ascii_alphanumeric() || ch == '_' {
3573 ch
3574 } else {
3575 '_'
3576 }
3577 })
3578 .collect()
3579}
3580
3581fn extract_sensitive_url(
3582 url: &mut String,
3583 prefix: &str,
3584 field_name: &str,
3585 secrets: &mut HashMap<String, String>,
3586) {
3587 if !url.is_empty() && url_has_userinfo(url) {
3588 secrets.insert(
3589 sanitize_secret_key(&format!("{}__{}", prefix, field_name)),
3590 std::mem::take(url),
3591 );
3592 }
3593}
3594
3595fn extract_sensitive_optional_url(
3596 url: &mut Option<String>,
3597 prefix: &str,
3598 field_name: &str,
3599 secrets: &mut HashMap<String, String>,
3600) {
3601 if url.as_ref().is_some_and(|url| url_has_userinfo(url)) {
3602 if let Some(url) = url.take() {
3603 secrets.insert(
3604 sanitize_secret_key(&format!("{}__{}", prefix, field_name)),
3605 url,
3606 );
3607 }
3608 }
3609}
3610
3611impl SecretExtractor for Route {
3612 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3613 self.input
3614 .extract_secrets(&format!("{}__{}", prefix, "INPUT"), secrets);
3615 self.output
3616 .extract_secrets(&format!("{}__{}", prefix, "OUTPUT"), secrets);
3617 }
3618}
3619
3620impl SecretExtractor for Endpoint {
3621 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3622 for (i, middleware) in self.middlewares.iter_mut().enumerate() {
3623 middleware.extract_secrets(&format!("{}__{}__{}", prefix, "MIDDLEWARES", i), secrets);
3624 }
3625 self.endpoint_type.extract_secrets(prefix, secrets);
3626 }
3627}
3628
3629impl SecretExtractor for EndpointType {
3630 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3631 match self {
3632 EndpointType::Aws(cfg) => {
3633 cfg.extract_secrets(&format!("{}__{}", prefix, "AWS"), secrets)
3634 }
3635 EndpointType::Kafka(cfg) => {
3636 cfg.extract_secrets(&format!("{}__{}", prefix, "KAFKA"), secrets)
3637 }
3638 EndpointType::Nats(cfg) => {
3639 cfg.extract_secrets(&format!("{}__{}", prefix, "NATS"), secrets)
3640 }
3641 EndpointType::Amqp(cfg) => {
3642 cfg.extract_secrets(&format!("{}__{}", prefix, "AMQP"), secrets)
3643 }
3644 EndpointType::MongoDb(cfg) => {
3645 cfg.extract_secrets(&format!("{}__{}", prefix, "MONGODB"), secrets)
3646 }
3647 EndpointType::Mqtt(cfg) => {
3648 cfg.extract_secrets(&format!("{}__{}", prefix, "MQTT"), secrets)
3649 }
3650 EndpointType::Http(cfg) => {
3651 cfg.extract_secrets(&format!("{}__{}", prefix, "HTTP"), secrets)
3652 }
3653 EndpointType::WebSocket(cfg) => {
3654 cfg.extract_secrets(&format!("{}__{}", prefix, "WEBSOCKET"), secrets)
3655 }
3656 EndpointType::IbmMq(cfg) => {
3657 cfg.extract_secrets(&format!("{}__{}", prefix, "IBMMQ"), secrets)
3658 }
3659 EndpointType::ZeroMq(cfg) => {
3660 cfg.extract_secrets(&format!("{}__{}", prefix, "ZEROMQ"), secrets)
3661 }
3662 EndpointType::RedisStreams(cfg) => {
3663 cfg.extract_secrets(&format!("{}__{}", prefix, "REDIS_STREAMS"), secrets)
3664 }
3665 EndpointType::Sqlx(cfg) => {
3666 cfg.extract_secrets(&format!("{}__{}", prefix, "SQLX"), secrets)
3667 }
3668 EndpointType::ClickHouse(cfg) => {
3669 cfg.extract_secrets(&format!("{}__{}", prefix, "CLICKHOUSE"), secrets)
3670 }
3671 EndpointType::PostgresCdc(cfg) => {
3672 cfg.extract_secrets(&format!("{}__{}", prefix, "POSTGRES_CDC"), secrets)
3673 }
3674 EndpointType::Grpc(cfg) => {
3675 cfg.extract_secrets(&format!("{}__{}", prefix, "GRPC"), secrets)
3676 }
3677 EndpointType::Fanout(endpoints) => {
3678 for (i, ep) in endpoints.iter_mut().enumerate() {
3679 ep.extract_secrets(&format!("{}__{}__{}", prefix, "FANOUT", i), secrets);
3680 }
3681 }
3682 EndpointType::Switch(cfg) => {
3683 for (key, ep) in cfg.cases.iter_mut() {
3684 ep.extract_secrets(
3685 &format!(
3686 "{}__{}__{}",
3687 prefix,
3688 "SWITCH__CASES",
3689 sanitize_secret_key(key)
3690 ),
3691 secrets,
3692 );
3693 }
3694 if let Some(default) = &mut cfg.default {
3695 default.extract_secrets(&format!("{}__{}", prefix, "SWITCH__DEFAULT"), secrets);
3696 }
3697 }
3698 EndpointType::Reader(ep) => {
3699 ep.extract_secrets(&format!("{}__{}", prefix, "READER"), secrets)
3700 }
3701 EndpointType::Request(cfg) => {
3702 cfg.to
3703 .extract_secrets(&format!("{}__{}", prefix, "REQUEST__TO"), secrets);
3704 cfg.forward_to
3705 .extract_secrets(&format!("{}__{}", prefix, "REQUEST__FORWARD_TO"), secrets);
3706 }
3707 EndpointType::File(cfg) => {
3708 if let Some(enc) = &mut cfg.encryption {
3709 enc.extract_secrets(&format!("{}__{}", prefix, "FILE__ENCRYPTION"), secrets);
3710 }
3711 }
3712 EndpointType::ObjectStore(cfg) => {
3713 if let Some(enc) = &mut cfg.encryption {
3714 enc.extract_secrets(
3715 &format!("{}__{}", prefix, "OBJECT_STORE__ENCRYPTION"),
3716 secrets,
3717 );
3718 }
3719 }
3720 _ => {}
3721 }
3722 }
3723}
3724
3725impl SecretExtractor for Middleware {
3726 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3727 match self {
3728 Middleware::Dlq(cfg) => {
3729 cfg.endpoint
3730 .extract_secrets(&format!("{}__{}__{}", prefix, "DLQ", "ENDPOINT"), secrets);
3731 }
3732 Middleware::Encryption(cfg) => {
3733 cfg.extract_secrets(&format!("{}__{}", prefix, "ENCRYPTION"), secrets);
3734 }
3735 _ => {}
3736 }
3737 }
3738}
3739
3740impl SecretExtractor for EncryptionConfig {
3741 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3742 if !self.key.is_empty() {
3743 secrets.insert(
3744 sanitize_secret_key(&format!("{}__{}", prefix, "KEY")),
3745 std::mem::take(&mut self.key),
3746 );
3747 }
3748 for (id, k) in std::mem::take(&mut self.decrypt_keys) {
3749 secrets.insert(
3750 sanitize_secret_key(&format!("{}__{}__{}", prefix, "DECRYPT_KEYS", id)),
3751 k,
3752 );
3753 }
3754 }
3755}
3756
3757impl SecretExtractor for AwsConfig {
3758 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3759 if let Some(val) = self.access_key.take() {
3760 secrets.insert(format!("{}__{}", prefix, "ACCESS_KEY"), val);
3761 }
3762 if let Some(val) = self.secret_key.take() {
3763 secrets.insert(format!("{}__{}", prefix, "SECRET_KEY"), val);
3764 }
3765 if let Some(val) = self.session_token.take() {
3766 secrets.insert(format!("{}__{}", prefix, "SESSION_TOKEN"), val);
3767 }
3768 extract_sensitive_optional_url(&mut self.queue_url, prefix, "QUEUE_URL", secrets);
3769 extract_sensitive_optional_url(&mut self.endpoint_url, prefix, "ENDPOINT_URL", secrets);
3770 }
3771}
3772
3773impl SecretExtractor for KafkaConfig {
3774 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3775 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3776 if let Some(val) = self.username.take() {
3777 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3778 }
3779 if let Some(val) = self.password.take() {
3780 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3781 }
3782 self.tls
3783 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3784 }
3785}
3786
3787impl SecretExtractor for NatsConfig {
3788 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3789 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3790 if let Some(val) = self.username.take() {
3791 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3792 }
3793 if let Some(val) = self.password.take() {
3794 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3795 }
3796 if let Some(val) = self.token.take() {
3797 secrets.insert(format!("{}__{}", prefix, "TOKEN"), val);
3798 }
3799 self.tls
3800 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3801 }
3802}
3803
3804impl SecretExtractor for AmqpConfig {
3805 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3806 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3807 if let Some(val) = self.username.take() {
3808 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3809 }
3810 if let Some(val) = self.password.take() {
3811 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3812 }
3813 self.tls
3814 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3815 }
3816}
3817
3818impl SecretExtractor for MongoDbConfig {
3819 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3820 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3821 if let Some(val) = self.username.take() {
3822 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3823 }
3824 if let Some(val) = self.password.take() {
3825 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3826 }
3827 extract_sensitive_optional_url(
3829 &mut self.checkpoint_store,
3830 prefix,
3831 "CHECKPOINT_STORE",
3832 secrets,
3833 );
3834 self.tls
3835 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3836 }
3837}
3838
3839impl SecretExtractor for MqttConfig {
3840 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3841 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3842 if let Some(val) = self.username.take() {
3843 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3844 }
3845 if let Some(val) = self.password.take() {
3846 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3847 }
3848 self.tls
3849 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3850 }
3851}
3852
3853impl SecretExtractor for HttpConfig {
3854 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3855 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3856 if let Some((u, p)) = self.basic_auth.take() {
3857 secrets.insert(format!("{}__{}__{}", prefix, "BASIC_AUTH", 0), u);
3858 secrets.insert(format!("{}__{}__{}", prefix, "BASIC_AUTH", 1), p);
3859 }
3860 extract_sensitive_string_map_entries(
3861 &mut self.custom_headers,
3862 prefix,
3863 "CUSTOM_HEADERS",
3864 secrets,
3865 );
3866 self.tls
3867 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3868 if let Some(endpoint) = &mut self.stream_response_to {
3869 endpoint.extract_secrets(&format!("{}__{}", prefix, "STREAM_RESPONSE_TO"), secrets);
3870 }
3871 }
3872}
3873
3874impl SecretExtractor for WebSocketConfig {
3875 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3876 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3877 }
3878}
3879
3880impl SecretExtractor for IbmMqConfig {
3881 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3882 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3883 if let Some(val) = self.username.take() {
3884 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3885 }
3886 if let Some(val) = self.password.take() {
3887 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3888 }
3889 self.tls
3890 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3891 }
3892}
3893
3894impl SecretExtractor for ZeroMqConfig {
3895 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3896 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3897 }
3898}
3899
3900impl SecretExtractor for RedisStreamsConfig {
3901 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3902 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3903 if let Some(val) = self.username.take() {
3904 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3905 }
3906 if let Some(val) = self.password.take() {
3907 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3908 }
3909 }
3910}
3911
3912impl SecretExtractor for SqlxConfig {
3913 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3914 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3915 if let Some(val) = self.username.take() {
3916 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3917 }
3918 if let Some(val) = self.password.take() {
3919 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3920 }
3921 self.tls
3922 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3923 }
3924}
3925
3926impl SecretExtractor for ClickHouseConfig {
3927 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3928 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3929 if let Some(val) = self.username.take() {
3930 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3931 }
3932 if let Some(val) = self.password.take() {
3933 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3934 }
3935 if let Some(val) = self.checkpoint_store.take() {
3936 secrets.insert(format!("{}__{}", prefix, "CHECKPOINT_STORE"), val);
3937 }
3938 self.tls
3939 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3940 }
3941}
3942
3943impl SecretExtractor for PostgresCdcConfig {
3944 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3945 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3946 if let Some(val) = self.checkpoint_store.take() {
3947 secrets.insert(format!("{}__{}", prefix, "CHECKPOINT_STORE"), val);
3948 }
3949 self.tls
3950 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3951 }
3952}
3953
3954impl SecretExtractor for GrpcConfig {
3955 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3956 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3957 self.tls
3958 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3959 }
3960}
3961
3962impl SecretExtractor for TlsConfig {
3963 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3964 if let Some(val) = self.cert_password.take() {
3965 secrets.insert(format!("{}__{}", prefix, "CERT_PASSWORD"), val);
3966 }
3967 }
3968}
3969
3970impl SecretExtractor for IbmTlsConfig {
3971 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3972 if let Some(val) = self.key_repository_password.take() {
3973 secrets.insert(format!("{}__{}", prefix, "CERT_PASSWORD"), val);
3976 }
3977 }
3978}
3979
3980pub fn extract_config_secrets(config: &mut Config) -> HashMap<String, String> {
3987 let mut secrets = HashMap::new();
3988 for (route_name, route) in config.iter_mut() {
3989 let prefix = sanitize_secret_key(&format!("MQB__{}", route_name));
3990 route.extract_secrets(&prefix, &mut secrets);
3991 }
3992 secrets
3993}
3994
3995#[cfg(test)]
3996mod null_endpoint_tests {
3997 use super::*;
3998
3999 #[test]
4000 fn null_endpoint_json_round_trip() {
4001 let value = serde_json::to_value(Endpoint::null()).expect("serialize");
4002 let back: Endpoint = serde_json::from_value(value).expect("deserialize");
4003 assert!(matches!(back.endpoint_type, EndpointType::Null));
4004 }
4005
4006 #[test]
4008 fn null_endpoint_accepts_string_and_unit_forms() {
4009 for input in ["\"null\"", "null", "{}"] {
4010 let endpoint: Endpoint = serde_json::from_str(input).unwrap_or_else(|e| {
4011 panic!("failed to parse {input}: {e}");
4012 });
4013 assert!(matches!(endpoint.endpoint_type, EndpointType::Null));
4014 }
4015 }
4016
4017 #[test]
4018 fn unknown_endpoint_string_is_rejected() {
4019 let err = serde_json::from_str::<Endpoint>("\"kafka\"").expect_err("should fail");
4020 assert!(
4021 err.to_string().contains("unknown variant"),
4022 "unexpected error: {err}"
4023 );
4024 }
4025
4026 #[test]
4027 fn nested_null_endpoint_json_round_trip() {
4028 let config =
4029 HttpConfig::new("http://localhost:8080").with_stream_response_to(Endpoint::null());
4030 let value = serde_json::to_value(&config).expect("serialize");
4031 let back: HttpConfig = serde_json::from_value(value).expect("deserialize");
4032 let nested = back.stream_response_to.expect("stream_response_to present");
4033 assert!(matches!(nested.endpoint_type, EndpointType::Null));
4034 }
4035
4036 #[test]
4037 fn nested_null_endpoint_yaml_forms() {
4038 for yaml in [
4039 "url: http://localhost:8080\nstream_response_to: \"null\"\n",
4040 "url: http://localhost:8080\nstream_response_to: {}\n",
4041 ] {
4042 let config: HttpConfig = serde_yaml_ng::from_str(yaml)
4043 .unwrap_or_else(|e| panic!("failed to parse {yaml:?}: {e}"));
4044 let nested = config
4045 .stream_response_to
4046 .expect("stream_response_to present");
4047 assert!(matches!(nested.endpoint_type, EndpointType::Null));
4048 }
4049 }
4050
4051 #[test]
4054 fn nested_bare_null_yaml_is_none() {
4055 let config: HttpConfig =
4056 serde_yaml_ng::from_str("url: http://localhost:8080\nstream_response_to: null\n")
4057 .expect("deserialize");
4058 assert!(config.stream_response_to.is_none());
4059 }
4060
4061 #[test]
4062 fn null_endpoint_yaml_round_trip() {
4063 let yaml = serde_yaml_ng::to_string(&Endpoint::null()).expect("serialize");
4064 let back: Endpoint = serde_yaml_ng::from_str(&yaml).expect("deserialize");
4065 assert!(matches!(back.endpoint_type, EndpointType::Null));
4066 }
4067}
4068
4069#[cfg(test)]
4070mod tests {
4071 use super::*;
4072 use config::{Config as ConfigBuilder, Environment};
4073
4074 const TEST_YAML: &str = r#"
4075kafka_to_nats:
4076 concurrency: 10
4077 input:
4078 middlewares:
4079 - deduplication:
4080 sled_path: "/tmp/mq-bridge/dedup_db"
4081 ttl_seconds: 3600
4082 - metrics: {}
4083 - retry:
4084 max_attempts: 5
4085 initial_interval_ms: 200
4086 - random_panic:
4087 mode: nack
4088 - dlq:
4089 endpoint:
4090 nats:
4091 subject: "dlq-subject"
4092 url: "nats://localhost:4222"
4093 kafka:
4094 topic: "input-topic"
4095 url: "localhost:9092"
4096 group_id: "my-consumer-group"
4097 tls:
4098 required: true
4099 ca_file: "/path_to_ca"
4100 cert_file: "/path_to_cert"
4101 key_file: "/path_to_key"
4102 cert_password: "password"
4103 accept_invalid_certs: true
4104 output:
4105 middlewares:
4106 - metrics: {}
4107 - dlq:
4108 endpoint:
4109 file:
4110 path: "error.out"
4111 nats:
4112 subject: "output-subject"
4113 url: "nats://localhost:4222"
4114"#;
4115
4116 fn assert_config_values(config: &Config) {
4117 assert_eq!(config.len(), 1);
4118 let route = config.get("kafka_to_nats").expect("Route should exist");
4119
4120 assert_eq!(route.options.concurrency, 10);
4121
4122 let input = &route.input;
4124 assert_eq!(input.middlewares.len(), 5);
4125
4126 let mut has_dedup = false;
4127 let mut has_metrics = false;
4128 let mut has_dlq = false;
4129 let mut has_retry = false;
4130 let mut has_random_panic = false;
4131 for middleware in &input.middlewares {
4132 match middleware {
4133 Middleware::Deduplication(dedup) => {
4134 assert_eq!(dedup.sled_path.as_deref(), Some("/tmp/mq-bridge/dedup_db"));
4135 assert_eq!(dedup.ttl_seconds, 3600);
4136 has_dedup = true;
4137 }
4138 Middleware::Metrics(_) => {
4139 has_metrics = true;
4140 }
4141 Middleware::Custom { .. } => {}
4142 Middleware::Dlq(dlq) => {
4143 assert!(dlq.endpoint.middlewares.is_empty());
4144 if let EndpointType::Nats(nats_cfg) = &dlq.endpoint.endpoint_type {
4145 assert_eq!(nats_cfg.subject, Some("dlq-subject".to_string()));
4146 assert_eq!(nats_cfg.url, "nats://localhost:4222");
4147 }
4148 has_dlq = true;
4149 }
4150 Middleware::Retry(retry) => {
4151 assert_eq!(retry.max_attempts, 5);
4152 assert_eq!(retry.initial_interval_ms, 200);
4153 has_retry = true;
4154 }
4155 Middleware::RandomPanic(rp) => {
4156 assert!(rp.mode == FaultMode::Nack);
4157 has_random_panic = true;
4158 }
4159 Middleware::Delay(_) => {}
4160 Middleware::WeakJoin(_) => {}
4161 Middleware::Limiter(_) => {}
4162 Middleware::Buffer(_) => {}
4163 Middleware::CookieJar(_) => {}
4164 Middleware::Transform(_) => {}
4165 Middleware::Encryption(_) => {}
4166 Middleware::Compression(_) => {}
4167 }
4168 }
4169
4170 if let EndpointType::Kafka(kafka) = &input.endpoint_type {
4171 assert_eq!(kafka.topic, Some("input-topic".to_string()));
4172 assert_eq!(kafka.url, "localhost:9092");
4173 assert_eq!(kafka.group_id, Some("my-consumer-group".to_string()));
4174 let tls = &kafka.tls;
4175 assert!(tls.required);
4176 assert_eq!(tls.ca_file.as_deref(), Some("/path_to_ca"));
4177 assert!(tls.accept_invalid_certs);
4178 } else {
4179 panic!("Input endpoint should be Kafka");
4180 }
4181 assert!(has_dedup);
4182 assert!(has_metrics);
4183 assert!(has_dlq);
4184 assert!(has_retry);
4185 assert!(has_random_panic);
4186
4187 let output = &route.output;
4189 assert_eq!(output.middlewares.len(), 2);
4190 assert!(matches!(output.middlewares[0], Middleware::Metrics(_)));
4191
4192 if let EndpointType::Nats(nats) = &output.endpoint_type {
4193 assert_eq!(nats.subject, Some("output-subject".to_string()));
4194 assert_eq!(nats.url, "nats://localhost:4222");
4195 } else {
4196 panic!("Output endpoint should be NATS");
4197 }
4198 }
4199
4200 #[test]
4201 fn test_deserialize_from_yaml() {
4202 let result: Result<Config, _> = serde_yaml_ng::from_str(TEST_YAML);
4205 println!("Deserialized from YAML: {:#?}", result);
4206 let config = result.expect("Failed to deserialize TEST_YAML");
4207 assert_config_values(&config);
4208 }
4209
4210 #[test]
4211 fn test_deserialize_from_env() {
4212 unsafe {
4214 std::env::set_var("MQB__KAFKA_TO_NATS__CONCURRENCY", "10");
4215 std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__TOPIC", "input-topic");
4216 std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__URL", "localhost:9092");
4217 std::env::set_var(
4218 "MQB__KAFKA_TO_NATS__INPUT__KAFKA__GROUP_ID",
4219 "my-consumer-group",
4220 );
4221 std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__REQUIRED", "true");
4222 std::env::set_var(
4223 "MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__CA_FILE",
4224 "/path_to_ca",
4225 );
4226 std::env::set_var(
4227 "MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__ACCEPT_INVALID_CERTS",
4228 "true",
4229 );
4230 std::env::set_var(
4231 "MQB__KAFKA_TO_NATS__OUTPUT__NATS__SUBJECT",
4232 "output-subject",
4233 );
4234 std::env::set_var(
4235 "MQB__KAFKA_TO_NATS__OUTPUT__NATS__URL",
4236 "nats://localhost:4222",
4237 );
4238 std::env::set_var(
4239 "MQB__KAFKA_TO_NATS__INPUT__MIDDLEWARES__0__DLQ__ENDPOINT__NATS__SUBJECT",
4240 "dlq-subject",
4241 );
4242 std::env::set_var(
4243 "MQB__KAFKA_TO_NATS__INPUT__MIDDLEWARES__0__DLQ__ENDPOINT__NATS__URL",
4244 "nats://localhost:4222",
4245 );
4246 }
4247
4248 let builder = ConfigBuilder::builder()
4249 .add_source(
4251 Environment::with_prefix("MQB")
4252 .separator("__")
4253 .try_parsing(true),
4254 );
4255
4256 let config: Config = builder
4257 .build()
4258 .expect("Failed to build config")
4259 .try_deserialize()
4260 .expect("Failed to deserialize config");
4261
4262 assert_eq!(config.get("kafka_to_nats").unwrap().options.concurrency, 10);
4264 if let EndpointType::Kafka(k) = &config.get("kafka_to_nats").unwrap().input.endpoint_type {
4265 assert_eq!(k.topic, Some("input-topic".to_string()));
4266 assert!(k.tls.required);
4267 } else {
4268 panic!("Expected Kafka endpoint");
4269 }
4270
4271 let input = &config.get("kafka_to_nats").unwrap().input;
4272 assert_eq!(input.middlewares.len(), 1);
4273 if let Middleware::Dlq(_) = &input.middlewares[0] {
4274 } else {
4276 panic!("Expected DLQ middleware");
4277 }
4278 }
4279
4280 #[test]
4281 fn test_extract_secrets() {
4282 let mut config = Config::new();
4283 let mut route = Route::default();
4284
4285 let mut kafka_config = KafkaConfig::new("kafka://user:pass@localhost:9092");
4287 kafka_config.username = Some("user".to_string());
4288 kafka_config.password = Some("pass".to_string());
4289 kafka_config.tls.cert_password = Some("certpass".to_string());
4290
4291 route.input = Endpoint {
4292 endpoint_type: EndpointType::Kafka(kafka_config),
4293 middlewares: vec![],
4294 handler: None,
4295 };
4296
4297 let mut http_config = HttpConfig::new("http://httpuser:httppass@localhost");
4299 http_config.basic_auth = Some(("httpuser".to_string(), "httppass".to_string()));
4300 http_config
4301 .custom_headers
4302 .insert("X-API-Key".to_string(), "http-api-key".to_string());
4303 http_config.custom_headers.insert(
4304 "X-Access-Token".to_string(),
4305 "http-access-token".to_string(),
4306 );
4307 http_config.custom_headers.insert(
4308 "X-Authentication".to_string(),
4309 "http-authentication".to_string(),
4310 );
4311 http_config.custom_headers.insert(
4312 "Authorization".to_string(),
4313 "Bearer secret-token".to_string(),
4314 );
4315 http_config
4316 .custom_headers
4317 .insert("X-Trace-Id".to_string(), "trace-value".to_string());
4318
4319 route.output = Endpoint {
4320 endpoint_type: EndpointType::Http(http_config),
4321 middlewares: vec![],
4322 handler: None,
4323 };
4324
4325 config.insert("test_route".to_string(), route);
4326
4327 let secrets = extract_config_secrets(&mut config);
4328
4329 assert_eq!(
4331 secrets
4332 .get("MQB__TEST_ROUTE__INPUT__KAFKA__URL")
4333 .map(|s| s.as_str()),
4334 Some("kafka://user:pass@localhost:9092")
4335 );
4336 assert_eq!(
4337 secrets
4338 .get("MQB__TEST_ROUTE__INPUT__KAFKA__USERNAME")
4339 .map(|s| s.as_str()),
4340 Some("user")
4341 );
4342 assert_eq!(
4343 secrets
4344 .get("MQB__TEST_ROUTE__INPUT__KAFKA__PASSWORD")
4345 .map(|s| s.as_str()),
4346 Some("pass")
4347 );
4348 assert_eq!(
4349 secrets
4350 .get("MQB__TEST_ROUTE__INPUT__KAFKA__TLS__CERT_PASSWORD")
4351 .map(|s| s.as_str()),
4352 Some("certpass")
4353 );
4354 assert_eq!(
4355 secrets
4356 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__URL")
4357 .map(|s| s.as_str()),
4358 Some("http://httpuser:httppass@localhost")
4359 );
4360 assert_eq!(
4361 secrets
4362 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__BASIC_AUTH__0")
4363 .map(|s| s.as_str()),
4364 Some("httpuser")
4365 );
4366 assert_eq!(
4367 secrets
4368 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__BASIC_AUTH__1")
4369 .map(|s| s.as_str()),
4370 Some("httppass")
4371 );
4372 assert_eq!(
4373 secrets
4374 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_API_KEY")
4375 .map(|s| s.as_str()),
4376 Some("http-api-key")
4377 );
4378 assert_eq!(
4379 secrets
4380 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_ACCESS_TOKEN")
4381 .map(|s| s.as_str()),
4382 Some("http-access-token")
4383 );
4384 assert_eq!(
4385 secrets
4386 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_AUTHENTICATION")
4387 .map(|s| s.as_str()),
4388 Some("http-authentication")
4389 );
4390 assert_eq!(
4391 secrets
4392 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__AUTHORIZATION")
4393 .map(|s| s.as_str()),
4394 Some("Bearer secret-token")
4395 );
4396
4397 let route = config.get("test_route").unwrap();
4399 if let EndpointType::Kafka(k) = &route.input.endpoint_type {
4400 assert!(k.url.is_empty());
4401 assert!(k.username.is_none());
4402 assert!(k.password.is_none());
4403 assert!(k.tls.cert_password.is_none());
4404 }
4405 if let EndpointType::Http(h) = &route.output.endpoint_type {
4406 assert!(h.url.is_empty());
4407 assert!(h.basic_auth.is_none());
4408 assert!(!h.custom_headers.contains_key("X-API-Key"));
4409 assert!(!h.custom_headers.contains_key("X-Access-Token"));
4410 assert!(!h.custom_headers.contains_key("X-Authentication"));
4411 assert!(!h.custom_headers.contains_key("Authorization"));
4412 assert_eq!(
4413 h.custom_headers.get("X-Trace-Id").map(|s| s.as_str()),
4414 Some("trace-value")
4415 );
4416 }
4417 }
4418
4419 #[test]
4420 fn test_extract_sensitive_url_only_strips_authority_credentials() {
4421 let mut config = Config::new();
4422 let path_at_route = Route {
4423 output: Endpoint {
4424 endpoint_type: EndpointType::Http(HttpConfig::new(
4425 "https://example.com/path/user@example.com?email=a@b.test",
4426 )),
4427 middlewares: vec![],
4428 handler: None,
4429 },
4430 ..Default::default()
4431 };
4432 config.insert("path_at_route".to_string(), path_at_route);
4433
4434 let credential_route = Route {
4435 output: Endpoint {
4436 endpoint_type: EndpointType::Http(HttpConfig::new(
4437 "https://user:pass@example.com/path",
4438 )),
4439 middlewares: vec![],
4440 handler: None,
4441 },
4442 ..Default::default()
4443 };
4444 config.insert("credential_route".to_string(), credential_route);
4445
4446 let query_at_route = Route {
4447 output: Endpoint {
4448 endpoint_type: EndpointType::Http(HttpConfig::new(
4449 "https://example.com?next=a@b.test",
4450 )),
4451 middlewares: vec![],
4452 handler: None,
4453 },
4454 ..Default::default()
4455 };
4456 config.insert("query_at_route".to_string(), query_at_route);
4457
4458 let fragment_at_route = Route {
4459 output: Endpoint {
4460 endpoint_type: EndpointType::Http(HttpConfig::new(
4461 "https://example.com#user@example.com",
4462 )),
4463 middlewares: vec![],
4464 handler: None,
4465 },
4466 ..Default::default()
4467 };
4468 config.insert("fragment_at_route".to_string(), fragment_at_route);
4469
4470 let secrets = extract_config_secrets(&mut config);
4471
4472 if let EndpointType::Http(http) = &config.get("path_at_route").unwrap().output.endpoint_type
4473 {
4474 assert_eq!(
4475 http.url,
4476 "https://example.com/path/user@example.com?email=a@b.test"
4477 );
4478 }
4479 if let EndpointType::Http(http) =
4480 &config.get("query_at_route").unwrap().output.endpoint_type
4481 {
4482 assert_eq!(http.url, "https://example.com?next=a@b.test");
4483 }
4484 if let EndpointType::Http(http) = &config
4485 .get("fragment_at_route")
4486 .unwrap()
4487 .output
4488 .endpoint_type
4489 {
4490 assert_eq!(http.url, "https://example.com#user@example.com");
4491 }
4492 if let EndpointType::Http(http) =
4493 &config.get("credential_route").unwrap().output.endpoint_type
4494 {
4495 assert!(http.url.is_empty());
4496 }
4497 assert_eq!(
4498 secrets
4499 .get("MQB__CREDENTIAL_ROUTE__OUTPUT__HTTP__URL")
4500 .map(String::as_str),
4501 Some("https://user:pass@example.com/path")
4502 );
4503 assert!(!secrets.contains_key("MQB__PATH_AT_ROUTE__OUTPUT__HTTP__URL"));
4504 assert!(!secrets.contains_key("MQB__QUERY_AT_ROUTE__OUTPUT__HTTP__URL"));
4505 assert!(!secrets.contains_key("MQB__FRAGMENT_AT_ROUTE__OUTPUT__HTTP__URL"));
4506 }
4507
4508 #[test]
4509 fn test_memory_config_requires_topic_or_url() {
4510 let err = serde_yaml_ng::from_str::<MemoryConfig>("{}").unwrap_err();
4511 assert!(err
4512 .to_string()
4513 .contains("MemoryConfig: 'topic' (or 'url' alias) is required."));
4514 }
4515
4516 #[test]
4517 fn test_file_config_inference() {
4518 let yaml = r#"
4519mode: group_subscribe
4520path: "/tmp/test"
4521group_id: "my_group"
4522"#;
4523 let config: FileConfig = serde_yaml_ng::from_str(yaml).unwrap();
4524 match config.mode {
4525 Some(FileConsumerMode::GroupSubscribe { group_id, .. }) => {
4526 assert_eq!(group_id, "my_group")
4527 }
4528 _ => panic!("Expected GroupSubscribe"),
4529 }
4530
4531 let yaml_queue = r#"
4532mode: consume
4533path: "/tmp/test"
4534"#;
4535 let config_queue: FileConfig = serde_yaml_ng::from_str(yaml_queue).unwrap();
4536 match config_queue.mode {
4537 Some(FileConsumerMode::Consume { delete }) => assert!(!delete),
4538 _ => panic!("Expected Consume"),
4539 }
4540 }
4541}
4542
4543#[cfg(all(test, feature = "schema"))]
4544mod schema_tests {
4545 use super::*;
4546
4547 #[test]
4548 fn generate_json_schema() {
4549 let schema = schemars::schema_for!(Config);
4550 let schema_json = serde_json::to_string_pretty(&schema).unwrap();
4551
4552 let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
4553 path.push("mq-bridge.schema.json");
4554 std::fs::write(path, schema_json).expect("Failed to write schema file");
4555 }
4556}