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#[cfg_attr(feature = "schema", schemars(transform = endpoint_schema_transform))]
318#[serde(deny_unknown_fields)]
319pub struct Endpoint {
320 #[serde(default)]
322 pub middlewares: Vec<Middleware>,
323
324 #[serde(flatten)]
326 pub endpoint_type: EndpointType,
327
328 #[serde(skip_serializing)]
329 #[cfg_attr(feature = "schema", schemars(skip))]
330 pub handler: Option<Arc<dyn Handler>>,
332}
333
334impl std::fmt::Debug for Endpoint {
335 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336 f.debug_struct("Endpoint")
337 .field("middlewares", &self.middlewares)
338 .field("endpoint_type", &self.endpoint_type)
339 .field(
340 "handler",
341 &if self.handler.is_some() {
342 "Some(<Handler>)"
343 } else {
344 "None"
345 },
346 )
347 .finish()
348 }
349}
350
351impl<'de> Deserialize<'de> for Endpoint {
352 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
353 where
354 D: Deserializer<'de>,
355 {
356 struct EndpointVisitor;
357
358 impl<'de> Visitor<'de> for EndpointVisitor {
359 type Value = Endpoint;
360
361 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
362 formatter.write_str("a map representing an endpoint, the string \"null\", or null")
363 }
364
365 fn visit_unit<E>(self) -> Result<Self::Value, E>
366 where
367 E: serde::de::Error,
368 {
369 Ok(Endpoint::new(EndpointType::Null))
370 }
371
372 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
375 where
376 E: serde::de::Error,
377 {
378 if value == "null" {
379 Ok(Endpoint::new(EndpointType::Null))
380 } else {
381 Err(serde::de::Error::unknown_variant(value, &["null"]))
382 }
383 }
384
385 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
386 where
387 E: serde::de::Error,
388 {
389 self.visit_str(&value)
390 }
391
392 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
393 where
394 A: MapAccess<'de>,
395 {
396 let mut temp_map = serde_json::Map::new();
399 let mut middlewares_val = None;
400
401 while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
402 if key == "middlewares" {
403 middlewares_val = Some(value);
404 } else {
405 temp_map.insert(key, value);
406 }
407 }
408
409 let temp_val = serde_json::Value::Object(temp_map);
411 let endpoint_type: EndpointType = match serde_json::from_value(temp_val.clone()) {
412 Ok(et) => et,
413 Err(original_err) => {
414 if let serde_json::Value::Object(map) = &temp_val {
415 if map.len() == 1 {
416 let (name, config) = map.iter().next().unwrap();
417 if is_known_endpoint_name(name) {
418 return Err(serde::de::Error::custom(original_err));
419 }
420 trace!("Falling back to Custom endpoint for key: {}", name);
421 EndpointType::Custom {
422 name: name.clone(),
423 config: config.clone(),
424 }
425 } else if map.is_empty() {
426 EndpointType::Null
427 } else {
428 return Err(serde::de::Error::custom(
429 "Invalid endpoint configuration: multiple keys found or unknown endpoint type",
430 ));
431 }
432 } else {
433 return Err(serde::de::Error::custom("Invalid endpoint configuration"));
434 }
435 }
436 };
437
438 let middlewares = match middlewares_val {
440 Some(val) => {
441 deserialize_middlewares_from_value(val).map_err(serde::de::Error::custom)?
442 }
443 None => Vec::new(),
444 };
445
446 Ok(Endpoint {
447 middlewares,
448 endpoint_type,
449 handler: None,
450 })
451 }
452 }
453
454 deserializer.deserialize_any(EndpointVisitor)
455 }
456}
457
458fn is_known_middleware_name(name: &str) -> bool {
459 matches!(
460 name,
461 "deduplication"
462 | "metrics"
463 | "dlq"
464 | "retry"
465 | "random_panic"
466 | "delay"
467 | "weak_join"
468 | "limiter"
469 | "buffer"
470 | "cookie_jar"
471 | "custom"
472 )
473}
474
475fn deserialize_middlewares_from_value(value: serde_json::Value) -> anyhow::Result<Vec<Middleware>> {
479 let arr = match value {
480 serde_json::Value::Array(arr) => arr,
481 serde_json::Value::Object(map) => {
482 let mut middlewares: Vec<_> = map
483 .into_iter()
484 .filter_map(|(key, value)| key.parse::<usize>().ok().map(|index| (index, value)))
487 .collect();
488 middlewares.sort_by_key(|(index, _)| *index);
489
490 middlewares.into_iter().map(|(_, value)| value).collect()
491 }
492 _ => return Err(anyhow::anyhow!("Expected an array or object")),
493 };
494
495 let mut middlewares = Vec::new();
496 for item in arr {
497 let known_name = if let serde_json::Value::Object(map) = &item {
499 if map.len() == 1 {
500 let (name, _) = map.iter().next().unwrap();
501 if is_known_middleware_name(name) {
502 Some(name.clone())
503 } else {
504 None
505 }
506 } else {
507 None
508 }
509 } else {
510 None
511 };
512
513 if let Some(name) = known_name {
514 match serde_json::from_value::<Middleware>(item.clone()) {
515 Ok(m) => middlewares.push(m),
516 Err(e) => {
517 return Err(anyhow::anyhow!(
518 "Failed to deserialize known middleware '{}': {}",
519 name,
520 e
521 ))
522 }
523 }
524 } else if let Ok(m) = serde_json::from_value::<Middleware>(item.clone()) {
525 middlewares.push(m);
526 } else if let serde_json::Value::Object(map) = &item {
527 if map.len() == 1 {
528 let (name, config) = map.iter().next().unwrap();
529 middlewares.push(Middleware::Custom {
530 name: name.clone(),
531 config: config.clone(),
532 });
533 } else {
534 return Err(anyhow::anyhow!(
535 "Invalid middleware configuration: {:?}",
536 item
537 ));
538 }
539 } else {
540 return Err(anyhow::anyhow!(
541 "Invalid middleware configuration: {:?}",
542 item
543 ));
544 }
545 }
546 Ok(middlewares)
547}
548
549#[derive(Debug, Clone, Default)]
579pub struct StaticConfig {
580 pub body: String,
582 pub raw: bool,
584 pub metadata: std::collections::HashMap<String, String>,
586}
587
588#[cfg(feature = "schema")]
592impl schemars::JsonSchema for StaticConfig {
593 fn schema_name() -> std::borrow::Cow<'static, str> {
594 "StaticConfig".into()
595 }
596
597 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
598 schemars::json_schema!({
599 "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.",
600 "oneOf": [
601 {
602 "type": "string",
603 "description": "The response body, JSON-encoded as a string."
604 },
605 {
606 "type": "object",
607 "properties": {
608 "body": {
609 "type": "string",
610 "description": "The static response body."
611 },
612 "raw": {
613 "type": "boolean",
614 "description": "Send the body verbatim instead of JSON-encoding it as a string.",
615 "default": false
616 },
617 "metadata": {
618 "type": "object",
619 "description": "Extra metadata entries attached to the produced message.",
620 "additionalProperties": { "type": "string" }
621 }
622 },
623 "required": ["body"],
624 "additionalProperties": false
625 }
626 ]
627 })
628 }
629}
630
631impl Serialize for StaticConfig {
632 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
633 where
634 S: serde::Serializer,
635 {
636 if !self.raw && self.metadata.is_empty() {
640 return serializer.serialize_str(&self.body);
641 }
642 use serde::ser::SerializeStruct;
643 let mut state = serializer.serialize_struct("StaticConfig", 3)?;
644 state.serialize_field("body", &self.body)?;
645 state.serialize_field("raw", &self.raw)?;
646 state.serialize_field("metadata", &self.metadata)?;
647 state.end()
648 }
649}
650
651impl From<String> for StaticConfig {
652 fn from(body: String) -> Self {
653 StaticConfig {
654 body,
655 raw: false,
656 metadata: std::collections::HashMap::new(),
657 }
658 }
659}
660
661impl From<&str> for StaticConfig {
662 fn from(body: &str) -> Self {
663 StaticConfig::from(body.to_string())
664 }
665}
666
667impl<'de> Deserialize<'de> for StaticConfig {
668 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
669 where
670 D: serde::Deserializer<'de>,
671 {
672 #[derive(Deserialize)]
673 #[serde(untagged)]
674 enum Repr {
675 Str(String),
676 Map {
677 body: String,
678 #[serde(default)]
679 raw: bool,
680 #[serde(default)]
681 metadata: std::collections::HashMap<String, String>,
682 },
683 }
684 Ok(match Repr::deserialize(deserializer)? {
685 Repr::Str(body) => StaticConfig {
686 body,
687 raw: false,
688 metadata: std::collections::HashMap::new(),
689 },
690 Repr::Map {
691 body,
692 raw,
693 metadata,
694 } => StaticConfig {
695 body,
696 raw,
697 metadata,
698 },
699 })
700 }
701}
702
703#[derive(Debug, Deserialize, Serialize, Clone, Default)]
725#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
726#[serde(rename_all = "lowercase")]
727pub enum EndpointType {
728 Aws(AwsConfig),
729 Kafka(KafkaConfig),
730 Nats(NatsConfig),
731 File(FileConfig),
732 #[serde(rename = "object_store", alias = "objectstore", alias = "s3")]
733 ObjectStore(ObjectStoreConfig),
734 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
735 Static(StaticConfig),
736 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
737 Ref(String),
738 Memory(MemoryConfig),
739 Sled(SledConfig),
740 Amqp(AmqpConfig),
741 MongoDb(MongoDbConfig),
742 Mqtt(MqttConfig),
743 Http(HttpConfig),
744 WebSocket(WebSocketConfig),
745 IbmMq(IbmMqConfig),
746 ZeroMq(ZeroMqConfig),
747 #[serde(rename = "redis_streams", alias = "redis")]
748 RedisStreams(RedisStreamsConfig),
749 Grpc(GrpcConfig),
750 Sqlx(SqlxConfig),
751 #[serde(rename = "clickhouse", alias = "click_house")]
752 ClickHouse(ClickHouseConfig),
753 #[serde(rename = "postgres_cdc", alias = "postgres-cdc")]
754 PostgresCdc(PostgresCdcConfig),
755 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
756 Fanout(Vec<Endpoint>),
757 #[serde(rename = "stream_buffer")]
758 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
759 StreamBuffer(StreamBufferConfig),
760 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
761 Switch(SwitchConfig),
762 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
763 Response(ResponseConfig),
764 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
765 Reader(Box<Endpoint>),
766 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
767 Request(RequestForwardConfig),
768 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
769 Custom {
770 name: String,
771 config: serde_json::Value,
772 },
773 #[default]
774 #[cfg_attr(feature = "schema", schemars(extend("format" = "structural_endpoint")))]
775 Null,
776}
777
778impl EndpointType {
779 pub fn name(&self) -> &'static str {
780 match self {
781 EndpointType::Aws(_) => "aws",
782 EndpointType::Kafka(_) => "kafka",
783 EndpointType::Nats(_) => "nats",
784 EndpointType::File(_) => "file",
785 EndpointType::ObjectStore(_) => "object_store",
786 EndpointType::Static(_) => "static",
787 EndpointType::Ref(_) => "ref",
788 EndpointType::Memory(_) => "memory",
789 EndpointType::Sled(_) => "sled",
790 EndpointType::Amqp(_) => "amqp",
791 EndpointType::MongoDb(_) => "mongodb",
792 EndpointType::Mqtt(_) => "mqtt",
793 EndpointType::Http(_) => "http",
794 EndpointType::WebSocket(_) => "websocket",
795 EndpointType::IbmMq(_) => "ibmmq",
796 EndpointType::ZeroMq(_) => "zeromq",
797 EndpointType::RedisStreams(_) => "redis_streams",
798 EndpointType::Grpc(_) => "grpc",
799 EndpointType::Sqlx(_) => "sqlx",
800 EndpointType::ClickHouse(_) => "clickhouse",
801 EndpointType::PostgresCdc(_) => "postgres_cdc",
802 EndpointType::Fanout(_) => "fanout",
803 EndpointType::StreamBuffer(_) => "stream_buffer",
804 EndpointType::Switch(_) => "switch",
805 EndpointType::Response(_) => "response",
806 EndpointType::Reader(_) => "reader",
807 EndpointType::Request(_) => "request",
808 EndpointType::Custom { .. } => "custom",
809 EndpointType::Null => "null",
810 }
811 }
812
813 pub fn is_core(&self) -> bool {
814 matches!(
815 self,
816 EndpointType::File(_)
817 | EndpointType::Static(_)
818 | EndpointType::Ref(_)
819 | EndpointType::Memory(_)
820 | EndpointType::Fanout(_)
821 | EndpointType::StreamBuffer(_)
822 | EndpointType::Switch(_)
823 | EndpointType::Response(_)
824 | EndpointType::Reader(_)
825 | EndpointType::Request(_)
826 | EndpointType::Custom { .. }
827 | EndpointType::Null
828 )
829 }
830}
831
832#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
834#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
835#[serde(rename_all = "snake_case")]
836pub enum CipherKind {
837 #[default]
839 Xchacha20poly1305,
840 Aes256gcm,
842}
843
844#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
848#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
849#[serde(deny_unknown_fields)]
850pub struct EncryptionConfig {
851 #[serde(default)]
853 pub cipher: CipherKind,
854 #[serde(default = "default_encryption_key_id")]
856 pub key_id: String,
857 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
859 pub key: String,
860 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
862 #[cfg_attr(feature = "schema", schemars(extend("format" = "password")))]
863 pub decrypt_keys: HashMap<String, String>,
864}
865
866fn default_encryption_key_id() -> String {
867 "default".to_string()
868}
869
870impl Default for EncryptionConfig {
871 fn default() -> Self {
872 Self {
873 cipher: CipherKind::default(),
874 key_id: default_encryption_key_id(),
875 key: String::new(),
876 decrypt_keys: HashMap::new(),
877 }
878 }
879}
880
881#[derive(Debug, Deserialize, Serialize, Clone)]
883#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
884#[serde(rename_all = "snake_case")]
885pub enum Middleware {
886 Deduplication(DeduplicationMiddleware),
887 Metrics(MetricsMiddleware),
888 Dlq(Box<DeadLetterQueueMiddleware>),
889 Retry(RetryMiddleware),
890 RandomPanic(RandomPanicMiddleware),
891 Delay(DelayMiddleware),
892 WeakJoin(WeakJoinMiddleware),
893 Limiter(LimiterMiddleware),
894 Buffer(BufferMiddleware),
895 CookieJar(CookieJarMiddleware),
896 Transform(TransformMiddleware),
897 Encryption(EncryptionConfig),
898 Compression(CompressionMiddleware),
899 Custom {
900 name: String,
901 config: serde_json::Value,
902 },
903}
904
905#[derive(Debug, Deserialize, Serialize, Clone)]
910#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
911#[serde(deny_unknown_fields)]
912pub struct DeduplicationMiddleware {
913 #[serde(default)]
915 pub store: Option<String>,
916 #[serde(default)]
918 pub sled_path: Option<String>,
919 pub ttl_seconds: u64,
921 #[serde(default)]
923 pub key: Option<String>,
924}
925
926#[derive(Debug, Deserialize, Serialize, Clone)]
934#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
935#[serde(deny_unknown_fields)]
936pub struct MetricsMiddleware {}
937
938#[derive(Debug, Deserialize, Serialize, Clone, Default)]
945#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
946#[serde(deny_unknown_fields)]
947pub struct DeadLetterQueueMiddleware {
948 pub endpoint: Endpoint,
950}
951
952#[derive(Debug, Deserialize, Serialize, Clone, Default)]
957#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
958#[serde(deny_unknown_fields)]
959pub struct RetryMiddleware {
960 #[serde(default = "default_retry_attempts")]
962 pub max_attempts: usize,
963 #[serde(default = "default_initial_interval_ms")]
965 pub initial_interval_ms: u64,
966 #[serde(default = "default_max_interval_ms")]
968 pub max_interval_ms: u64,
969 #[serde(default = "default_multiplier")]
971 pub multiplier: f64,
972}
973
974#[derive(Debug, Deserialize, Serialize, Clone)]
979#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
980#[serde(deny_unknown_fields)]
981pub struct DelayMiddleware {
982 pub delay_ms: u64,
984}
985
986#[derive(Debug, Deserialize, Serialize, Clone)]
992#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
993#[serde(deny_unknown_fields)]
994pub struct LimiterMiddleware {
995 pub messages_per_second: f64,
997}
998
999#[derive(Debug, Deserialize, Serialize, Clone)]
1004#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1005#[serde(deny_unknown_fields)]
1006pub struct BufferMiddleware {
1007 pub max_messages: usize,
1009 pub max_delay_ms: u64,
1011}
1012
1013#[derive(Debug, Deserialize, Serialize, Clone)]
1021#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1022#[serde(deny_unknown_fields)]
1023pub struct CookieJarMiddleware {
1024 #[serde(default)]
1027 pub shared_scope: Option<String>,
1028 #[serde(default = "default_cookie_metadata_key")]
1030 pub cookie_metadata_key: String,
1031 #[serde(default = "default_set_cookie_metadata_key")]
1033 pub set_cookie_metadata_key: String,
1034 #[serde(default)]
1036 pub capture_metadata_keys: Vec<String>,
1037 #[serde(default)]
1042 pub export_metadata_prefix: Option<String>,
1043 #[serde(default)]
1048 pub inject_metadata: HashMap<String, String>,
1049}
1050
1051impl Default for CookieJarMiddleware {
1052 fn default() -> Self {
1053 Self {
1054 shared_scope: None,
1055 cookie_metadata_key: default_cookie_metadata_key(),
1056 set_cookie_metadata_key: default_set_cookie_metadata_key(),
1057 capture_metadata_keys: Vec::new(),
1058 export_metadata_prefix: None,
1059 inject_metadata: HashMap::new(),
1060 }
1061 }
1062}
1063
1064#[derive(Debug, Deserialize, Serialize, Clone)]
1070#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1071#[serde(deny_unknown_fields)]
1072pub struct WeakJoinMiddleware {
1073 pub group_by: String,
1075 pub expected_count: usize,
1077 pub timeout_ms: u64,
1079 #[serde(default)]
1081 pub branch_by: Option<String>,
1082 #[serde(default)]
1084 pub required: Vec<String>,
1085 #[serde(default)]
1087 pub on_timeout: WeakJoinTimeout,
1088}
1089
1090#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
1092#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1093#[serde(rename_all = "snake_case")]
1094pub enum WeakJoinTimeout {
1095 #[default]
1097 Fire,
1098 Discard,
1100}
1101
1102#[derive(Debug, Deserialize, Serialize, Clone)]
1115#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1116#[cfg_attr(feature = "schema", schemars(transform = transform_middleware_schema_transform))]
1117#[serde(deny_unknown_fields)]
1118pub struct TransformMiddleware {
1119 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1121 pub mapping: HashMap<String, MappingRule>,
1122 #[serde(default, skip_serializing_if = "Option::is_none")]
1124 pub schema: Option<serde_json::Value>,
1125 #[serde(default, skip_serializing_if = "Option::is_none")]
1127 pub schema_file: Option<String>,
1128 #[serde(default = "default_true")]
1130 pub coerce: bool,
1131 #[serde(default = "default_true")]
1133 pub apply_defaults: bool,
1134 #[serde(default)]
1136 pub on_error: TransformErrorPolicy,
1137}
1138
1139impl Default for TransformMiddleware {
1144 fn default() -> Self {
1145 Self {
1146 mapping: HashMap::new(),
1147 schema: None,
1148 schema_file: None,
1149 coerce: default_true(),
1150 apply_defaults: default_true(),
1151 on_error: TransformErrorPolicy::default(),
1152 }
1153 }
1154}
1155
1156#[derive(Debug, Deserialize, Serialize, Clone)]
1161#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1162#[serde(untagged)]
1163pub enum MappingRule {
1164 Path(String),
1166 Detailed(DetailedMappingRule),
1168}
1169
1170#[derive(Debug, Deserialize, Serialize, Clone)]
1172#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1173#[serde(deny_unknown_fields)]
1174pub struct DetailedMappingRule {
1175 pub path: String,
1177 #[serde(default, skip_serializing_if = "Option::is_none")]
1179 pub default: Option<serde_json::Value>,
1180 #[serde(default)]
1182 pub required: bool,
1183}
1184
1185impl MappingRule {
1186 pub fn path(&self) -> &str {
1188 match self {
1189 MappingRule::Path(p) => p,
1190 MappingRule::Detailed(d) => &d.path,
1191 }
1192 }
1193}
1194
1195#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
1197#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1198#[serde(rename_all = "snake_case")]
1199pub enum TransformErrorPolicy {
1200 #[default]
1202 Reject,
1203 PassThrough,
1205}
1206
1207#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1209#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1210#[serde(rename_all = "snake_case")]
1211pub enum FaultMode {
1212 #[default]
1214 Panic,
1215 Disconnect,
1217 Timeout,
1219 JsonFormatError,
1221 Nack,
1223}
1224
1225impl std::fmt::Display for FaultMode {
1226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1227 match self {
1228 FaultMode::Panic => write!(f, "panic"),
1229 FaultMode::Disconnect => write!(f, "disconnect"),
1230 FaultMode::Timeout => write!(f, "timeout"),
1231 FaultMode::JsonFormatError => write!(f, "json_format_error"),
1232 FaultMode::Nack => write!(f, "nack"),
1233 }
1234 }
1235}
1236
1237#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1250#[serde(deny_unknown_fields)]
1251#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1252pub struct RandomPanicMiddleware {
1253 #[serde(default)]
1255 pub mode: FaultMode,
1256 #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
1258 #[serde(default)]
1259 pub trigger_on_message: Option<usize>,
1260 #[serde(default = "default_true")]
1262 pub enabled: bool,
1263 #[serde(skip, default = "default_atomic_usize_arc")]
1264 #[cfg_attr(feature = "schema", schemars(skip))]
1265 pub message_count: Arc<AtomicUsize>,
1266}
1267
1268fn default_true() -> bool {
1269 true
1270}
1271
1272fn default_atomic_usize_arc() -> Arc<AtomicUsize> {
1273 Arc::new(AtomicUsize::new(0))
1274}
1275
1276fn deserialize_null_as_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
1277where
1278 D: Deserializer<'de>,
1279{
1280 let opt = Option::<bool>::deserialize(deserializer)?;
1281 Ok(opt.unwrap_or(false))
1282}
1283
1284#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1286#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1287#[serde(deny_unknown_fields)]
1288pub struct AwsConfig {
1289 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1291 pub queue_url: Option<String>,
1292 pub topic_arn: Option<String>,
1294 pub region: Option<String>,
1296 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1298 pub endpoint_url: Option<String>,
1299 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1301 pub access_key: Option<String>,
1302 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1304 pub secret_key: Option<String>,
1305 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1307 pub session_token: Option<String>,
1308 #[cfg_attr(feature = "schema", schemars(range(min = 1, max = 10)))]
1310 pub max_messages: Option<i32>,
1311 #[cfg_attr(feature = "schema", schemars(range(min = 0, max = 20)))]
1313 pub wait_time_seconds: Option<i32>,
1314 #[serde(default)]
1316 pub binary_payload_mode: bool,
1317}
1318
1319impl AwsConfig {
1320 pub fn new() -> Self {
1322 Self::default()
1323 }
1324
1325 pub fn with_queue_url(mut self, queue_url: impl Into<String>) -> Self {
1326 self.queue_url = Some(queue_url.into());
1327 self
1328 }
1329
1330 pub fn with_topic_arn(mut self, topic_arn: impl Into<String>) -> Self {
1331 self.topic_arn = Some(topic_arn.into());
1332 self
1333 }
1334
1335 pub fn with_region(mut self, region: impl Into<String>) -> Self {
1336 self.region = Some(region.into());
1337 self
1338 }
1339
1340 pub fn with_endpoint_url(mut self, endpoint_url: impl Into<String>) -> Self {
1341 self.endpoint_url = Some(endpoint_url.into());
1342 self
1343 }
1344
1345 pub fn with_credentials(
1346 mut self,
1347 access_key: impl Into<String>,
1348 secret_key: impl Into<String>,
1349 ) -> Self {
1350 self.access_key = Some(access_key.into());
1351 self.secret_key = Some(secret_key.into());
1352 self
1353 }
1354}
1355
1356#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1360#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1361#[serde(deny_unknown_fields)]
1362pub struct KafkaConfig {
1363 #[serde(alias = "brokers")]
1365 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1366 pub url: String,
1367 pub topic: Option<String>,
1369 pub username: Option<String>,
1371 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1373 pub password: Option<String>,
1374 #[serde(default)]
1376 pub tls: TlsConfig,
1377 pub group_id: Option<String>,
1380 #[serde(default)]
1382 pub delayed_ack: bool,
1383 #[serde(default)]
1385 pub producer_options: Option<Vec<(String, String)>>,
1386 #[serde(default)]
1388 pub consumer_options: Option<Vec<(String, String)>>,
1389 #[serde(default)]
1391 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
1392 pub shared: Option<bool>,
1393 #[serde(default)]
1397 #[cfg_attr(
1398 feature = "schema",
1399 schemars(default = "default_kafka_partitions_schema", range(min = 1))
1400 )]
1401 pub partitions: Option<i32>,
1402 #[serde(default)]
1406 pub partition_key: Option<String>,
1407}
1408
1409impl KafkaConfig {
1410 pub fn new(url: impl Into<String>) -> Self {
1412 Self {
1413 url: url.into(),
1414 ..Default::default()
1415 }
1416 }
1417
1418 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
1419 self.topic = Some(topic.into());
1420 self
1421 }
1422
1423 pub fn with_group_id(mut self, group_id: impl Into<String>) -> Self {
1424 self.group_id = Some(group_id.into());
1425 self
1426 }
1427
1428 pub fn with_credentials(
1429 mut self,
1430 username: impl Into<String>,
1431 password: impl Into<String>,
1432 ) -> Self {
1433 self.username = Some(username.into());
1434 self.password = Some(password.into());
1435 self
1436 }
1437
1438 pub fn with_producer_option(
1439 mut self,
1440 key: impl Into<String>,
1441 value: impl Into<String>,
1442 ) -> Self {
1443 let options = self.producer_options.get_or_insert_with(Vec::new);
1444 options.push((key.into(), value.into()));
1445 self
1446 }
1447
1448 pub fn with_consumer_option(
1449 mut self,
1450 key: impl Into<String>,
1451 value: impl Into<String>,
1452 ) -> Self {
1453 let options = self.consumer_options.get_or_insert_with(Vec::new);
1454 options.push((key.into(), value.into()));
1455 self
1456 }
1457}
1458
1459#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1463#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1464#[serde(deny_unknown_fields)]
1465pub struct SledConfig {
1466 pub path: String,
1468 pub tree: Option<String>,
1470 #[serde(default)]
1472 pub read_from_start: bool,
1473 #[serde(default)]
1475 pub delete_after_read: bool,
1476}
1477
1478impl SledConfig {
1479 pub fn new(path: impl Into<String>) -> Self {
1481 Self {
1482 path: path.into(),
1483 ..Default::default()
1484 }
1485 }
1486
1487 pub fn with_tree(mut self, tree: impl Into<String>) -> Self {
1488 self.tree = Some(tree.into());
1489 self
1490 }
1491
1492 pub fn with_read_from_start(mut self, read_from_start: bool) -> Self {
1493 self.read_from_start = read_from_start;
1494 self
1495 }
1496}
1497
1498#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
1500#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1501#[serde(rename_all = "snake_case")]
1502pub enum FileFormat {
1503 #[default]
1505 Normal,
1506 Json,
1508 Text,
1510 Raw,
1512 Csv,
1514}
1515
1516#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
1519#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1520#[serde(rename_all = "snake_case")]
1521pub enum Compression {
1522 #[default]
1524 None,
1525 Gzip,
1527 Lz4,
1529 Zstd,
1532}
1533
1534fn default_compression_algorithm() -> Compression {
1535 Compression::Zstd
1536}
1537
1538#[derive(Debug, Deserialize, Serialize, Clone)]
1545#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1546#[serde(deny_unknown_fields)]
1547pub struct CompressionMiddleware {
1548 #[serde(default = "default_compression_algorithm")]
1550 pub algorithm: Compression,
1551 #[serde(default)]
1554 pub max_decompressed_bytes: Option<u64>,
1555}
1556
1557impl Default for CompressionMiddleware {
1558 fn default() -> Self {
1559 Self {
1560 algorithm: default_compression_algorithm(),
1561 max_decompressed_bytes: None,
1562 }
1563 }
1564}
1565
1566#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1569#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1570pub struct FileConfig {
1571 pub path: String,
1573 pub delimiter: Option<String>,
1577 #[serde(flatten, default)]
1580 pub mode: Option<FileConsumerMode>,
1581 #[serde(default)]
1583 pub format: FileFormat,
1584 #[serde(default)]
1586 pub compression: Compression,
1587 #[serde(default)]
1589 pub encryption: Option<EncryptionConfig>,
1590}
1591
1592#[derive(Debug, Clone, Deserialize, Serialize)]
1593#[serde(tag = "mode", rename_all = "snake_case")]
1594#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1595pub enum FileConsumerMode {
1596 Consume {
1600 #[serde(default)]
1603 delete: bool,
1604 },
1605 Subscribe {
1609 #[serde(default)]
1612 delete: bool,
1613 },
1614 GroupSubscribe {
1619 group_id: String,
1621 #[serde(default)]
1624 read_from_tail: bool,
1625 },
1626}
1627
1628impl Default for FileConsumerMode {
1629 fn default() -> Self {
1630 Self::Consume { delete: false }
1631 }
1632}
1633
1634impl FileConfig {
1635 pub fn new(path: impl Into<String>) -> Self {
1637 Self {
1638 path: path.into(),
1639 mode: Some(FileConsumerMode::default()),
1640 delimiter: None,
1641 format: FileFormat::default(),
1642 compression: Compression::default(),
1643 encryption: None,
1644 }
1645 }
1646
1647 pub fn with_mode(mut self, mode: FileConsumerMode) -> Self {
1648 self.mode = Some(mode);
1649 self
1650 }
1651
1652 pub fn effective_mode(&self) -> FileConsumerMode {
1654 self.mode.clone().unwrap_or_default()
1655 }
1656}
1657
1658#[derive(Debug, Clone, Serialize, Deserialize)]
1668#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1669pub struct ObjectStoreConfig {
1670 pub url: String,
1675 #[serde(default)]
1678 pub format: FileFormat,
1679 pub delimiter: Option<String>,
1682 pub checkpoint_store: Option<String>,
1686 pub cursor_id: Option<String>,
1688 pub polling_interval_ms: Option<u64>,
1691 pub max_object_bytes: Option<u64>,
1695 #[serde(default = "default_true")]
1698 pub date_partition: bool,
1699 pub extension: Option<String>,
1704 #[serde(default)]
1706 pub compression: Compression,
1707 #[serde(default)]
1709 pub encryption: Option<EncryptionConfig>,
1710}
1711
1712impl Default for ObjectStoreConfig {
1713 fn default() -> Self {
1714 Self {
1715 url: String::new(),
1716 format: FileFormat::default(),
1717 delimiter: None,
1718 checkpoint_store: None,
1719 cursor_id: None,
1720 polling_interval_ms: None,
1721 max_object_bytes: None,
1722 date_partition: true,
1723 extension: None,
1724 compression: Compression::default(),
1725 encryption: None,
1726 }
1727 }
1728}
1729
1730#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1734#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1735#[serde(deny_unknown_fields)]
1736pub struct NatsConfig {
1737 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1739 pub url: String,
1740 pub subject: Option<String>,
1743 pub stream: Option<String>,
1746 pub username: Option<String>,
1748 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1750 pub password: Option<String>,
1751 #[serde(default)]
1753 pub tls: TlsConfig,
1754 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1756 pub token: Option<String>,
1757 #[serde(default)]
1761 pub request_reply: bool,
1762 pub request_timeout_ms: Option<u64>,
1764 #[serde(default)]
1766 pub delayed_ack: bool,
1767 #[serde(default)]
1770 pub deduplicate: bool,
1771 #[serde(default)]
1773 pub no_jetstream: bool,
1774 #[serde(default)]
1776 pub subscriber_mode: bool,
1777 pub stream_max_messages: Option<i64>,
1779 pub deliver_policy: Option<NatsDeliverPolicy>,
1781 pub stream_max_bytes: Option<i64>,
1783 pub prefetch_count: Option<usize>,
1785 #[serde(default)]
1787 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
1788 pub shared: Option<bool>,
1789}
1790
1791#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
1792#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1793#[serde(rename_all = "snake_case")]
1794pub enum NatsDeliverPolicy {
1795 #[default]
1796 All,
1797 Last,
1798 New,
1799 LastPerSubject,
1800}
1801
1802impl NatsConfig {
1803 pub fn new(url: impl Into<String>) -> Self {
1805 Self {
1806 url: url.into(),
1807 ..Default::default()
1808 }
1809 }
1810
1811 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
1812 self.subject = Some(subject.into());
1813 self
1814 }
1815
1816 pub fn with_stream(mut self, stream: impl Into<String>) -> Self {
1817 self.stream = Some(stream.into());
1818 self
1819 }
1820
1821 pub fn with_deliver_policy(mut self, policy: NatsDeliverPolicy) -> Self {
1822 self.deliver_policy = Some(policy);
1823 self
1824 }
1825
1826 pub fn with_credentials(
1827 mut self,
1828 username: impl Into<String>,
1829 password: impl Into<String>,
1830 ) -> Self {
1831 self.username = Some(username.into());
1832 self.password = Some(password.into());
1833 self
1834 }
1835}
1836
1837#[derive(Debug, Serialize, Clone, Default)]
1838#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1839#[cfg_attr(feature = "schema", schemars(transform = memory_config_schema_transform))]
1840#[serde(deny_unknown_fields)]
1841pub struct MemoryConfig {
1842 #[serde(default, skip_serializing_if = "String::is_empty", alias = "url")]
1851 pub topic: String,
1852 #[serde(skip)]
1854 pub url: Option<String>,
1855 pub capacity: Option<usize>,
1857 #[serde(default)]
1859 pub request_reply: bool,
1860 pub request_timeout_ms: Option<u64>,
1862 #[serde(default)]
1864 pub subscribe_mode: bool,
1865 #[serde(default)]
1868 pub enable_nack: bool,
1869 #[serde(skip)]
1870 pub enable_nack_overridden: bool,
1871}
1872
1873impl MemoryConfig {
1874 pub fn new(topic: impl Into<String>, capacity: Option<usize>) -> Self {
1875 Self {
1876 topic: topic.into(),
1877 url: None,
1878 capacity,
1879 ..Default::default()
1880 }
1881 }
1882
1883 pub fn new_with_url(url: impl Into<String>, capacity: Option<usize>) -> Self {
1884 let url = url.into();
1885 Self {
1886 topic: url.clone(),
1887 url: Some(url),
1888 capacity,
1889 ..Default::default()
1890 }
1891 }
1892
1893 pub fn with_subscribe(self, subscribe_mode: bool) -> Self {
1894 Self {
1895 subscribe_mode,
1896 ..self
1897 }
1898 }
1899
1900 pub fn with_request_reply(mut self, request_reply: bool) -> Self {
1901 self.request_reply = request_reply;
1902 self
1903 }
1904
1905 pub fn get_transport_identifier(&self) -> anyhow::Result<String> {
1908 let identifier = if !self.topic.is_empty() {
1909 &self.topic
1910 } else if let Some(url) = self.url.as_ref().filter(|url| !url.is_empty()) {
1911 url
1912 } else {
1913 return Err(anyhow::anyhow!(
1914 "MemoryConfig: 'topic' (or 'url' alias) is required."
1915 ));
1916 };
1917
1918 if identifier.contains("://") {
1920 Ok(identifier.clone())
1921 } else {
1922 Ok(format!("memory://{}", identifier))
1923 }
1924 }
1925
1926 pub fn is_ipc_transport(&self) -> bool {
1929 if let Ok(identifier) = self.get_transport_identifier() {
1930 identifier.starts_with("ipc://")
1931 || identifier.starts_with("unix://")
1932 || identifier.starts_with("pipe://")
1933 } else {
1934 false
1935 }
1936 }
1937
1938 pub fn with_smart_defaults(mut self) -> Self {
1941 if !self.enable_nack_overridden && self.is_ipc_transport() {
1942 self.enable_nack = true;
1943 }
1944 self
1945 }
1946}
1947
1948impl<'de> Deserialize<'de> for MemoryConfig {
1949 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1950 where
1951 D: Deserializer<'de>,
1952 {
1953 #[derive(Deserialize, Default)]
1954 #[serde(deny_unknown_fields)]
1955 struct MemoryConfigSerde {
1956 #[serde(default)]
1957 topic: String,
1958 #[serde(default)]
1959 url: Option<String>,
1960 capacity: Option<usize>,
1961 #[serde(default)]
1962 request_reply: bool,
1963 request_timeout_ms: Option<u64>,
1964 #[serde(default)]
1965 subscribe_mode: bool,
1966 #[serde(default)]
1967 enable_nack: Option<bool>,
1968 }
1969
1970 let raw = MemoryConfigSerde::deserialize(deserializer)?;
1971 if raw.topic.is_empty() && raw.url.as_deref().is_none_or(str::is_empty) {
1972 return Err(serde::de::Error::custom(
1973 "MemoryConfig: 'topic' (or 'url' alias) is required.",
1974 ));
1975 }
1976 let topic = if raw.topic.is_empty() {
1977 raw.url.clone().unwrap_or_default()
1978 } else {
1979 raw.topic
1980 };
1981 Ok(Self {
1982 topic,
1983 url: raw.url,
1984 capacity: raw.capacity,
1985 request_reply: raw.request_reply,
1986 request_timeout_ms: raw.request_timeout_ms,
1987 subscribe_mode: raw.subscribe_mode,
1988 enable_nack: raw.enable_nack.unwrap_or(false),
1989 enable_nack_overridden: raw.enable_nack.is_some(),
1990 })
1991 }
1992}
1993
1994#[cfg(feature = "schema")]
1995fn memory_config_schema_transform(schema: &mut schemars::Schema) {
1996 let Some(schema_obj) = schema.as_object_mut() else {
1997 return;
1998 };
1999
2000 let Some(properties) = schema_obj
2001 .get_mut("properties")
2002 .and_then(serde_json::Value::as_object_mut)
2003 else {
2004 return;
2005 };
2006
2007 properties.insert(
2008 "url".to_string(),
2009 serde_json::json!({
2010 "description": "Alias for `topic`. Use either `topic` or `url`.",
2011 "type": "string",
2012 "minLength": 1
2013 }),
2014 );
2015
2016 if let Some(topic) = properties
2019 .get_mut("topic")
2020 .and_then(serde_json::Value::as_object_mut)
2021 {
2022 topic.insert("minLength".to_string(), serde_json::json!(1));
2023 }
2024
2025 schema_obj.insert(
2026 "anyOf".to_string(),
2027 serde_json::json!([
2028 { "required": ["topic"] },
2029 { "required": ["url"] }
2030 ]),
2031 );
2032}
2033
2034#[cfg(feature = "schema")]
2038fn endpoint_schema_transform(schema: &mut schemars::Schema) {
2039 let Some(one_of) = schema
2040 .as_object_mut()
2041 .and_then(|schema_obj| schema_obj.get_mut("oneOf"))
2042 .and_then(serde_json::Value::as_array_mut)
2043 else {
2044 return;
2045 };
2046
2047 for branch in one_of.iter_mut() {
2048 if branch.get("const") == Some(&serde_json::Value::String("null".to_string())) {
2049 *branch = serde_json::json!({
2050 "type": "object",
2051 "format": "structural_endpoint",
2052 "properties": { "null": { "type": "null" } },
2053 "required": ["null"]
2054 });
2055 }
2056 }
2057}
2058
2059#[cfg(feature = "schema")]
2060fn route_schema_transform(schema: &mut schemars::Schema) {
2061 let Some(properties) = schema
2062 .as_object_mut()
2063 .and_then(|schema_obj| schema_obj.get_mut("properties"))
2064 .and_then(serde_json::Value::as_object_mut)
2065 else {
2066 return;
2067 };
2068
2069 if let Some(output) = properties
2072 .get_mut("output")
2073 .and_then(serde_json::Value::as_object_mut)
2074 {
2075 let reference = output.remove("$ref");
2076 let default = output.remove("default");
2077 let description = output.remove("description");
2078 output.clear();
2079 let mut any_of = Vec::new();
2080 if let Some(reference) = reference {
2081 any_of.push(serde_json::json!({ "$ref": reference }));
2082 }
2083 any_of.push(serde_json::json!({ "type": "null" }));
2084 output.insert("anyOf".to_string(), serde_json::Value::Array(any_of));
2085 if let Some(description) = description {
2086 output.insert("description".to_string(), description);
2087 }
2088 if let Some(default) = default {
2089 output.insert("default".to_string(), default);
2090 }
2091 }
2092
2093 let Some(allow_fault_injection) = properties
2094 .get_mut("allow_fault_injection")
2095 .and_then(serde_json::Value::as_object_mut)
2096 else {
2097 return;
2098 };
2099
2100 allow_fault_injection.insert("default".to_string(), serde_json::Value::Bool(false));
2101}
2102
2103#[cfg(feature = "schema")]
2105fn transform_middleware_schema_transform(schema: &mut schemars::Schema) {
2106 if let Some(schema_obj) = schema.as_object_mut() {
2107 schema_obj.insert(
2110 "not".to_string(),
2111 serde_json::json!({
2112 "required": ["schema", "schema_file"],
2113 "properties": { "schema_file": { "type": "string" } }
2114 }),
2115 );
2116 }
2117}
2118
2119#[derive(Debug, Serialize, Deserialize, Clone, Default)]
2121#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2122#[serde(deny_unknown_fields)]
2123pub struct StreamBufferConfig {
2124 pub topic: String,
2126 #[serde(default, skip_serializing_if = "Option::is_none")]
2132 pub correlation_id: Option<String>,
2133 #[serde(default, skip_serializing_if = "Option::is_none")]
2135 pub capacity: Option<usize>,
2136}
2137
2138impl StreamBufferConfig {
2139 pub fn new(topic: impl Into<String>) -> Self {
2145 Self {
2146 topic: topic.into(),
2147 ..Default::default()
2148 }
2149 }
2150
2151 pub fn with_correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
2153 self.correlation_id = Some(correlation_id.into());
2154 self
2155 }
2156
2157 pub fn with_capacity(mut self, capacity: usize) -> Self {
2159 self.capacity = Some(capacity);
2160 self
2161 }
2162}
2163
2164#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2168#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2169#[serde(deny_unknown_fields)]
2170pub struct AmqpConfig {
2171 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2175 pub url: String,
2176 pub queue: Option<String>,
2178 #[serde(default)]
2180 pub subscribe_mode: bool,
2181 pub username: Option<String>,
2183 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2185 pub password: Option<String>,
2186 #[serde(default)]
2188 pub tls: TlsConfig,
2189 pub exchange: Option<String>,
2191 pub prefetch_count: Option<u16>,
2193 #[serde(default)]
2195 pub no_persistence: bool,
2196 #[serde(default)]
2198 pub no_declare_queue: bool,
2199 #[serde(default)]
2201 pub delayed_ack: bool,
2202}
2203
2204impl AmqpConfig {
2205 pub fn new(url: impl Into<String>) -> Self {
2207 Self {
2208 url: url.into(),
2209 ..Default::default()
2210 }
2211 }
2212
2213 pub fn with_queue(mut self, queue: impl Into<String>) -> Self {
2214 self.queue = Some(queue.into());
2215 self
2216 }
2217
2218 pub fn with_exchange(mut self, exchange: impl Into<String>) -> Self {
2219 self.exchange = Some(exchange.into());
2220 self
2221 }
2222
2223 pub fn with_credentials(
2224 mut self,
2225 username: impl Into<String>,
2226 password: impl Into<String>,
2227 ) -> Self {
2228 self.username = Some(username.into());
2229 self.password = Some(password.into());
2230 self
2231 }
2232}
2233
2234#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
2238#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2239#[serde(rename_all = "lowercase")]
2240pub enum MongoDbFormat {
2241 #[default]
2242 Normal,
2243 Json,
2244 Text,
2245 Raw,
2246}
2247
2248#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
2251#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2252#[serde(rename_all = "snake_case")]
2253pub enum MongoConsume {
2254 #[default]
2257 Consumer,
2258 Subscriber,
2260 CaptureNew,
2263 CaptureAll,
2266}
2267
2268#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2272#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2273#[serde(deny_unknown_fields)]
2274pub struct MongoDbConfig {
2275 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2278 pub url: String,
2279 pub collection: Option<String>,
2281 pub username: Option<String>,
2284 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2286 pub password: Option<String>,
2288 #[serde(default)]
2290 pub tls: TlsConfig,
2291 pub database: String,
2293 pub polling_interval_ms: Option<u64>,
2295 pub reply_polling_ms: Option<u64>,
2297 #[serde(default)]
2299 pub request_reply: bool,
2300 pub consume: Option<MongoConsume>,
2307 pub receive_query: Option<String>,
2309 #[serde(default)]
2311 pub change_stream: bool,
2312 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2324 pub checkpoint_store: Option<String>,
2325 pub request_timeout_ms: Option<u64>,
2327 pub ttl_seconds: Option<u64>,
2329 pub capped_size_bytes: Option<i64>,
2331 #[serde(default)]
2333 pub format: MongoDbFormat,
2334 pub id_field: Option<String>,
2337 #[serde(default)]
2340 pub report_outcome: bool,
2341 pub cursor_id: Option<String>,
2343 pub meta_collection: Option<String>,
2345 #[serde(default)]
2347 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
2348 pub shared: Option<bool>,
2349}
2350
2351impl MongoDbConfig {
2352 pub fn new(url: impl Into<String>, database: impl Into<String>) -> Self {
2354 Self {
2355 url: url.into(),
2356 database: database.into(),
2357 ..Default::default()
2358 }
2359 }
2360
2361 pub fn with_collection(mut self, collection: impl Into<String>) -> Self {
2362 self.collection = Some(collection.into());
2363 self
2364 }
2365
2366 pub fn with_credentials(
2367 mut self,
2368 username: impl Into<String>,
2369 password: impl Into<String>,
2370 ) -> Self {
2371 self.username = Some(username.into());
2372 self.password = Some(password.into());
2373 self
2374 }
2375
2376 pub fn with_change_stream(mut self, change_stream: bool) -> Self {
2377 self.change_stream = change_stream;
2378 self
2379 }
2380
2381 pub fn resolved_consume(&self) -> MongoConsume {
2384 if let Some(mode) = self.consume {
2385 return mode;
2386 }
2387 if self.change_stream {
2388 MongoConsume::Subscriber
2389 } else {
2390 MongoConsume::Consumer
2391 }
2392 }
2393}
2394
2395#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2399#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2400#[serde(deny_unknown_fields)]
2401pub struct MqttConfig {
2402 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2404 pub url: String,
2405 pub topic: Option<String>,
2407 pub username: Option<String>,
2409 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2411 pub password: Option<String>,
2412 #[serde(default)]
2414 pub tls: TlsConfig,
2415 pub client_id: Option<String>,
2417 pub queue_capacity: Option<usize>,
2419 pub max_inflight: Option<u16>,
2421 pub qos: Option<u8>,
2423 #[serde(default = "default_clean_session")]
2425 pub clean_session: bool,
2426 pub keep_alive_seconds: Option<u64>,
2428 #[serde(default)]
2430 pub protocol: MqttProtocol,
2431 pub session_expiry_interval: Option<u32>,
2433 #[serde(default)]
2439 pub delayed_ack: bool,
2440}
2441
2442impl MqttConfig {
2443 pub fn new(url: impl Into<String>) -> Self {
2445 Self {
2446 url: url.into(),
2447 ..Default::default()
2448 }
2449 }
2450
2451 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
2452 self.topic = Some(topic.into());
2453 self
2454 }
2455
2456 pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
2457 self.client_id = Some(client_id.into());
2458 self
2459 }
2460
2461 pub fn with_credentials(
2462 mut self,
2463 username: impl Into<String>,
2464 password: impl Into<String>,
2465 ) -> Self {
2466 self.username = Some(username.into());
2467 self.password = Some(password.into());
2468 self
2469 }
2470}
2471
2472#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
2476#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2477#[serde(rename_all = "lowercase")]
2478pub enum MqttProtocol {
2479 #[default]
2480 V5,
2481 V3,
2482}
2483
2484#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2487#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2488#[serde(deny_unknown_fields)]
2489pub struct ZeroMqConfig {
2490 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2491 pub url: String,
2493 #[serde(default)]
2495 pub socket_type: Option<ZeroMqSocketType>,
2496 pub topic: Option<String>,
2498 #[serde(default)]
2500 pub bind: bool,
2501 #[serde(default)]
2503 pub internal_buffer_size: Option<usize>,
2504 #[serde(default)]
2506 pub format: ZeroMqFormat,
2507 #[serde(default)]
2509 pub backend: ZeroMqBackend,
2510 #[serde(default)]
2512 pub request_timeout_ms: Option<u64>,
2513}
2514
2515impl ZeroMqConfig {
2516 pub fn new(url: impl Into<String>) -> Self {
2518 Self {
2519 url: url.into(),
2520 ..Default::default()
2521 }
2522 }
2523
2524 pub fn with_socket_type(mut self, socket_type: ZeroMqSocketType) -> Self {
2525 self.socket_type = Some(socket_type);
2526 self
2527 }
2528
2529 pub fn with_bind(mut self, bind: bool) -> Self {
2530 self.bind = bind;
2531 self
2532 }
2533}
2534
2535#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
2543#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2544#[serde(rename_all = "snake_case")]
2545pub enum ZeroMqFormat {
2546 #[default]
2547 Json,
2548 Raw,
2549 RawFramed,
2550}
2551
2552#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
2557#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2558#[serde(rename_all = "lowercase")]
2559pub enum ZeroMqSocketType {
2560 Push,
2561 Pull,
2562 Pub,
2563 Sub,
2564 Req,
2565 Rep,
2566}
2567
2568#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
2575#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2576#[serde(rename_all = "lowercase")]
2577pub enum ZeroMqBackend {
2578 #[default]
2579 Zmq,
2580 Omq,
2581}
2582
2583#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2592#[serde(deny_unknown_fields)]
2593pub struct RedisStreamsConfig {
2594 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2596 pub url: String,
2597 pub stream: Option<String>,
2599 pub group: Option<String>,
2601 pub consumer_name: Option<String>,
2603 #[serde(default)]
2605 pub subscriber_mode: bool,
2606 pub block_ms: Option<u64>,
2608 #[serde(default)]
2610 pub read_from_start: bool,
2611 pub redelivery_timeout_ms: Option<u64>,
2613 pub maxlen: Option<usize>,
2615 pub approx_trim: Option<bool>,
2617 pub username: Option<String>,
2619 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2621 pub password: Option<String>,
2622 pub internal_buffer_size: Option<usize>,
2624 pub reader_connections: Option<usize>,
2627}
2628
2629impl RedisStreamsConfig {
2630 pub fn new(url: impl Into<String>) -> Self {
2632 Self {
2633 url: url.into(),
2634 ..Default::default()
2635 }
2636 }
2637
2638 pub fn with_stream(mut self, stream: impl Into<String>) -> Self {
2639 self.stream = Some(stream.into());
2640 self
2641 }
2642
2643 pub fn with_group(mut self, group: impl Into<String>) -> Self {
2644 self.group = Some(group.into());
2645 self
2646 }
2647
2648 pub fn with_subscriber(mut self, subscriber: bool) -> Self {
2649 self.subscriber_mode = subscriber;
2650 self
2651 }
2652
2653 pub fn with_reader_connections(mut self, connections: usize) -> Self {
2654 self.reader_connections = Some(connections);
2655 self
2656 }
2657}
2658
2659#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2662#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2663#[serde(deny_unknown_fields)]
2664pub struct GrpcConfig {
2665 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2666 pub url: String,
2668 pub topic: Option<String>,
2670 pub timeout_ms: Option<u64>,
2674 #[serde(default)]
2676 pub tls: TlsConfig,
2677 #[serde(default)]
2680 pub server_mode: bool,
2681 #[serde(default)]
2683 pub initial_stream_window_size: Option<u32>,
2684 #[serde(default)]
2686 pub initial_connection_window_size: Option<u32>,
2687 #[serde(default)]
2689 pub concurrency_limit_per_connection: Option<usize>,
2690 #[serde(default)]
2692 pub http2_keepalive_interval_ms: Option<u64>,
2693 #[serde(default)]
2695 pub http2_keepalive_timeout_ms: Option<u64>,
2696 #[serde(default)]
2698 pub max_decoding_message_size: Option<usize>,
2699 #[serde(default)]
2701 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
2702 pub shared: Option<bool>,
2703}
2704
2705impl GrpcConfig {
2706 pub fn new(url: impl Into<String>) -> Self {
2708 Self {
2709 url: url.into(),
2710 ..Default::default()
2711 }
2712 }
2713
2714 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
2715 self.topic = Some(topic.into());
2716 self
2717 }
2718
2719 pub fn with_server_mode(mut self, server_mode: bool) -> Self {
2721 self.server_mode = server_mode;
2722 self
2723 }
2724}
2725
2726#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, Default)]
2730#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2731#[serde(rename_all = "snake_case")]
2732pub enum HttpServerProtocol {
2733 #[default]
2735 Auto,
2736 Http1Only,
2738 Http2Only,
2740}
2741
2742#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
2744#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2745#[serde(rename_all = "snake_case")]
2746pub enum WebSocketExecutionMode {
2747 #[default]
2750 Auto,
2751 DirectOnly,
2753 Routed,
2755}
2756
2757#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2759#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2760#[serde(deny_unknown_fields)]
2761pub struct HttpConfig {
2762 pub url: String,
2764 pub path: Option<String>,
2766 pub method: Option<String>,
2768 #[serde(default)]
2770 pub tls: TlsConfig,
2771 pub workers: Option<usize>,
2773 pub message_id_header: Option<String>,
2775 pub request_timeout_ms: Option<u64>,
2777 pub internal_buffer_size: Option<usize>,
2779 #[serde(default)]
2781 pub fire_and_forget: bool,
2782 #[serde(default)]
2784 pub receive_streamable: bool,
2785 #[serde(default, skip_serializing_if = "Option::is_none")]
2788 #[cfg_attr(
2789 feature = "schema",
2790 schemars(default = "default_inline_response_fast_path_schema")
2791 )]
2792 pub inline_response_fast_path: Option<bool>,
2793 #[serde(default)]
2797 pub server_protocol: HttpServerProtocol,
2798 #[serde(default, skip_serializing_if = "Option::is_none")]
2808 pub stream_response_to: Option<Box<Endpoint>>,
2809 #[serde(default, skip_serializing_if = "Option::is_none")]
2811 pub batch_concurrency: Option<usize>,
2812 #[serde(default, skip_serializing_if = "Option::is_none")]
2814 pub tcp_keepalive_ms: Option<u64>,
2815 #[serde(default, skip_serializing_if = "Option::is_none")]
2817 pub pool_idle_timeout_ms: Option<u64>,
2818 #[serde(default)]
2822 pub compression: Compression,
2823 #[serde(default, skip_serializing_if = "Option::is_none")]
2827 pub compression_enabled: Option<bool>,
2828 #[serde(default)]
2830 pub compression_threshold_bytes: Option<usize>,
2831 pub concurrency_limit: Option<usize>,
2833 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2835 #[serde(
2836 default,
2837 skip_serializing_if = "Option::is_none",
2838 deserialize_with = "deserialize_basic_auth"
2839 )]
2840 pub basic_auth: Option<(String, String)>,
2841 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2843 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
2844 pub custom_headers: HashMap<String, String>,
2845 #[serde(default)]
2847 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
2848 pub shared: Option<bool>,
2849}
2850
2851#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2853#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2854#[serde(deny_unknown_fields)]
2855pub struct WebSocketConfig {
2856 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2858 pub url: String,
2859 pub path: Option<String>,
2861 pub message_id_header: Option<String>,
2863 pub routed_queue_capacity: Option<usize>,
2865 pub backlog: Option<u32>,
2869 #[serde(default)]
2871 pub execution_mode: WebSocketExecutionMode,
2872}
2873
2874fn deserialize_basic_auth<'de, D>(deserializer: D) -> Result<Option<(String, String)>, D::Error>
2875where
2876 D: Deserializer<'de>,
2877{
2878 let val = serde_json::Value::deserialize(deserializer)?;
2879 match val {
2880 serde_json::Value::Null => Ok(None),
2881 serde_json::Value::Array(arr) => {
2882 if arr.len() != 2 {
2883 return Err(serde::de::Error::custom("basic_auth must have 2 elements"));
2884 }
2885 let u = arr[0]
2886 .as_str()
2887 .ok_or_else(|| serde::de::Error::custom("basic_auth[0] must be string"))?
2888 .to_string();
2889 let p = arr[1]
2890 .as_str()
2891 .ok_or_else(|| serde::de::Error::custom("basic_auth[1] must be string"))?
2892 .to_string();
2893 Ok(Some((u, p)))
2894 }
2895 serde_json::Value::Object(map) => {
2896 let u = map
2897 .get("0")
2898 .and_then(|v| v.as_str())
2899 .ok_or_else(|| serde::de::Error::custom("basic_auth map missing '0'"))?
2900 .to_string();
2901 let p = map
2902 .get("1")
2903 .and_then(|v| v.as_str())
2904 .ok_or_else(|| serde::de::Error::custom("basic_auth map missing '1'"))?
2905 .to_string();
2906 Ok(Some((u, p)))
2907 }
2908 _ => Err(serde::de::Error::custom("invalid type for basic_auth")),
2909 }
2910}
2911
2912impl HttpConfig {
2913 pub fn new(url: impl Into<String>) -> Self {
2915 Self {
2916 url: url.into(),
2917 ..Default::default()
2918 }
2919 }
2920
2921 pub fn with_workers(mut self, workers: usize) -> Self {
2922 self.workers = Some(workers);
2923 self
2924 }
2925
2926 pub fn with_method(mut self, method: impl Into<String>) -> Self {
2927 self.method = Some(method.into());
2928 self
2929 }
2930
2931 pub fn with_path(mut self, path: impl Into<String>) -> Self {
2932 self.path = Some(path.into());
2933 self
2934 }
2935
2936 pub fn with_receive_streamable(mut self, receive_streamable: bool) -> Self {
2937 self.receive_streamable = receive_streamable;
2938 self
2939 }
2940
2941 pub fn with_inline_response_fast_path(mut self, inline_response_fast_path: bool) -> Self {
2942 self.inline_response_fast_path = Some(inline_response_fast_path);
2943 self
2944 }
2945
2946 pub fn with_server_protocol(mut self, server_protocol: HttpServerProtocol) -> Self {
2947 self.server_protocol = server_protocol;
2948 self
2949 }
2950
2951 pub fn inline_response_fast_path_enabled(&self) -> bool {
2952 self.inline_response_fast_path.unwrap_or(true)
2953 }
2954
2955 pub fn publisher_compression(&self) -> Compression {
2958 match self.compression {
2959 Compression::None if self.compression_enabled == Some(true) => Compression::Gzip,
2960 other => other,
2961 }
2962 }
2963
2964 pub fn consumer_compression_enabled(&self) -> bool {
2967 self.compression_enabled == Some(true)
2968 }
2969
2970 pub fn with_stream_response_to(mut self, endpoint: Endpoint) -> Self {
2971 self.stream_response_to = Some(Box::new(endpoint));
2972 self
2973 }
2974}
2975
2976impl WebSocketConfig {
2977 pub fn new(url: impl Into<String>) -> Self {
2979 Self {
2980 url: url.into(),
2981 ..Default::default()
2982 }
2983 }
2984
2985 pub fn with_path(mut self, path: impl Into<String>) -> Self {
2986 self.path = Some(path.into());
2987 self
2988 }
2989
2990 pub fn with_backlog(mut self, backlog: u32) -> Self {
2991 self.backlog = Some(backlog);
2992 self
2993 }
2994
2995 pub fn with_execution_mode(mut self, execution_mode: WebSocketExecutionMode) -> Self {
2996 self.execution_mode = execution_mode;
2997 self
2998 }
2999}
3000
3001#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
3008#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3009#[cfg_attr(feature = "schema", schemars(transform = ibm_tls_config_schema_transform))]
3010#[serde(deny_unknown_fields)]
3011pub struct IbmTlsConfig {
3012 #[serde(default, deserialize_with = "deserialize_null_as_false")]
3014 pub required: bool,
3015 pub cipher_spec: Option<String>,
3017 #[serde(rename = "cert_file", alias = "key_repository")]
3021 pub key_repository: Option<String>,
3022 #[serde(rename = "cert_password", alias = "key_repository_password")]
3025 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3026 pub key_repository_password: Option<String>,
3027 #[serde(default)]
3029 pub accept_invalid_certs: bool,
3030}
3031
3032#[cfg(feature = "schema")]
3036fn ibm_tls_config_schema_transform(schema: &mut schemars::Schema) {
3037 let Some(properties) = schema
3038 .as_object_mut()
3039 .and_then(|schema_obj| schema_obj.get_mut("properties"))
3040 .and_then(serde_json::Value::as_object_mut)
3041 else {
3042 return;
3043 };
3044
3045 properties.insert(
3046 "key_repository".to_string(),
3047 serde_json::json!({
3048 "description": "MQ-native alias for `cert_file`: the CMS key repository stem \
3049 (e.g. `/path/to/tls` for `tls.kdb`/`tls.sth`).",
3050 "type": ["string", "null"]
3051 }),
3052 );
3053
3054 properties.insert(
3055 "key_repository_password".to_string(),
3056 serde_json::json!({
3057 "description": "MQ-native alias for `cert_password`: password unlocking the key \
3058 repository. Requires an IBM MQ client/server at 9.3.0.0+.",
3059 "type": ["string", "null"],
3060 "format": "password"
3061 }),
3062 );
3063}
3064
3065#[derive(Debug, Deserialize, Serialize, Clone)]
3070#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3071#[serde(deny_unknown_fields)]
3072pub struct IbmMqConfig {
3073 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3075 pub url: String,
3076 pub queue: Option<String>,
3078 pub topic: Option<String>,
3080 pub queue_manager: String,
3082 pub channel: String,
3084 pub username: Option<String>,
3086 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3088 pub password: Option<String>,
3089 #[serde(default)]
3091 pub tls: IbmTlsConfig,
3092 #[serde(default = "default_max_message_size")]
3094 pub max_message_size: usize,
3095 #[serde(default = "default_wait_timeout_ms")]
3097 pub wait_timeout_ms: i32,
3098 #[serde(default)]
3100 pub internal_buffer_size: Option<usize>,
3101 #[serde(default)]
3103 pub disable_status_inq: bool,
3104}
3105
3106impl IbmMqConfig {
3107 pub fn new(
3109 url: impl Into<String>,
3110 queue_manager: impl Into<String>,
3111 channel: impl Into<String>,
3112 ) -> Self {
3113 Self {
3114 url: url.into(),
3115 queue_manager: queue_manager.into(),
3116 channel: channel.into(),
3117 disable_status_inq: false,
3118 ..Default::default()
3119 }
3120 }
3121
3122 pub fn with_queue(mut self, queue: impl Into<String>) -> Self {
3123 self.queue = Some(queue.into());
3124 self
3125 }
3126
3127 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
3128 self.topic = Some(topic.into());
3129 self
3130 }
3131
3132 pub fn with_credentials(
3133 mut self,
3134 username: impl Into<String>,
3135 password: impl Into<String>,
3136 ) -> Self {
3137 self.username = Some(username.into());
3138 self.password = Some(password.into());
3139 self
3140 }
3141}
3142
3143impl Default for IbmMqConfig {
3144 fn default() -> Self {
3145 Self {
3146 url: String::new(),
3147 queue: None,
3148 topic: None,
3149 queue_manager: String::new(),
3150 channel: String::new(),
3151 username: None,
3152 password: None,
3153 tls: IbmTlsConfig::default(),
3154 max_message_size: default_max_message_size(),
3155 wait_timeout_ms: default_wait_timeout_ms(),
3156 internal_buffer_size: None,
3157 disable_status_inq: false,
3158 }
3159 }
3160}
3161
3162fn default_max_message_size() -> usize {
3163 4 * 1024 * 1024 }
3165
3166fn default_wait_timeout_ms() -> i32 {
3167 1000 }
3169
3170#[derive(Debug, Deserialize, Serialize, Clone)]
3173#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3174#[serde(deny_unknown_fields)]
3175pub struct SwitchConfig {
3176 pub metadata_key: String,
3178 pub cases: HashMap<String, Endpoint>,
3180 pub default: Option<Box<Endpoint>>,
3182}
3183
3184#[derive(Debug, Deserialize, Serialize, Clone, Default)]
3186#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3187#[serde(deny_unknown_fields)]
3188pub struct ResponseConfig {
3189 }
3191
3192#[derive(Debug, Deserialize, Serialize, Clone)]
3203#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3204#[serde(deny_unknown_fields)]
3205pub struct RequestForwardConfig {
3206 pub to: Box<Endpoint>,
3208 pub forward_to: Box<Endpoint>,
3210}
3211
3212#[derive(Debug, Deserialize, Serialize, Clone, Default)]
3216#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3217#[serde(deny_unknown_fields)]
3218pub struct PostgresCdcConfig {
3219 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3221 pub url: String,
3222 pub publication: String,
3224 #[serde(default = "default_pg_cdc_slot")]
3226 pub slot_name: String,
3227 #[serde(default = "default_true")]
3229 pub create_slot: bool,
3230 #[serde(default)]
3233 pub create_publication: bool,
3234 #[serde(default)]
3237 pub publication_tables: Vec<String>,
3238 #[serde(default)]
3240 pub temporary_slot: bool,
3241 pub cursor_id: Option<String>,
3243 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3245 pub checkpoint_store: Option<String>,
3246 #[serde(default = "default_pg_cdc_status_interval_ms")]
3248 pub status_interval_ms: u64,
3249 #[serde(default)]
3251 pub tls: TlsConfig,
3252}
3253
3254fn default_pg_cdc_slot() -> String {
3255 "mq_bridge_slot".to_string()
3256}
3257
3258fn default_pg_cdc_status_interval_ms() -> u64 {
3259 10_000
3260}
3261
3262#[derive(Debug, Deserialize, Serialize, Clone, Default)]
3266#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3267#[serde(deny_unknown_fields)]
3268pub struct SqlxConfig {
3269 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3271 pub url: String,
3272 #[serde(default)]
3274 pub username: Option<String>,
3275 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3277 #[serde(default)]
3278 pub password: Option<String>,
3279 pub table: String,
3281 pub insert_query: Option<String>,
3298 pub select_query: Option<String>,
3302 #[serde(default)]
3304 pub delete_after_read: bool,
3305 pub cursor_column: Option<String>,
3310 pub cursor_id: Option<String>,
3314 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3326 pub checkpoint_store: Option<String>,
3327 #[serde(default)]
3329 pub auto_create_table: bool,
3330 #[serde(default)]
3332 pub bulk_copy: bool,
3333 pub polling_interval_ms: Option<u64>,
3335 pub max_polling_interval_ms: Option<u64>,
3338 pub publication: Option<String>,
3342 pub slot_name: Option<String>,
3344 #[serde(default)]
3347 pub create_publication: bool,
3348 #[serde(default)]
3350 pub tls: TlsConfig,
3351 pub max_connections: Option<u32>,
3353 pub min_connections: Option<u32>,
3355 pub acquire_timeout_ms: Option<u64>,
3357 pub idle_timeout_ms: Option<u64>,
3359 pub max_lifetime_ms: Option<u64>,
3361 #[serde(default)]
3363 #[cfg_attr(feature = "schema", schemars(default = "default_shared_schema"))]
3364 pub shared: Option<bool>,
3365}
3366
3367#[derive(Debug, Deserialize, Serialize, Clone, Default)]
3377#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3378#[serde(deny_unknown_fields)]
3379pub struct ClickHouseConfig {
3380 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3383 pub url: String,
3384 #[serde(default)]
3386 pub username: Option<String>,
3387 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3389 #[serde(default)]
3390 pub password: Option<String>,
3391 pub database: Option<String>,
3393 pub table: String,
3395 pub columns: Option<std::collections::BTreeMap<String, String>>,
3400 #[serde(default)]
3403 pub async_insert: bool,
3404 #[serde(default)]
3407 pub wait_for_async_insert: Option<bool>,
3408 pub cursor_column: Option<String>,
3412 pub cursor_id: Option<String>,
3415 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3424 pub checkpoint_store: Option<String>,
3425 pub select_columns: Option<String>,
3427 pub polling_interval_ms: Option<u64>,
3429 pub max_polling_interval_ms: Option<u64>,
3432 pub request_timeout_ms: Option<u64>,
3435 pub connect_timeout_ms: Option<u64>,
3437 #[serde(default)]
3439 pub tls: TlsConfig,
3440 #[serde(default = "default_gzip_compression")]
3444 pub compression: Compression,
3445}
3446
3447fn default_gzip_compression() -> Compression {
3448 Compression::Gzip
3449}
3450
3451#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
3472#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3473#[serde(deny_unknown_fields)]
3474pub struct TlsConfig {
3475 #[serde(default, deserialize_with = "deserialize_null_as_false")]
3477 pub required: bool,
3478 pub ca_file: Option<String>,
3480 pub cert_file: Option<String>,
3482 pub key_file: Option<String>,
3484 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
3486 pub cert_password: Option<String>,
3487 #[serde(default)]
3489 pub accept_invalid_certs: bool,
3490}
3491
3492impl TlsConfig {
3493 pub fn new() -> Self {
3495 Self::default()
3496 }
3497
3498 pub fn with_ca_file(mut self, ca_file: impl Into<String>) -> Self {
3499 self.ca_file = Some(ca_file.into());
3500 self.required = true;
3501 self
3502 }
3503
3504 pub fn with_client_cert(
3505 mut self,
3506 cert_file: impl Into<String>,
3507 key_file: impl Into<String>,
3508 ) -> Self {
3509 self.cert_file = Some(cert_file.into());
3510 self.key_file = Some(key_file.into());
3511 self.required = true;
3512 self
3513 }
3514
3515 pub fn with_insecure(mut self, accept_invalid_certs: bool) -> Self {
3516 self.accept_invalid_certs = accept_invalid_certs;
3517 self
3518 }
3519
3520 pub fn is_mtls_client_configured(&self) -> bool {
3522 self.required && self.cert_file.is_some() && self.key_file.is_some()
3523 }
3524
3525 pub fn is_tls_server_configured(&self) -> bool {
3527 self.required && self.cert_file.is_some() && self.key_file.is_some()
3528 }
3529
3530 pub fn is_tls_client_configured(&self) -> bool {
3532 self.required
3533 || self.ca_file.is_some()
3534 || (self.cert_file.is_some() && self.key_file.is_some())
3535 }
3536
3537 pub fn normalize_url(&self, url: &str) -> String {
3539 if url
3540 .get(..7)
3541 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("http://"))
3542 || url
3543 .get(..8)
3544 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("https://"))
3545 {
3546 url.to_string()
3547 } else {
3548 let is_tls = self.required;
3549 let scheme = if is_tls { "https" } else { "http" };
3550 format!("{}://{}", scheme, url)
3551 }
3552 }
3553}
3554
3555pub trait SecretExtractor {
3557 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>);
3559}
3560
3561fn extract_sensitive_string_map_entries(
3562 values: &mut HashMap<String, String>,
3563 prefix: &str,
3564 field_name: &str,
3565 secrets: &mut HashMap<String, String>,
3566) {
3567 let secret_keys = values
3568 .keys()
3569 .filter(|key| {
3570 let key = key.to_ascii_lowercase();
3571 key.contains("key") || key.contains("token") || key.contains("auth")
3572 })
3573 .cloned()
3574 .collect::<Vec<_>>();
3575
3576 for key in secret_keys {
3577 if let Some(value) = values.remove(&key) {
3578 secrets.insert(
3579 sanitize_secret_key(&format!("{}__{}__{}", prefix, field_name, key)),
3580 value,
3581 );
3582 }
3583 }
3584}
3585
3586fn url_has_userinfo(url: &str) -> bool {
3587 let Some(authority_start) = url.find("://").map(|idx| idx + 3) else {
3588 return false;
3589 };
3590 let authority_end = url[authority_start..]
3591 .find(['/', '?', '#'])
3592 .map(|idx| authority_start + idx)
3593 .unwrap_or(url.len());
3594 url[authority_start..authority_end].contains('@')
3595}
3596
3597fn sanitize_secret_key(key: &str) -> String {
3598 key.chars()
3599 .map(|ch| {
3600 let ch = ch.to_ascii_uppercase();
3601 if ch.is_ascii_alphanumeric() || ch == '_' {
3602 ch
3603 } else {
3604 '_'
3605 }
3606 })
3607 .collect()
3608}
3609
3610fn extract_sensitive_url(
3611 url: &mut String,
3612 prefix: &str,
3613 field_name: &str,
3614 secrets: &mut HashMap<String, String>,
3615) {
3616 if !url.is_empty() && url_has_userinfo(url) {
3617 secrets.insert(
3618 sanitize_secret_key(&format!("{}__{}", prefix, field_name)),
3619 std::mem::take(url),
3620 );
3621 }
3622}
3623
3624fn extract_sensitive_optional_url(
3625 url: &mut Option<String>,
3626 prefix: &str,
3627 field_name: &str,
3628 secrets: &mut HashMap<String, String>,
3629) {
3630 if url.as_ref().is_some_and(|url| url_has_userinfo(url)) {
3631 if let Some(url) = url.take() {
3632 secrets.insert(
3633 sanitize_secret_key(&format!("{}__{}", prefix, field_name)),
3634 url,
3635 );
3636 }
3637 }
3638}
3639
3640impl SecretExtractor for Route {
3641 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3642 self.input
3643 .extract_secrets(&format!("{}__{}", prefix, "INPUT"), secrets);
3644 self.output
3645 .extract_secrets(&format!("{}__{}", prefix, "OUTPUT"), secrets);
3646 }
3647}
3648
3649impl SecretExtractor for Endpoint {
3650 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3651 for (i, middleware) in self.middlewares.iter_mut().enumerate() {
3652 middleware.extract_secrets(&format!("{}__{}__{}", prefix, "MIDDLEWARES", i), secrets);
3653 }
3654 self.endpoint_type.extract_secrets(prefix, secrets);
3655 }
3656}
3657
3658impl SecretExtractor for EndpointType {
3659 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3660 match self {
3661 EndpointType::Aws(cfg) => {
3662 cfg.extract_secrets(&format!("{}__{}", prefix, "AWS"), secrets)
3663 }
3664 EndpointType::Kafka(cfg) => {
3665 cfg.extract_secrets(&format!("{}__{}", prefix, "KAFKA"), secrets)
3666 }
3667 EndpointType::Nats(cfg) => {
3668 cfg.extract_secrets(&format!("{}__{}", prefix, "NATS"), secrets)
3669 }
3670 EndpointType::Amqp(cfg) => {
3671 cfg.extract_secrets(&format!("{}__{}", prefix, "AMQP"), secrets)
3672 }
3673 EndpointType::MongoDb(cfg) => {
3674 cfg.extract_secrets(&format!("{}__{}", prefix, "MONGODB"), secrets)
3675 }
3676 EndpointType::Mqtt(cfg) => {
3677 cfg.extract_secrets(&format!("{}__{}", prefix, "MQTT"), secrets)
3678 }
3679 EndpointType::Http(cfg) => {
3680 cfg.extract_secrets(&format!("{}__{}", prefix, "HTTP"), secrets)
3681 }
3682 EndpointType::WebSocket(cfg) => {
3683 cfg.extract_secrets(&format!("{}__{}", prefix, "WEBSOCKET"), secrets)
3684 }
3685 EndpointType::IbmMq(cfg) => {
3686 cfg.extract_secrets(&format!("{}__{}", prefix, "IBMMQ"), secrets)
3687 }
3688 EndpointType::ZeroMq(cfg) => {
3689 cfg.extract_secrets(&format!("{}__{}", prefix, "ZEROMQ"), secrets)
3690 }
3691 EndpointType::RedisStreams(cfg) => {
3692 cfg.extract_secrets(&format!("{}__{}", prefix, "REDIS_STREAMS"), secrets)
3693 }
3694 EndpointType::Sqlx(cfg) => {
3695 cfg.extract_secrets(&format!("{}__{}", prefix, "SQLX"), secrets)
3696 }
3697 EndpointType::ClickHouse(cfg) => {
3698 cfg.extract_secrets(&format!("{}__{}", prefix, "CLICKHOUSE"), secrets)
3699 }
3700 EndpointType::PostgresCdc(cfg) => {
3701 cfg.extract_secrets(&format!("{}__{}", prefix, "POSTGRES_CDC"), secrets)
3702 }
3703 EndpointType::Grpc(cfg) => {
3704 cfg.extract_secrets(&format!("{}__{}", prefix, "GRPC"), secrets)
3705 }
3706 EndpointType::Fanout(endpoints) => {
3707 for (i, ep) in endpoints.iter_mut().enumerate() {
3708 ep.extract_secrets(&format!("{}__{}__{}", prefix, "FANOUT", i), secrets);
3709 }
3710 }
3711 EndpointType::Switch(cfg) => {
3712 for (key, ep) in cfg.cases.iter_mut() {
3713 ep.extract_secrets(
3714 &format!(
3715 "{}__{}__{}",
3716 prefix,
3717 "SWITCH__CASES",
3718 sanitize_secret_key(key)
3719 ),
3720 secrets,
3721 );
3722 }
3723 if let Some(default) = &mut cfg.default {
3724 default.extract_secrets(&format!("{}__{}", prefix, "SWITCH__DEFAULT"), secrets);
3725 }
3726 }
3727 EndpointType::Reader(ep) => {
3728 ep.extract_secrets(&format!("{}__{}", prefix, "READER"), secrets)
3729 }
3730 EndpointType::Request(cfg) => {
3731 cfg.to
3732 .extract_secrets(&format!("{}__{}", prefix, "REQUEST__TO"), secrets);
3733 cfg.forward_to
3734 .extract_secrets(&format!("{}__{}", prefix, "REQUEST__FORWARD_TO"), secrets);
3735 }
3736 EndpointType::File(cfg) => {
3737 if let Some(enc) = &mut cfg.encryption {
3738 enc.extract_secrets(&format!("{}__{}", prefix, "FILE__ENCRYPTION"), secrets);
3739 }
3740 }
3741 EndpointType::ObjectStore(cfg) => {
3742 if let Some(enc) = &mut cfg.encryption {
3743 enc.extract_secrets(
3744 &format!("{}__{}", prefix, "OBJECT_STORE__ENCRYPTION"),
3745 secrets,
3746 );
3747 }
3748 }
3749 _ => {}
3750 }
3751 }
3752}
3753
3754impl SecretExtractor for Middleware {
3755 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3756 match self {
3757 Middleware::Dlq(cfg) => {
3758 cfg.endpoint
3759 .extract_secrets(&format!("{}__{}__{}", prefix, "DLQ", "ENDPOINT"), secrets);
3760 }
3761 Middleware::Encryption(cfg) => {
3762 cfg.extract_secrets(&format!("{}__{}", prefix, "ENCRYPTION"), secrets);
3763 }
3764 _ => {}
3765 }
3766 }
3767}
3768
3769impl SecretExtractor for EncryptionConfig {
3770 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3771 if !self.key.is_empty() {
3772 secrets.insert(
3773 sanitize_secret_key(&format!("{}__{}", prefix, "KEY")),
3774 std::mem::take(&mut self.key),
3775 );
3776 }
3777 for (id, k) in std::mem::take(&mut self.decrypt_keys) {
3778 secrets.insert(
3779 sanitize_secret_key(&format!("{}__{}__{}", prefix, "DECRYPT_KEYS", id)),
3780 k,
3781 );
3782 }
3783 }
3784}
3785
3786impl SecretExtractor for AwsConfig {
3787 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3788 if let Some(val) = self.access_key.take() {
3789 secrets.insert(format!("{}__{}", prefix, "ACCESS_KEY"), val);
3790 }
3791 if let Some(val) = self.secret_key.take() {
3792 secrets.insert(format!("{}__{}", prefix, "SECRET_KEY"), val);
3793 }
3794 if let Some(val) = self.session_token.take() {
3795 secrets.insert(format!("{}__{}", prefix, "SESSION_TOKEN"), val);
3796 }
3797 extract_sensitive_optional_url(&mut self.queue_url, prefix, "QUEUE_URL", secrets);
3798 extract_sensitive_optional_url(&mut self.endpoint_url, prefix, "ENDPOINT_URL", secrets);
3799 }
3800}
3801
3802impl SecretExtractor for KafkaConfig {
3803 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3804 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3805 if let Some(val) = self.username.take() {
3806 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3807 }
3808 if let Some(val) = self.password.take() {
3809 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3810 }
3811 self.tls
3812 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3813 }
3814}
3815
3816impl SecretExtractor for NatsConfig {
3817 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3818 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3819 if let Some(val) = self.username.take() {
3820 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3821 }
3822 if let Some(val) = self.password.take() {
3823 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3824 }
3825 if let Some(val) = self.token.take() {
3826 secrets.insert(format!("{}__{}", prefix, "TOKEN"), val);
3827 }
3828 self.tls
3829 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3830 }
3831}
3832
3833impl SecretExtractor for AmqpConfig {
3834 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3835 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3836 if let Some(val) = self.username.take() {
3837 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3838 }
3839 if let Some(val) = self.password.take() {
3840 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3841 }
3842 self.tls
3843 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3844 }
3845}
3846
3847impl SecretExtractor for MongoDbConfig {
3848 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3849 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3850 if let Some(val) = self.username.take() {
3851 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3852 }
3853 if let Some(val) = self.password.take() {
3854 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3855 }
3856 extract_sensitive_optional_url(
3858 &mut self.checkpoint_store,
3859 prefix,
3860 "CHECKPOINT_STORE",
3861 secrets,
3862 );
3863 self.tls
3864 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3865 }
3866}
3867
3868impl SecretExtractor for MqttConfig {
3869 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3870 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3871 if let Some(val) = self.username.take() {
3872 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3873 }
3874 if let Some(val) = self.password.take() {
3875 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3876 }
3877 self.tls
3878 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3879 }
3880}
3881
3882impl SecretExtractor for HttpConfig {
3883 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3884 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3885 if let Some((u, p)) = self.basic_auth.take() {
3886 secrets.insert(format!("{}__{}__{}", prefix, "BASIC_AUTH", 0), u);
3887 secrets.insert(format!("{}__{}__{}", prefix, "BASIC_AUTH", 1), p);
3888 }
3889 extract_sensitive_string_map_entries(
3890 &mut self.custom_headers,
3891 prefix,
3892 "CUSTOM_HEADERS",
3893 secrets,
3894 );
3895 self.tls
3896 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3897 if let Some(endpoint) = &mut self.stream_response_to {
3898 endpoint.extract_secrets(&format!("{}__{}", prefix, "STREAM_RESPONSE_TO"), secrets);
3899 }
3900 }
3901}
3902
3903impl SecretExtractor for WebSocketConfig {
3904 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3905 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3906 }
3907}
3908
3909impl SecretExtractor for IbmMqConfig {
3910 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3911 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3912 if let Some(val) = self.username.take() {
3913 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3914 }
3915 if let Some(val) = self.password.take() {
3916 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3917 }
3918 self.tls
3919 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3920 }
3921}
3922
3923impl SecretExtractor for ZeroMqConfig {
3924 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3925 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3926 }
3927}
3928
3929impl SecretExtractor for RedisStreamsConfig {
3930 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3931 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3932 if let Some(val) = self.username.take() {
3933 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3934 }
3935 if let Some(val) = self.password.take() {
3936 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3937 }
3938 }
3939}
3940
3941impl SecretExtractor for SqlxConfig {
3942 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3943 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3944 if let Some(val) = self.username.take() {
3945 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3946 }
3947 if let Some(val) = self.password.take() {
3948 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3949 }
3950 self.tls
3951 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3952 }
3953}
3954
3955impl SecretExtractor for ClickHouseConfig {
3956 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3957 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3958 if let Some(val) = self.username.take() {
3959 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
3960 }
3961 if let Some(val) = self.password.take() {
3962 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
3963 }
3964 if let Some(val) = self.checkpoint_store.take() {
3965 secrets.insert(format!("{}__{}", prefix, "CHECKPOINT_STORE"), val);
3966 }
3967 self.tls
3968 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3969 }
3970}
3971
3972impl SecretExtractor for PostgresCdcConfig {
3973 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3974 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3975 if let Some(val) = self.checkpoint_store.take() {
3976 secrets.insert(format!("{}__{}", prefix, "CHECKPOINT_STORE"), val);
3977 }
3978 self.tls
3979 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3980 }
3981}
3982
3983impl SecretExtractor for GrpcConfig {
3984 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3985 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
3986 self.tls
3987 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
3988 }
3989}
3990
3991impl SecretExtractor for TlsConfig {
3992 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
3993 if let Some(val) = self.cert_password.take() {
3994 secrets.insert(format!("{}__{}", prefix, "CERT_PASSWORD"), val);
3995 }
3996 }
3997}
3998
3999impl SecretExtractor for IbmTlsConfig {
4000 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
4001 if let Some(val) = self.key_repository_password.take() {
4002 secrets.insert(format!("{}__{}", prefix, "CERT_PASSWORD"), val);
4005 }
4006 }
4007}
4008
4009pub fn extract_config_secrets(config: &mut Config) -> HashMap<String, String> {
4016 let mut secrets = HashMap::new();
4017 for (route_name, route) in config.iter_mut() {
4018 let prefix = sanitize_secret_key(&format!("MQB__{}", route_name));
4019 route.extract_secrets(&prefix, &mut secrets);
4020 }
4021 secrets
4022}
4023
4024#[cfg(test)]
4025mod null_endpoint_tests {
4026 use super::*;
4027
4028 #[test]
4029 fn null_endpoint_json_round_trip() {
4030 let value = serde_json::to_value(Endpoint::null()).expect("serialize");
4031 let back: Endpoint = serde_json::from_value(value).expect("deserialize");
4032 assert!(matches!(back.endpoint_type, EndpointType::Null));
4033 }
4034
4035 #[test]
4037 fn null_endpoint_accepts_string_and_unit_forms() {
4038 for input in ["\"null\"", "null", "{}"] {
4039 let endpoint: Endpoint = serde_json::from_str(input).unwrap_or_else(|e| {
4040 panic!("failed to parse {input}: {e}");
4041 });
4042 assert!(matches!(endpoint.endpoint_type, EndpointType::Null));
4043 }
4044 }
4045
4046 #[test]
4047 fn unknown_endpoint_string_is_rejected() {
4048 let err = serde_json::from_str::<Endpoint>("\"kafka\"").expect_err("should fail");
4049 assert!(
4050 err.to_string().contains("unknown variant"),
4051 "unexpected error: {err}"
4052 );
4053 }
4054
4055 #[test]
4056 fn nested_null_endpoint_json_round_trip() {
4057 let config =
4058 HttpConfig::new("http://localhost:8080").with_stream_response_to(Endpoint::null());
4059 let value = serde_json::to_value(&config).expect("serialize");
4060 let back: HttpConfig = serde_json::from_value(value).expect("deserialize");
4061 let nested = back.stream_response_to.expect("stream_response_to present");
4062 assert!(matches!(nested.endpoint_type, EndpointType::Null));
4063 }
4064
4065 #[test]
4066 fn nested_null_endpoint_yaml_forms() {
4067 for yaml in [
4068 "url: http://localhost:8080\nstream_response_to: \"null\"\n",
4069 "url: http://localhost:8080\nstream_response_to: {}\n",
4070 ] {
4071 let config: HttpConfig = serde_yaml_ng::from_str(yaml)
4072 .unwrap_or_else(|e| panic!("failed to parse {yaml:?}: {e}"));
4073 let nested = config
4074 .stream_response_to
4075 .expect("stream_response_to present");
4076 assert!(matches!(nested.endpoint_type, EndpointType::Null));
4077 }
4078 }
4079
4080 #[test]
4083 fn nested_bare_null_yaml_is_none() {
4084 let config: HttpConfig =
4085 serde_yaml_ng::from_str("url: http://localhost:8080\nstream_response_to: null\n")
4086 .expect("deserialize");
4087 assert!(config.stream_response_to.is_none());
4088 }
4089
4090 #[test]
4091 fn null_endpoint_yaml_round_trip() {
4092 let yaml = serde_yaml_ng::to_string(&Endpoint::null()).expect("serialize");
4093 let back: Endpoint = serde_yaml_ng::from_str(&yaml).expect("deserialize");
4094 assert!(matches!(back.endpoint_type, EndpointType::Null));
4095 }
4096}
4097
4098#[cfg(test)]
4099mod tests {
4100 use super::*;
4101 use config::{Config as ConfigBuilder, Environment};
4102
4103 const TEST_YAML: &str = r#"
4104kafka_to_nats:
4105 concurrency: 10
4106 input:
4107 middlewares:
4108 - deduplication:
4109 sled_path: "/tmp/mq-bridge/dedup_db"
4110 ttl_seconds: 3600
4111 - metrics: {}
4112 - retry:
4113 max_attempts: 5
4114 initial_interval_ms: 200
4115 - random_panic:
4116 mode: nack
4117 - dlq:
4118 endpoint:
4119 nats:
4120 subject: "dlq-subject"
4121 url: "nats://localhost:4222"
4122 kafka:
4123 topic: "input-topic"
4124 url: "localhost:9092"
4125 group_id: "my-consumer-group"
4126 tls:
4127 required: true
4128 ca_file: "/path_to_ca"
4129 cert_file: "/path_to_cert"
4130 key_file: "/path_to_key"
4131 cert_password: "password"
4132 accept_invalid_certs: true
4133 output:
4134 middlewares:
4135 - metrics: {}
4136 - dlq:
4137 endpoint:
4138 file:
4139 path: "error.out"
4140 nats:
4141 subject: "output-subject"
4142 url: "nats://localhost:4222"
4143"#;
4144
4145 fn assert_config_values(config: &Config) {
4146 assert_eq!(config.len(), 1);
4147 let route = config.get("kafka_to_nats").expect("Route should exist");
4148
4149 assert_eq!(route.options.concurrency, 10);
4150
4151 let input = &route.input;
4153 assert_eq!(input.middlewares.len(), 5);
4154
4155 let mut has_dedup = false;
4156 let mut has_metrics = false;
4157 let mut has_dlq = false;
4158 let mut has_retry = false;
4159 let mut has_random_panic = false;
4160 for middleware in &input.middlewares {
4161 match middleware {
4162 Middleware::Deduplication(dedup) => {
4163 assert_eq!(dedup.sled_path.as_deref(), Some("/tmp/mq-bridge/dedup_db"));
4164 assert_eq!(dedup.ttl_seconds, 3600);
4165 has_dedup = true;
4166 }
4167 Middleware::Metrics(_) => {
4168 has_metrics = true;
4169 }
4170 Middleware::Custom { .. } => {}
4171 Middleware::Dlq(dlq) => {
4172 assert!(dlq.endpoint.middlewares.is_empty());
4173 if let EndpointType::Nats(nats_cfg) = &dlq.endpoint.endpoint_type {
4174 assert_eq!(nats_cfg.subject, Some("dlq-subject".to_string()));
4175 assert_eq!(nats_cfg.url, "nats://localhost:4222");
4176 }
4177 has_dlq = true;
4178 }
4179 Middleware::Retry(retry) => {
4180 assert_eq!(retry.max_attempts, 5);
4181 assert_eq!(retry.initial_interval_ms, 200);
4182 has_retry = true;
4183 }
4184 Middleware::RandomPanic(rp) => {
4185 assert!(rp.mode == FaultMode::Nack);
4186 has_random_panic = true;
4187 }
4188 Middleware::Delay(_) => {}
4189 Middleware::WeakJoin(_) => {}
4190 Middleware::Limiter(_) => {}
4191 Middleware::Buffer(_) => {}
4192 Middleware::CookieJar(_) => {}
4193 Middleware::Transform(_) => {}
4194 Middleware::Encryption(_) => {}
4195 Middleware::Compression(_) => {}
4196 }
4197 }
4198
4199 if let EndpointType::Kafka(kafka) = &input.endpoint_type {
4200 assert_eq!(kafka.topic, Some("input-topic".to_string()));
4201 assert_eq!(kafka.url, "localhost:9092");
4202 assert_eq!(kafka.group_id, Some("my-consumer-group".to_string()));
4203 let tls = &kafka.tls;
4204 assert!(tls.required);
4205 assert_eq!(tls.ca_file.as_deref(), Some("/path_to_ca"));
4206 assert!(tls.accept_invalid_certs);
4207 } else {
4208 panic!("Input endpoint should be Kafka");
4209 }
4210 assert!(has_dedup);
4211 assert!(has_metrics);
4212 assert!(has_dlq);
4213 assert!(has_retry);
4214 assert!(has_random_panic);
4215
4216 let output = &route.output;
4218 assert_eq!(output.middlewares.len(), 2);
4219 assert!(matches!(output.middlewares[0], Middleware::Metrics(_)));
4220
4221 if let EndpointType::Nats(nats) = &output.endpoint_type {
4222 assert_eq!(nats.subject, Some("output-subject".to_string()));
4223 assert_eq!(nats.url, "nats://localhost:4222");
4224 } else {
4225 panic!("Output endpoint should be NATS");
4226 }
4227 }
4228
4229 #[test]
4230 fn test_deserialize_from_yaml() {
4231 let result: Result<Config, _> = serde_yaml_ng::from_str(TEST_YAML);
4234 println!("Deserialized from YAML: {:#?}", result);
4235 let config = result.expect("Failed to deserialize TEST_YAML");
4236 assert_config_values(&config);
4237 }
4238
4239 #[test]
4240 fn test_deserialize_from_env() {
4241 unsafe {
4243 std::env::set_var("MQB__KAFKA_TO_NATS__CONCURRENCY", "10");
4244 std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__TOPIC", "input-topic");
4245 std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__URL", "localhost:9092");
4246 std::env::set_var(
4247 "MQB__KAFKA_TO_NATS__INPUT__KAFKA__GROUP_ID",
4248 "my-consumer-group",
4249 );
4250 std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__REQUIRED", "true");
4251 std::env::set_var(
4252 "MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__CA_FILE",
4253 "/path_to_ca",
4254 );
4255 std::env::set_var(
4256 "MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__ACCEPT_INVALID_CERTS",
4257 "true",
4258 );
4259 std::env::set_var(
4260 "MQB__KAFKA_TO_NATS__OUTPUT__NATS__SUBJECT",
4261 "output-subject",
4262 );
4263 std::env::set_var(
4264 "MQB__KAFKA_TO_NATS__OUTPUT__NATS__URL",
4265 "nats://localhost:4222",
4266 );
4267 std::env::set_var(
4268 "MQB__KAFKA_TO_NATS__INPUT__MIDDLEWARES__0__DLQ__ENDPOINT__NATS__SUBJECT",
4269 "dlq-subject",
4270 );
4271 std::env::set_var(
4272 "MQB__KAFKA_TO_NATS__INPUT__MIDDLEWARES__0__DLQ__ENDPOINT__NATS__URL",
4273 "nats://localhost:4222",
4274 );
4275 }
4276
4277 let builder = ConfigBuilder::builder()
4278 .add_source(
4280 Environment::with_prefix("MQB")
4281 .separator("__")
4282 .try_parsing(true),
4283 );
4284
4285 let config: Config = builder
4286 .build()
4287 .expect("Failed to build config")
4288 .try_deserialize()
4289 .expect("Failed to deserialize config");
4290
4291 assert_eq!(config.get("kafka_to_nats").unwrap().options.concurrency, 10);
4293 if let EndpointType::Kafka(k) = &config.get("kafka_to_nats").unwrap().input.endpoint_type {
4294 assert_eq!(k.topic, Some("input-topic".to_string()));
4295 assert!(k.tls.required);
4296 } else {
4297 panic!("Expected Kafka endpoint");
4298 }
4299
4300 let input = &config.get("kafka_to_nats").unwrap().input;
4301 assert_eq!(input.middlewares.len(), 1);
4302 if let Middleware::Dlq(_) = &input.middlewares[0] {
4303 } else {
4305 panic!("Expected DLQ middleware");
4306 }
4307 }
4308
4309 #[test]
4310 fn test_extract_secrets() {
4311 let mut config = Config::new();
4312 let mut route = Route::default();
4313
4314 let mut kafka_config = KafkaConfig::new("kafka://user:pass@localhost:9092");
4316 kafka_config.username = Some("user".to_string());
4317 kafka_config.password = Some("pass".to_string());
4318 kafka_config.tls.cert_password = Some("certpass".to_string());
4319
4320 route.input = Endpoint {
4321 endpoint_type: EndpointType::Kafka(kafka_config),
4322 middlewares: vec![],
4323 handler: None,
4324 };
4325
4326 let mut http_config = HttpConfig::new("http://httpuser:httppass@localhost");
4328 http_config.basic_auth = Some(("httpuser".to_string(), "httppass".to_string()));
4329 http_config
4330 .custom_headers
4331 .insert("X-API-Key".to_string(), "http-api-key".to_string());
4332 http_config.custom_headers.insert(
4333 "X-Access-Token".to_string(),
4334 "http-access-token".to_string(),
4335 );
4336 http_config.custom_headers.insert(
4337 "X-Authentication".to_string(),
4338 "http-authentication".to_string(),
4339 );
4340 http_config.custom_headers.insert(
4341 "Authorization".to_string(),
4342 "Bearer secret-token".to_string(),
4343 );
4344 http_config
4345 .custom_headers
4346 .insert("X-Trace-Id".to_string(), "trace-value".to_string());
4347
4348 route.output = Endpoint {
4349 endpoint_type: EndpointType::Http(http_config),
4350 middlewares: vec![],
4351 handler: None,
4352 };
4353
4354 config.insert("test_route".to_string(), route);
4355
4356 let secrets = extract_config_secrets(&mut config);
4357
4358 assert_eq!(
4360 secrets
4361 .get("MQB__TEST_ROUTE__INPUT__KAFKA__URL")
4362 .map(|s| s.as_str()),
4363 Some("kafka://user:pass@localhost:9092")
4364 );
4365 assert_eq!(
4366 secrets
4367 .get("MQB__TEST_ROUTE__INPUT__KAFKA__USERNAME")
4368 .map(|s| s.as_str()),
4369 Some("user")
4370 );
4371 assert_eq!(
4372 secrets
4373 .get("MQB__TEST_ROUTE__INPUT__KAFKA__PASSWORD")
4374 .map(|s| s.as_str()),
4375 Some("pass")
4376 );
4377 assert_eq!(
4378 secrets
4379 .get("MQB__TEST_ROUTE__INPUT__KAFKA__TLS__CERT_PASSWORD")
4380 .map(|s| s.as_str()),
4381 Some("certpass")
4382 );
4383 assert_eq!(
4384 secrets
4385 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__URL")
4386 .map(|s| s.as_str()),
4387 Some("http://httpuser:httppass@localhost")
4388 );
4389 assert_eq!(
4390 secrets
4391 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__BASIC_AUTH__0")
4392 .map(|s| s.as_str()),
4393 Some("httpuser")
4394 );
4395 assert_eq!(
4396 secrets
4397 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__BASIC_AUTH__1")
4398 .map(|s| s.as_str()),
4399 Some("httppass")
4400 );
4401 assert_eq!(
4402 secrets
4403 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_API_KEY")
4404 .map(|s| s.as_str()),
4405 Some("http-api-key")
4406 );
4407 assert_eq!(
4408 secrets
4409 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_ACCESS_TOKEN")
4410 .map(|s| s.as_str()),
4411 Some("http-access-token")
4412 );
4413 assert_eq!(
4414 secrets
4415 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_AUTHENTICATION")
4416 .map(|s| s.as_str()),
4417 Some("http-authentication")
4418 );
4419 assert_eq!(
4420 secrets
4421 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__AUTHORIZATION")
4422 .map(|s| s.as_str()),
4423 Some("Bearer secret-token")
4424 );
4425
4426 let route = config.get("test_route").unwrap();
4428 if let EndpointType::Kafka(k) = &route.input.endpoint_type {
4429 assert!(k.url.is_empty());
4430 assert!(k.username.is_none());
4431 assert!(k.password.is_none());
4432 assert!(k.tls.cert_password.is_none());
4433 }
4434 if let EndpointType::Http(h) = &route.output.endpoint_type {
4435 assert!(h.url.is_empty());
4436 assert!(h.basic_auth.is_none());
4437 assert!(!h.custom_headers.contains_key("X-API-Key"));
4438 assert!(!h.custom_headers.contains_key("X-Access-Token"));
4439 assert!(!h.custom_headers.contains_key("X-Authentication"));
4440 assert!(!h.custom_headers.contains_key("Authorization"));
4441 assert_eq!(
4442 h.custom_headers.get("X-Trace-Id").map(|s| s.as_str()),
4443 Some("trace-value")
4444 );
4445 }
4446 }
4447
4448 #[test]
4449 fn test_extract_sensitive_url_only_strips_authority_credentials() {
4450 let mut config = Config::new();
4451 let path_at_route = Route {
4452 output: Endpoint {
4453 endpoint_type: EndpointType::Http(HttpConfig::new(
4454 "https://example.com/path/user@example.com?email=a@b.test",
4455 )),
4456 middlewares: vec![],
4457 handler: None,
4458 },
4459 ..Default::default()
4460 };
4461 config.insert("path_at_route".to_string(), path_at_route);
4462
4463 let credential_route = Route {
4464 output: Endpoint {
4465 endpoint_type: EndpointType::Http(HttpConfig::new(
4466 "https://user:pass@example.com/path",
4467 )),
4468 middlewares: vec![],
4469 handler: None,
4470 },
4471 ..Default::default()
4472 };
4473 config.insert("credential_route".to_string(), credential_route);
4474
4475 let query_at_route = Route {
4476 output: Endpoint {
4477 endpoint_type: EndpointType::Http(HttpConfig::new(
4478 "https://example.com?next=a@b.test",
4479 )),
4480 middlewares: vec![],
4481 handler: None,
4482 },
4483 ..Default::default()
4484 };
4485 config.insert("query_at_route".to_string(), query_at_route);
4486
4487 let fragment_at_route = Route {
4488 output: Endpoint {
4489 endpoint_type: EndpointType::Http(HttpConfig::new(
4490 "https://example.com#user@example.com",
4491 )),
4492 middlewares: vec![],
4493 handler: None,
4494 },
4495 ..Default::default()
4496 };
4497 config.insert("fragment_at_route".to_string(), fragment_at_route);
4498
4499 let secrets = extract_config_secrets(&mut config);
4500
4501 if let EndpointType::Http(http) = &config.get("path_at_route").unwrap().output.endpoint_type
4502 {
4503 assert_eq!(
4504 http.url,
4505 "https://example.com/path/user@example.com?email=a@b.test"
4506 );
4507 }
4508 if let EndpointType::Http(http) =
4509 &config.get("query_at_route").unwrap().output.endpoint_type
4510 {
4511 assert_eq!(http.url, "https://example.com?next=a@b.test");
4512 }
4513 if let EndpointType::Http(http) = &config
4514 .get("fragment_at_route")
4515 .unwrap()
4516 .output
4517 .endpoint_type
4518 {
4519 assert_eq!(http.url, "https://example.com#user@example.com");
4520 }
4521 if let EndpointType::Http(http) =
4522 &config.get("credential_route").unwrap().output.endpoint_type
4523 {
4524 assert!(http.url.is_empty());
4525 }
4526 assert_eq!(
4527 secrets
4528 .get("MQB__CREDENTIAL_ROUTE__OUTPUT__HTTP__URL")
4529 .map(String::as_str),
4530 Some("https://user:pass@example.com/path")
4531 );
4532 assert!(!secrets.contains_key("MQB__PATH_AT_ROUTE__OUTPUT__HTTP__URL"));
4533 assert!(!secrets.contains_key("MQB__QUERY_AT_ROUTE__OUTPUT__HTTP__URL"));
4534 assert!(!secrets.contains_key("MQB__FRAGMENT_AT_ROUTE__OUTPUT__HTTP__URL"));
4535 }
4536
4537 #[test]
4538 fn test_memory_config_requires_topic_or_url() {
4539 let err = serde_yaml_ng::from_str::<MemoryConfig>("{}").unwrap_err();
4540 assert!(err
4541 .to_string()
4542 .contains("MemoryConfig: 'topic' (or 'url' alias) is required."));
4543 }
4544
4545 #[test]
4546 fn test_file_config_inference() {
4547 let yaml = r#"
4548mode: group_subscribe
4549path: "/tmp/test"
4550group_id: "my_group"
4551"#;
4552 let config: FileConfig = serde_yaml_ng::from_str(yaml).unwrap();
4553 match config.mode {
4554 Some(FileConsumerMode::GroupSubscribe { group_id, .. }) => {
4555 assert_eq!(group_id, "my_group")
4556 }
4557 _ => panic!("Expected GroupSubscribe"),
4558 }
4559
4560 let yaml_queue = r#"
4561mode: consume
4562path: "/tmp/test"
4563"#;
4564 let config_queue: FileConfig = serde_yaml_ng::from_str(yaml_queue).unwrap();
4565 match config_queue.mode {
4566 Some(FileConsumerMode::Consume { delete }) => assert!(!delete),
4567 _ => panic!("Expected Consume"),
4568 }
4569 }
4570}
4571
4572#[cfg(all(test, feature = "schema"))]
4573mod schema_tests {
4574 use super::*;
4575
4576 #[test]
4577 fn generate_json_schema() {
4578 let schema = schemars::schema_for!(Config);
4579 let schema_json = serde_json::to_string_pretty(&schema).unwrap();
4580
4581 let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
4582 path.push("mq-bridge.schema.json");
4583 std::fs::write(path, schema_json).expect("Failed to write schema file");
4584 }
4585}