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}
167
168impl Default for RouteOptions {
169 fn default() -> Self {
170 Self {
171 description: String::new(),
172 concurrency: default_concurrency(),
173 batch_size: default_batch_size(),
174 commit_concurrency_limit: default_commit_concurrency_limit(),
175 startup_timeout_ms: default_startup_timeout_ms(),
176 reconnect_interval_ms: default_reconnect_interval_ms(),
177 empty_batch_delay_ms: default_empty_batch_delay_ms(),
178 allow_fault_injection: false,
179 }
180 }
181}
182
183impl RouteOptions {
184 pub fn validate(&self) -> anyhow::Result<()> {
185 if self.concurrency == 0 {
186 return Err(anyhow::anyhow!("route concurrency must be at least 1"));
187 }
188 if self.batch_size == 0 {
189 return Err(anyhow::anyhow!("route batch_size must be at least 1"));
190 }
191 if self.commit_concurrency_limit == 0 {
192 return Err(anyhow::anyhow!(
193 "route commit_concurrency_limit must be at least 1"
194 ));
195 }
196 Ok(())
197 }
198}
199
200pub(crate) fn default_concurrency() -> usize {
201 1
202}
203
204pub(crate) fn default_batch_size() -> usize {
205 1
206}
207
208pub(crate) fn default_commit_concurrency_limit() -> usize {
209 4096
210}
211
212pub(crate) fn default_startup_timeout_ms() -> u64 {
213 5000
214}
215
216pub(crate) fn default_reconnect_interval_ms() -> u64 {
217 5000
218}
219
220pub(crate) fn default_empty_batch_delay_ms() -> u64 {
221 10
222}
223
224fn is_false(value: &bool) -> bool {
225 !*value
226}
227
228fn default_false() -> bool {
229 false
230}
231
232#[cfg(feature = "schema")]
233fn default_inline_response_fast_path_schema() -> Option<bool> {
234 Some(true)
235}
236
237fn default_output_endpoint() -> Endpoint {
238 Endpoint::new(EndpointType::Null)
239}
240
241fn default_retry_attempts() -> usize {
242 3
243}
244fn default_initial_interval_ms() -> u64 {
245 100
246}
247fn default_max_interval_ms() -> u64 {
248 5000
249}
250fn default_multiplier() -> f64 {
251 2.0
252}
253fn default_clean_session() -> bool {
254 false
255}
256fn default_cookie_metadata_key() -> String {
257 "cookie".to_string()
258}
259fn default_set_cookie_metadata_key() -> String {
260 "set-cookie".to_string()
261}
262
263fn is_known_endpoint_name(name: &str) -> bool {
264 matches!(
265 name,
266 "aws"
267 | "kafka"
268 | "nats"
269 | "file"
270 | "static"
271 | "memory"
272 | "sled"
273 | "amqp"
274 | "mongodb"
275 | "mqtt"
276 | "http"
277 | "websocket"
278 | "ibmmq"
279 | "zeromq"
280 | "grpc"
281 | "fanout"
282 | "stream_buffer"
283 | "ref"
284 | "switch"
285 | "response"
286 | "reader"
287 | "null"
288 | "sqlx"
289 )
290}
291
292#[derive(Serialize, Clone, Default)]
294#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
295#[serde(deny_unknown_fields)]
296pub struct Endpoint {
297 #[serde(default)]
299 pub middlewares: Vec<Middleware>,
300
301 #[serde(flatten)]
303 pub endpoint_type: EndpointType,
304
305 #[serde(skip_serializing)]
306 #[cfg_attr(feature = "schema", schemars(skip))]
307 pub handler: Option<Arc<dyn Handler>>,
309}
310
311impl std::fmt::Debug for Endpoint {
312 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 f.debug_struct("Endpoint")
314 .field("middlewares", &self.middlewares)
315 .field("endpoint_type", &self.endpoint_type)
316 .field(
317 "handler",
318 &if self.handler.is_some() {
319 "Some(<Handler>)"
320 } else {
321 "None"
322 },
323 )
324 .finish()
325 }
326}
327
328impl<'de> Deserialize<'de> for Endpoint {
329 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
330 where
331 D: Deserializer<'de>,
332 {
333 struct EndpointVisitor;
334
335 impl<'de> Visitor<'de> for EndpointVisitor {
336 type Value = Endpoint;
337
338 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
339 formatter.write_str("a map representing an endpoint or null")
340 }
341
342 fn visit_unit<E>(self) -> Result<Self::Value, E>
343 where
344 E: serde::de::Error,
345 {
346 Ok(Endpoint::new(EndpointType::Null))
347 }
348
349 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
350 where
351 A: MapAccess<'de>,
352 {
353 let mut temp_map = serde_json::Map::new();
356 let mut middlewares_val = None;
357
358 while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
359 if key == "middlewares" {
360 middlewares_val = Some(value);
361 } else {
362 temp_map.insert(key, value);
363 }
364 }
365
366 let temp_val = serde_json::Value::Object(temp_map);
368 let endpoint_type: EndpointType = match serde_json::from_value(temp_val.clone()) {
369 Ok(et) => et,
370 Err(original_err) => {
371 if let serde_json::Value::Object(map) = &temp_val {
372 if map.len() == 1 {
373 let (name, config) = map.iter().next().unwrap();
374 if is_known_endpoint_name(name) {
375 return Err(serde::de::Error::custom(original_err));
376 }
377 trace!("Falling back to Custom endpoint for key: {}", name);
378 EndpointType::Custom {
379 name: name.clone(),
380 config: config.clone(),
381 }
382 } else if map.is_empty() {
383 EndpointType::Null
384 } else {
385 return Err(serde::de::Error::custom(
386 "Invalid endpoint configuration: multiple keys found or unknown endpoint type",
387 ));
388 }
389 } else {
390 return Err(serde::de::Error::custom("Invalid endpoint configuration"));
391 }
392 }
393 };
394
395 let middlewares = match middlewares_val {
397 Some(val) => {
398 deserialize_middlewares_from_value(val).map_err(serde::de::Error::custom)?
399 }
400 None => Vec::new(),
401 };
402
403 Ok(Endpoint {
404 middlewares,
405 endpoint_type,
406 handler: None,
407 })
408 }
409 }
410
411 deserializer.deserialize_any(EndpointVisitor)
412 }
413}
414
415fn is_known_middleware_name(name: &str) -> bool {
416 matches!(
417 name,
418 "deduplication"
419 | "metrics"
420 | "dlq"
421 | "retry"
422 | "random_panic"
423 | "delay"
424 | "weak_join"
425 | "limiter"
426 | "buffer"
427 | "cookie_jar"
428 | "custom"
429 )
430}
431
432fn deserialize_middlewares_from_value(value: serde_json::Value) -> anyhow::Result<Vec<Middleware>> {
436 let arr = match value {
437 serde_json::Value::Array(arr) => arr,
438 serde_json::Value::Object(map) => {
439 let mut middlewares: Vec<_> = map
440 .into_iter()
441 .filter_map(|(key, value)| key.parse::<usize>().ok().map(|index| (index, value)))
444 .collect();
445 middlewares.sort_by_key(|(index, _)| *index);
446
447 middlewares.into_iter().map(|(_, value)| value).collect()
448 }
449 _ => return Err(anyhow::anyhow!("Expected an array or object")),
450 };
451
452 let mut middlewares = Vec::new();
453 for item in arr {
454 let known_name = if let serde_json::Value::Object(map) = &item {
456 if map.len() == 1 {
457 let (name, _) = map.iter().next().unwrap();
458 if is_known_middleware_name(name) {
459 Some(name.clone())
460 } else {
461 None
462 }
463 } else {
464 None
465 }
466 } else {
467 None
468 };
469
470 if let Some(name) = known_name {
471 match serde_json::from_value::<Middleware>(item.clone()) {
472 Ok(m) => middlewares.push(m),
473 Err(e) => {
474 return Err(anyhow::anyhow!(
475 "Failed to deserialize known middleware '{}': {}",
476 name,
477 e
478 ))
479 }
480 }
481 } else if let Ok(m) = serde_json::from_value::<Middleware>(item.clone()) {
482 middlewares.push(m);
483 } else if let serde_json::Value::Object(map) = &item {
484 if map.len() == 1 {
485 let (name, config) = map.iter().next().unwrap();
486 middlewares.push(Middleware::Custom {
487 name: name.clone(),
488 config: config.clone(),
489 });
490 } else {
491 return Err(anyhow::anyhow!(
492 "Invalid middleware configuration: {:?}",
493 item
494 ));
495 }
496 } else {
497 return Err(anyhow::anyhow!(
498 "Invalid middleware configuration: {:?}",
499 item
500 ));
501 }
502 }
503 Ok(middlewares)
504}
505
506#[derive(Debug, Clone, Default)]
529pub struct StaticConfig {
530 pub body: String,
532 pub raw: bool,
534 pub metadata: std::collections::HashMap<String, String>,
536}
537
538#[cfg(feature = "schema")]
542impl schemars::JsonSchema for StaticConfig {
543 fn schema_name() -> std::borrow::Cow<'static, str> {
544 "StaticConfig".into()
545 }
546
547 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
548 schemars::json_schema!({
549 "description": "Configuration for the `static` endpoint. Accepts either a bare string (the response body, JSON-encoded for backward compatibility) or a map where only `body` is required and `raw` / `metadata` are optional.",
550 "oneOf": [
551 {
552 "type": "string",
553 "description": "The response body, JSON-encoded as a string."
554 },
555 {
556 "type": "object",
557 "properties": {
558 "body": {
559 "type": "string",
560 "description": "The static response body."
561 },
562 "raw": {
563 "type": "boolean",
564 "description": "Send the body verbatim instead of JSON-encoding it as a string.",
565 "default": false
566 },
567 "metadata": {
568 "type": "object",
569 "description": "Extra metadata entries attached to the produced message.",
570 "additionalProperties": { "type": "string" }
571 }
572 },
573 "required": ["body"],
574 "additionalProperties": false
575 }
576 ]
577 })
578 }
579}
580
581impl Serialize for StaticConfig {
582 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
583 where
584 S: serde::Serializer,
585 {
586 if !self.raw && self.metadata.is_empty() {
590 return serializer.serialize_str(&self.body);
591 }
592 use serde::ser::SerializeStruct;
593 let mut state = serializer.serialize_struct("StaticConfig", 3)?;
594 state.serialize_field("body", &self.body)?;
595 state.serialize_field("raw", &self.raw)?;
596 state.serialize_field("metadata", &self.metadata)?;
597 state.end()
598 }
599}
600
601impl From<String> for StaticConfig {
602 fn from(body: String) -> Self {
603 StaticConfig {
604 body,
605 raw: false,
606 metadata: std::collections::HashMap::new(),
607 }
608 }
609}
610
611impl From<&str> for StaticConfig {
612 fn from(body: &str) -> Self {
613 StaticConfig::from(body.to_string())
614 }
615}
616
617impl<'de> Deserialize<'de> for StaticConfig {
618 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
619 where
620 D: serde::Deserializer<'de>,
621 {
622 #[derive(Deserialize)]
623 #[serde(untagged)]
624 enum Repr {
625 Str(String),
626 Map {
627 body: String,
628 #[serde(default)]
629 raw: bool,
630 #[serde(default)]
631 metadata: std::collections::HashMap<String, String>,
632 },
633 }
634 Ok(match Repr::deserialize(deserializer)? {
635 Repr::Str(body) => StaticConfig {
636 body,
637 raw: false,
638 metadata: std::collections::HashMap::new(),
639 },
640 Repr::Map {
641 body,
642 raw,
643 metadata,
644 } => StaticConfig {
645 body,
646 raw,
647 metadata,
648 },
649 })
650 }
651}
652
653#[derive(Debug, Deserialize, Serialize, Clone, Default)]
675#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
676#[serde(rename_all = "lowercase")]
677pub enum EndpointType {
678 Aws(AwsConfig),
679 Kafka(KafkaConfig),
680 Nats(NatsConfig),
681 File(FileConfig),
682 Static(StaticConfig),
683 Ref(String),
684 Memory(MemoryConfig),
685 Sled(SledConfig),
686 Amqp(AmqpConfig),
687 MongoDb(MongoDbConfig),
688 Mqtt(MqttConfig),
689 Http(HttpConfig),
690 WebSocket(WebSocketConfig),
691 IbmMq(IbmMqConfig),
692 ZeroMq(ZeroMqConfig),
693 Grpc(GrpcConfig),
694 Sqlx(SqlxConfig),
695 Fanout(Vec<Endpoint>),
696 #[serde(rename = "stream_buffer")]
697 StreamBuffer(StreamBufferConfig),
698 Switch(SwitchConfig),
699 Response(ResponseConfig),
700 Reader(Box<Endpoint>),
701 Custom {
702 name: String,
703 config: serde_json::Value,
704 },
705 #[default]
706 Null,
707}
708
709impl EndpointType {
710 pub fn name(&self) -> &'static str {
711 match self {
712 EndpointType::Aws(_) => "aws",
713 EndpointType::Kafka(_) => "kafka",
714 EndpointType::Nats(_) => "nats",
715 EndpointType::File(_) => "file",
716 EndpointType::Static(_) => "static",
717 EndpointType::Ref(_) => "ref",
718 EndpointType::Memory(_) => "memory",
719 EndpointType::Sled(_) => "sled",
720 EndpointType::Amqp(_) => "amqp",
721 EndpointType::MongoDb(_) => "mongodb",
722 EndpointType::Mqtt(_) => "mqtt",
723 EndpointType::Http(_) => "http",
724 EndpointType::WebSocket(_) => "websocket",
725 EndpointType::IbmMq(_) => "ibmmq",
726 EndpointType::ZeroMq(_) => "zeromq",
727 EndpointType::Grpc(_) => "grpc",
728 EndpointType::Sqlx(_) => "sqlx",
729 EndpointType::Fanout(_) => "fanout",
730 EndpointType::StreamBuffer(_) => "stream_buffer",
731 EndpointType::Switch(_) => "switch",
732 EndpointType::Response(_) => "response",
733 EndpointType::Reader(_) => "reader",
734 EndpointType::Custom { .. } => "custom",
735 EndpointType::Null => "null",
736 }
737 }
738
739 pub fn is_core(&self) -> bool {
740 matches!(
741 self,
742 EndpointType::File(_)
743 | EndpointType::Static(_)
744 | EndpointType::Ref(_)
745 | EndpointType::Memory(_)
746 | EndpointType::Fanout(_)
747 | EndpointType::StreamBuffer(_)
748 | EndpointType::Switch(_)
749 | EndpointType::Response(_)
750 | EndpointType::Reader(_)
751 | EndpointType::Custom { .. }
752 | EndpointType::Null
753 )
754 }
755}
756
757#[derive(Debug, Deserialize, Serialize, Clone)]
759#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
760#[serde(rename_all = "snake_case")]
761pub enum Middleware {
762 Deduplication(DeduplicationMiddleware),
763 Metrics(MetricsMiddleware),
764 Dlq(Box<DeadLetterQueueMiddleware>),
765 Retry(RetryMiddleware),
766 RandomPanic(RandomPanicMiddleware),
767 Delay(DelayMiddleware),
768 WeakJoin(WeakJoinMiddleware),
769 Limiter(LimiterMiddleware),
770 Buffer(BufferMiddleware),
771 CookieJar(CookieJarMiddleware),
772 Custom {
773 name: String,
774 config: serde_json::Value,
775 },
776}
777
778#[derive(Debug, Deserialize, Serialize, Clone)]
783#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
784#[serde(deny_unknown_fields)]
785pub struct DeduplicationMiddleware {
786 pub sled_path: String,
788 pub ttl_seconds: u64,
790}
791
792#[derive(Debug, Deserialize, Serialize, Clone)]
800#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
801#[serde(deny_unknown_fields)]
802pub struct MetricsMiddleware {}
803
804#[derive(Debug, Deserialize, Serialize, Clone, Default)]
811#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
812#[serde(deny_unknown_fields)]
813pub struct DeadLetterQueueMiddleware {
814 pub endpoint: Endpoint,
816}
817
818#[derive(Debug, Deserialize, Serialize, Clone, Default)]
823#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
824#[serde(deny_unknown_fields)]
825pub struct RetryMiddleware {
826 #[serde(default = "default_retry_attempts")]
828 pub max_attempts: usize,
829 #[serde(default = "default_initial_interval_ms")]
831 pub initial_interval_ms: u64,
832 #[serde(default = "default_max_interval_ms")]
834 pub max_interval_ms: u64,
835 #[serde(default = "default_multiplier")]
837 pub multiplier: f64,
838}
839
840#[derive(Debug, Deserialize, Serialize, Clone)]
845#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
846#[serde(deny_unknown_fields)]
847pub struct DelayMiddleware {
848 pub delay_ms: u64,
850}
851
852#[derive(Debug, Deserialize, Serialize, Clone)]
858#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
859#[serde(deny_unknown_fields)]
860pub struct LimiterMiddleware {
861 pub messages_per_second: f64,
863}
864
865#[derive(Debug, Deserialize, Serialize, Clone)]
870#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
871#[serde(deny_unknown_fields)]
872pub struct BufferMiddleware {
873 pub max_messages: usize,
875 pub max_delay_ms: u64,
877}
878
879#[derive(Debug, Deserialize, Serialize, Clone)]
887#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
888#[serde(deny_unknown_fields)]
889pub struct CookieJarMiddleware {
890 #[serde(default)]
893 pub shared_scope: Option<String>,
894 #[serde(default = "default_cookie_metadata_key")]
896 pub cookie_metadata_key: String,
897 #[serde(default = "default_set_cookie_metadata_key")]
899 pub set_cookie_metadata_key: String,
900 #[serde(default)]
902 pub capture_metadata_keys: Vec<String>,
903 #[serde(default)]
908 pub export_metadata_prefix: Option<String>,
909 #[serde(default)]
914 pub inject_metadata: HashMap<String, String>,
915}
916
917impl Default for CookieJarMiddleware {
918 fn default() -> Self {
919 Self {
920 shared_scope: None,
921 cookie_metadata_key: default_cookie_metadata_key(),
922 set_cookie_metadata_key: default_set_cookie_metadata_key(),
923 capture_metadata_keys: Vec::new(),
924 export_metadata_prefix: None,
925 inject_metadata: HashMap::new(),
926 }
927 }
928}
929
930#[derive(Debug, Deserialize, Serialize, Clone)]
936#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
937#[serde(deny_unknown_fields)]
938pub struct WeakJoinMiddleware {
939 pub group_by: String,
941 pub expected_count: usize,
943 pub timeout_ms: u64,
945}
946
947#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
949#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
950#[serde(rename_all = "snake_case")]
951pub enum FaultMode {
952 #[default]
954 Panic,
955 Disconnect,
957 Timeout,
959 JsonFormatError,
961 Nack,
963}
964
965impl std::fmt::Display for FaultMode {
966 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
967 match self {
968 FaultMode::Panic => write!(f, "panic"),
969 FaultMode::Disconnect => write!(f, "disconnect"),
970 FaultMode::Timeout => write!(f, "timeout"),
971 FaultMode::JsonFormatError => write!(f, "json_format_error"),
972 FaultMode::Nack => write!(f, "nack"),
973 }
974 }
975}
976
977#[derive(Debug, Clone, Serialize, Deserialize, Default)]
990#[serde(deny_unknown_fields)]
991#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
992pub struct RandomPanicMiddleware {
993 #[serde(default)]
995 pub mode: FaultMode,
996 #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
998 #[serde(default)]
999 pub trigger_on_message: Option<usize>,
1000 #[serde(default = "default_true")]
1002 pub enabled: bool,
1003 #[serde(skip, default = "default_atomic_usize_arc")]
1004 #[cfg_attr(feature = "schema", schemars(skip))]
1005 pub message_count: Arc<AtomicUsize>,
1006}
1007
1008fn default_true() -> bool {
1009 true
1010}
1011
1012fn default_atomic_usize_arc() -> Arc<AtomicUsize> {
1013 Arc::new(AtomicUsize::new(0))
1014}
1015
1016fn deserialize_null_as_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
1017where
1018 D: Deserializer<'de>,
1019{
1020 let opt = Option::<bool>::deserialize(deserializer)?;
1021 Ok(opt.unwrap_or(false))
1022}
1023
1024#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1026#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1027#[serde(deny_unknown_fields)]
1028pub struct AwsConfig {
1029 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1031 pub queue_url: Option<String>,
1032 pub topic_arn: Option<String>,
1034 pub region: Option<String>,
1036 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1038 pub endpoint_url: Option<String>,
1039 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1041 pub access_key: Option<String>,
1042 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1044 pub secret_key: Option<String>,
1045 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1047 pub session_token: Option<String>,
1048 #[cfg_attr(feature = "schema", schemars(range(min = 1, max = 10)))]
1050 pub max_messages: Option<i32>,
1051 #[cfg_attr(feature = "schema", schemars(range(min = 0, max = 20)))]
1053 pub wait_time_seconds: Option<i32>,
1054 #[serde(default)]
1056 pub binary_payload_mode: bool,
1057}
1058
1059impl AwsConfig {
1060 pub fn new() -> Self {
1062 Self::default()
1063 }
1064
1065 pub fn with_queue_url(mut self, queue_url: impl Into<String>) -> Self {
1066 self.queue_url = Some(queue_url.into());
1067 self
1068 }
1069
1070 pub fn with_topic_arn(mut self, topic_arn: impl Into<String>) -> Self {
1071 self.topic_arn = Some(topic_arn.into());
1072 self
1073 }
1074
1075 pub fn with_region(mut self, region: impl Into<String>) -> Self {
1076 self.region = Some(region.into());
1077 self
1078 }
1079
1080 pub fn with_endpoint_url(mut self, endpoint_url: impl Into<String>) -> Self {
1081 self.endpoint_url = Some(endpoint_url.into());
1082 self
1083 }
1084
1085 pub fn with_credentials(
1086 mut self,
1087 access_key: impl Into<String>,
1088 secret_key: impl Into<String>,
1089 ) -> Self {
1090 self.access_key = Some(access_key.into());
1091 self.secret_key = Some(secret_key.into());
1092 self
1093 }
1094}
1095
1096#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1100#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1101#[serde(deny_unknown_fields)]
1102pub struct KafkaConfig {
1103 #[serde(alias = "brokers")]
1105 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1106 pub url: String,
1107 pub topic: Option<String>,
1109 pub username: Option<String>,
1111 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1113 pub password: Option<String>,
1114 #[serde(default)]
1116 pub tls: TlsConfig,
1117 pub group_id: Option<String>,
1120 #[serde(default)]
1122 pub delayed_ack: bool,
1123 #[serde(default)]
1125 pub producer_options: Option<Vec<(String, String)>>,
1126 #[serde(default)]
1128 pub consumer_options: Option<Vec<(String, String)>>,
1129}
1130
1131impl KafkaConfig {
1132 pub fn new(url: impl Into<String>) -> Self {
1134 Self {
1135 url: url.into(),
1136 ..Default::default()
1137 }
1138 }
1139
1140 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
1141 self.topic = Some(topic.into());
1142 self
1143 }
1144
1145 pub fn with_group_id(mut self, group_id: impl Into<String>) -> Self {
1146 self.group_id = Some(group_id.into());
1147 self
1148 }
1149
1150 pub fn with_credentials(
1151 mut self,
1152 username: impl Into<String>,
1153 password: impl Into<String>,
1154 ) -> Self {
1155 self.username = Some(username.into());
1156 self.password = Some(password.into());
1157 self
1158 }
1159
1160 pub fn with_producer_option(
1161 mut self,
1162 key: impl Into<String>,
1163 value: impl Into<String>,
1164 ) -> Self {
1165 let options = self.producer_options.get_or_insert_with(Vec::new);
1166 options.push((key.into(), value.into()));
1167 self
1168 }
1169
1170 pub fn with_consumer_option(
1171 mut self,
1172 key: impl Into<String>,
1173 value: impl Into<String>,
1174 ) -> Self {
1175 let options = self.consumer_options.get_or_insert_with(Vec::new);
1176 options.push((key.into(), value.into()));
1177 self
1178 }
1179}
1180
1181#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1185#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1186#[serde(deny_unknown_fields)]
1187pub struct SledConfig {
1188 pub path: String,
1190 pub tree: Option<String>,
1192 #[serde(default)]
1194 pub read_from_start: bool,
1195 #[serde(default)]
1197 pub delete_after_read: bool,
1198}
1199
1200impl SledConfig {
1201 pub fn new(path: impl Into<String>) -> Self {
1203 Self {
1204 path: path.into(),
1205 ..Default::default()
1206 }
1207 }
1208
1209 pub fn with_tree(mut self, tree: impl Into<String>) -> Self {
1210 self.tree = Some(tree.into());
1211 self
1212 }
1213
1214 pub fn with_read_from_start(mut self, read_from_start: bool) -> Self {
1215 self.read_from_start = read_from_start;
1216 self
1217 }
1218}
1219
1220#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
1222#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1223#[serde(rename_all = "snake_case")]
1224pub enum FileFormat {
1225 #[default]
1227 Normal,
1228 Json,
1230 Text,
1232 Raw,
1234}
1235
1236#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1240pub struct FileConfig {
1241 pub path: String,
1243 pub delimiter: Option<String>,
1247 #[serde(flatten, default)]
1250 pub mode: Option<FileConsumerMode>,
1251 #[serde(default)]
1253 pub format: FileFormat,
1254}
1255
1256#[derive(Debug, Clone, Deserialize, Serialize)]
1257#[serde(tag = "mode", rename_all = "snake_case")]
1258#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1259pub enum FileConsumerMode {
1260 Consume {
1264 #[serde(default)]
1265 delete: bool,
1266 },
1267 Subscribe {
1271 #[serde(default)]
1272 delete: bool,
1273 },
1274 GroupSubscribe {
1279 group_id: String,
1281 #[serde(default)]
1284 read_from_tail: bool,
1285 },
1286}
1287
1288impl Default for FileConsumerMode {
1289 fn default() -> Self {
1290 Self::Consume { delete: false }
1291 }
1292}
1293
1294impl FileConfig {
1295 pub fn new(path: impl Into<String>) -> Self {
1297 Self {
1298 path: path.into(),
1299 mode: Some(FileConsumerMode::default()),
1300 delimiter: None,
1301 format: FileFormat::default(),
1302 }
1303 }
1304
1305 pub fn with_mode(mut self, mode: FileConsumerMode) -> Self {
1306 self.mode = Some(mode);
1307 self
1308 }
1309
1310 pub fn effective_mode(&self) -> FileConsumerMode {
1312 self.mode.clone().unwrap_or_default()
1313 }
1314}
1315
1316#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1320#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1321#[serde(deny_unknown_fields)]
1322pub struct NatsConfig {
1323 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1325 pub url: String,
1326 pub subject: Option<String>,
1328 pub stream: Option<String>,
1330 pub username: Option<String>,
1332 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1334 pub password: Option<String>,
1335 #[serde(default)]
1337 pub tls: TlsConfig,
1338 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1340 pub token: Option<String>,
1341 #[serde(default)]
1345 pub request_reply: bool,
1346 pub request_timeout_ms: Option<u64>,
1348 #[serde(default)]
1350 pub delayed_ack: bool,
1351 #[serde(default)]
1353 pub no_jetstream: bool,
1354 #[serde(default)]
1356 pub subscriber_mode: bool,
1357 pub stream_max_messages: Option<i64>,
1359 pub deliver_policy: Option<NatsDeliverPolicy>,
1361 pub stream_max_bytes: Option<i64>,
1363 pub prefetch_count: Option<usize>,
1365}
1366
1367#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
1368#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1369#[serde(rename_all = "snake_case")]
1370pub enum NatsDeliverPolicy {
1371 #[default]
1372 All,
1373 Last,
1374 New,
1375 LastPerSubject,
1376}
1377
1378impl NatsConfig {
1379 pub fn new(url: impl Into<String>) -> Self {
1381 Self {
1382 url: url.into(),
1383 ..Default::default()
1384 }
1385 }
1386
1387 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
1388 self.subject = Some(subject.into());
1389 self
1390 }
1391
1392 pub fn with_stream(mut self, stream: impl Into<String>) -> Self {
1393 self.stream = Some(stream.into());
1394 self
1395 }
1396
1397 pub fn with_deliver_policy(mut self, policy: NatsDeliverPolicy) -> Self {
1398 self.deliver_policy = Some(policy);
1399 self
1400 }
1401
1402 pub fn with_credentials(
1403 mut self,
1404 username: impl Into<String>,
1405 password: impl Into<String>,
1406 ) -> Self {
1407 self.username = Some(username.into());
1408 self.password = Some(password.into());
1409 self
1410 }
1411}
1412
1413#[derive(Debug, Serialize, Clone, Default)]
1414#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1415#[cfg_attr(feature = "schema", schemars(transform = memory_config_schema_transform))]
1416#[serde(deny_unknown_fields)]
1417pub struct MemoryConfig {
1418 #[serde(default, skip_serializing_if = "String::is_empty", alias = "url")]
1427 pub topic: String,
1428 #[serde(skip)]
1430 pub url: Option<String>,
1431 pub capacity: Option<usize>,
1433 #[serde(default)]
1435 pub request_reply: bool,
1436 pub request_timeout_ms: Option<u64>,
1438 #[serde(default)]
1440 pub subscribe_mode: bool,
1441 #[serde(default)]
1444 pub enable_nack: bool,
1445 #[serde(skip)]
1446 pub enable_nack_overridden: bool,
1447}
1448
1449impl MemoryConfig {
1450 pub fn new(topic: impl Into<String>, capacity: Option<usize>) -> Self {
1451 Self {
1452 topic: topic.into(),
1453 url: None,
1454 capacity,
1455 ..Default::default()
1456 }
1457 }
1458
1459 pub fn new_with_url(url: impl Into<String>, capacity: Option<usize>) -> Self {
1460 let url = url.into();
1461 Self {
1462 topic: url.clone(),
1463 url: Some(url),
1464 capacity,
1465 ..Default::default()
1466 }
1467 }
1468
1469 pub fn with_subscribe(self, subscribe_mode: bool) -> Self {
1470 Self {
1471 subscribe_mode,
1472 ..self
1473 }
1474 }
1475
1476 pub fn with_request_reply(mut self, request_reply: bool) -> Self {
1477 self.request_reply = request_reply;
1478 self
1479 }
1480
1481 pub fn get_transport_identifier(&self) -> anyhow::Result<String> {
1484 let identifier = if !self.topic.is_empty() {
1485 &self.topic
1486 } else if let Some(url) = self.url.as_ref().filter(|url| !url.is_empty()) {
1487 url
1488 } else {
1489 return Err(anyhow::anyhow!(
1490 "MemoryConfig: 'topic' (or 'url' alias) is required."
1491 ));
1492 };
1493
1494 if identifier.contains("://") {
1496 Ok(identifier.clone())
1497 } else {
1498 Ok(format!("memory://{}", identifier))
1499 }
1500 }
1501
1502 pub fn is_ipc_transport(&self) -> bool {
1505 if let Ok(identifier) = self.get_transport_identifier() {
1506 identifier.starts_with("ipc://")
1507 || identifier.starts_with("unix://")
1508 || identifier.starts_with("pipe://")
1509 } else {
1510 false
1511 }
1512 }
1513
1514 pub fn with_smart_defaults(mut self) -> Self {
1517 if !self.enable_nack_overridden && self.is_ipc_transport() {
1518 self.enable_nack = true;
1519 }
1520 self
1521 }
1522}
1523
1524impl<'de> Deserialize<'de> for MemoryConfig {
1525 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1526 where
1527 D: Deserializer<'de>,
1528 {
1529 #[derive(Deserialize, Default)]
1530 #[serde(deny_unknown_fields)]
1531 struct MemoryConfigSerde {
1532 #[serde(default)]
1533 topic: String,
1534 #[serde(default)]
1535 url: Option<String>,
1536 capacity: Option<usize>,
1537 #[serde(default)]
1538 request_reply: bool,
1539 request_timeout_ms: Option<u64>,
1540 #[serde(default)]
1541 subscribe_mode: bool,
1542 #[serde(default)]
1543 enable_nack: Option<bool>,
1544 }
1545
1546 let raw = MemoryConfigSerde::deserialize(deserializer)?;
1547 if raw.topic.is_empty() && raw.url.as_deref().map_or(true, str::is_empty) {
1548 return Err(serde::de::Error::custom(
1549 "MemoryConfig: 'topic' (or 'url' alias) is required.",
1550 ));
1551 }
1552 let topic = if raw.topic.is_empty() {
1553 raw.url.clone().unwrap_or_default()
1554 } else {
1555 raw.topic
1556 };
1557 Ok(Self {
1558 topic,
1559 url: raw.url,
1560 capacity: raw.capacity,
1561 request_reply: raw.request_reply,
1562 request_timeout_ms: raw.request_timeout_ms,
1563 subscribe_mode: raw.subscribe_mode,
1564 enable_nack: raw.enable_nack.unwrap_or(false),
1565 enable_nack_overridden: raw.enable_nack.is_some(),
1566 })
1567 }
1568}
1569
1570#[cfg(feature = "schema")]
1571fn memory_config_schema_transform(schema: &mut schemars::Schema) {
1572 let Some(schema_obj) = schema.as_object_mut() else {
1573 return;
1574 };
1575
1576 let Some(properties) = schema_obj
1577 .get_mut("properties")
1578 .and_then(serde_json::Value::as_object_mut)
1579 else {
1580 return;
1581 };
1582
1583 properties.insert(
1584 "url".to_string(),
1585 serde_json::json!({
1586 "description": "Alias for `topic`. Use either `topic` or `url`.",
1587 "type": "string",
1588 "minLength": 1
1589 }),
1590 );
1591
1592 if let Some(topic) = properties
1595 .get_mut("topic")
1596 .and_then(serde_json::Value::as_object_mut)
1597 {
1598 topic.insert("minLength".to_string(), serde_json::json!(1));
1599 }
1600
1601 schema_obj.insert(
1602 "anyOf".to_string(),
1603 serde_json::json!([
1604 { "required": ["topic"] },
1605 { "required": ["url"] }
1606 ]),
1607 );
1608}
1609
1610#[cfg(feature = "schema")]
1611fn route_schema_transform(schema: &mut schemars::Schema) {
1612 let Some(properties) = schema
1613 .as_object_mut()
1614 .and_then(|schema_obj| schema_obj.get_mut("properties"))
1615 .and_then(serde_json::Value::as_object_mut)
1616 else {
1617 return;
1618 };
1619
1620 let Some(allow_fault_injection) = properties
1621 .get_mut("allow_fault_injection")
1622 .and_then(serde_json::Value::as_object_mut)
1623 else {
1624 return;
1625 };
1626
1627 allow_fault_injection.insert("default".to_string(), serde_json::Value::Bool(false));
1628}
1629
1630#[derive(Debug, Serialize, Deserialize, Clone, Default)]
1632#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1633#[serde(deny_unknown_fields)]
1634pub struct StreamBufferConfig {
1635 pub topic: String,
1637 #[serde(default, skip_serializing_if = "Option::is_none")]
1643 pub correlation_id: Option<String>,
1644 #[serde(default, skip_serializing_if = "Option::is_none")]
1646 pub capacity: Option<usize>,
1647}
1648
1649impl StreamBufferConfig {
1650 pub fn new(topic: impl Into<String>) -> Self {
1656 Self {
1657 topic: topic.into(),
1658 ..Default::default()
1659 }
1660 }
1661
1662 pub fn with_correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
1664 self.correlation_id = Some(correlation_id.into());
1665 self
1666 }
1667
1668 pub fn with_capacity(mut self, capacity: usize) -> Self {
1670 self.capacity = Some(capacity);
1671 self
1672 }
1673}
1674
1675#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1679#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1680#[serde(deny_unknown_fields)]
1681pub struct AmqpConfig {
1682 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1686 pub url: String,
1687 pub queue: Option<String>,
1689 #[serde(default)]
1691 pub subscribe_mode: bool,
1692 pub username: Option<String>,
1694 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1696 pub password: Option<String>,
1697 #[serde(default)]
1699 pub tls: TlsConfig,
1700 pub exchange: Option<String>,
1702 pub prefetch_count: Option<u16>,
1704 #[serde(default)]
1706 pub no_persistence: bool,
1707 #[serde(default)]
1709 pub no_declare_queue: bool,
1710 #[serde(default)]
1712 pub delayed_ack: bool,
1713}
1714
1715impl AmqpConfig {
1716 pub fn new(url: impl Into<String>) -> Self {
1718 Self {
1719 url: url.into(),
1720 ..Default::default()
1721 }
1722 }
1723
1724 pub fn with_queue(mut self, queue: impl Into<String>) -> Self {
1725 self.queue = Some(queue.into());
1726 self
1727 }
1728
1729 pub fn with_exchange(mut self, exchange: impl Into<String>) -> Self {
1730 self.exchange = Some(exchange.into());
1731 self
1732 }
1733
1734 pub fn with_credentials(
1735 mut self,
1736 username: impl Into<String>,
1737 password: impl Into<String>,
1738 ) -> Self {
1739 self.username = Some(username.into());
1740 self.password = Some(password.into());
1741 self
1742 }
1743}
1744
1745#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
1749#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1750#[serde(rename_all = "lowercase")]
1751pub enum MongoDbFormat {
1752 #[default]
1753 Normal,
1754 Json,
1755 Text,
1756 Raw,
1757}
1758
1759#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1763#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1764#[serde(deny_unknown_fields)]
1765pub struct MongoDbConfig {
1766 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1769 pub url: String,
1770 pub collection: Option<String>,
1772 pub username: Option<String>,
1775 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1777 pub password: Option<String>,
1779 #[serde(default)]
1781 pub tls: TlsConfig,
1782 pub database: String,
1784 pub polling_interval_ms: Option<u64>,
1786 pub reply_polling_ms: Option<u64>,
1788 #[serde(default)]
1790 pub request_reply: bool,
1791 #[serde(default)]
1793 pub change_stream: bool,
1794 pub request_timeout_ms: Option<u64>,
1796 pub ttl_seconds: Option<u64>,
1798 pub capped_size_bytes: Option<i64>,
1800 #[serde(default)]
1802 pub format: MongoDbFormat,
1803 pub cursor_id: Option<String>,
1805 pub receive_query: Option<String>,
1807 pub meta_collection: Option<String>,
1809}
1810
1811impl MongoDbConfig {
1812 pub fn new(url: impl Into<String>, database: impl Into<String>) -> Self {
1814 Self {
1815 url: url.into(),
1816 database: database.into(),
1817 ..Default::default()
1818 }
1819 }
1820
1821 pub fn with_collection(mut self, collection: impl Into<String>) -> Self {
1822 self.collection = Some(collection.into());
1823 self
1824 }
1825
1826 pub fn with_credentials(
1827 mut self,
1828 username: impl Into<String>,
1829 password: impl Into<String>,
1830 ) -> Self {
1831 self.username = Some(username.into());
1832 self.password = Some(password.into());
1833 self
1834 }
1835
1836 pub fn with_change_stream(mut self, change_stream: bool) -> Self {
1837 self.change_stream = change_stream;
1838 self
1839 }
1840}
1841
1842#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1846#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1847#[serde(deny_unknown_fields)]
1848pub struct MqttConfig {
1849 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1851 pub url: String,
1852 pub topic: Option<String>,
1854 pub username: Option<String>,
1856 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1858 pub password: Option<String>,
1859 #[serde(default)]
1861 pub tls: TlsConfig,
1862 pub client_id: Option<String>,
1864 pub queue_capacity: Option<usize>,
1866 pub max_inflight: Option<u16>,
1868 pub qos: Option<u8>,
1870 #[serde(default = "default_clean_session")]
1872 pub clean_session: bool,
1873 pub keep_alive_seconds: Option<u64>,
1875 #[serde(default)]
1877 pub protocol: MqttProtocol,
1878 pub session_expiry_interval: Option<u32>,
1880 #[serde(default)]
1886 pub delayed_ack: bool,
1887}
1888
1889impl MqttConfig {
1890 pub fn new(url: impl Into<String>) -> Self {
1892 Self {
1893 url: url.into(),
1894 ..Default::default()
1895 }
1896 }
1897
1898 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
1899 self.topic = Some(topic.into());
1900 self
1901 }
1902
1903 pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
1904 self.client_id = Some(client_id.into());
1905 self
1906 }
1907
1908 pub fn with_credentials(
1909 mut self,
1910 username: impl Into<String>,
1911 password: impl Into<String>,
1912 ) -> Self {
1913 self.username = Some(username.into());
1914 self.password = Some(password.into());
1915 self
1916 }
1917}
1918
1919#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
1923#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1924#[serde(rename_all = "lowercase")]
1925pub enum MqttProtocol {
1926 #[default]
1927 V5,
1928 V3,
1929}
1930
1931#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1934#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1935#[serde(deny_unknown_fields)]
1936pub struct ZeroMqConfig {
1937 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1938 pub url: String,
1940 #[serde(default)]
1942 pub socket_type: Option<ZeroMqSocketType>,
1943 pub topic: Option<String>,
1945 #[serde(default)]
1947 pub bind: bool,
1948 #[serde(default)]
1950 pub internal_buffer_size: Option<usize>,
1951}
1952
1953impl ZeroMqConfig {
1954 pub fn new(url: impl Into<String>) -> Self {
1956 Self {
1957 url: url.into(),
1958 ..Default::default()
1959 }
1960 }
1961
1962 pub fn with_socket_type(mut self, socket_type: ZeroMqSocketType) -> Self {
1963 self.socket_type = Some(socket_type);
1964 self
1965 }
1966
1967 pub fn with_bind(mut self, bind: bool) -> Self {
1968 self.bind = bind;
1969 self
1970 }
1971}
1972
1973#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
1978#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1979#[serde(rename_all = "lowercase")]
1980pub enum ZeroMqSocketType {
1981 Push,
1982 Pull,
1983 Pub,
1984 Sub,
1985 Req,
1986 Rep,
1987}
1988
1989#[derive(Debug, Deserialize, Serialize, Clone, Default)]
1992#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1993#[serde(deny_unknown_fields)]
1994pub struct GrpcConfig {
1995 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
1996 pub url: String,
1998 pub topic: Option<String>,
2000 pub timeout_ms: Option<u64>,
2004 #[serde(default)]
2006 pub tls: TlsConfig,
2007 #[serde(default)]
2010 pub server_mode: bool,
2011 #[serde(default)]
2013 pub initial_stream_window_size: Option<u32>,
2014 #[serde(default)]
2016 pub initial_connection_window_size: Option<u32>,
2017 #[serde(default)]
2019 pub concurrency_limit_per_connection: Option<usize>,
2020 #[serde(default)]
2022 pub http2_keepalive_interval_ms: Option<u64>,
2023 #[serde(default)]
2025 pub http2_keepalive_timeout_ms: Option<u64>,
2026 #[serde(default)]
2028 pub max_decoding_message_size: Option<usize>,
2029}
2030
2031impl GrpcConfig {
2032 pub fn new(url: impl Into<String>) -> Self {
2034 Self {
2035 url: url.into(),
2036 ..Default::default()
2037 }
2038 }
2039
2040 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
2041 self.topic = Some(topic.into());
2042 self
2043 }
2044
2045 pub fn with_server_mode(mut self, server_mode: bool) -> Self {
2047 self.server_mode = server_mode;
2048 self
2049 }
2050}
2051
2052#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, Default)]
2056#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2057#[serde(rename_all = "snake_case")]
2058pub enum HttpServerProtocol {
2059 #[default]
2061 Auto,
2062 Http1Only,
2064 Http2Only,
2066}
2067
2068#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)]
2070#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2071#[serde(rename_all = "snake_case")]
2072pub enum WebSocketExecutionMode {
2073 #[default]
2076 Auto,
2077 DirectOnly,
2079 Routed,
2081}
2082
2083#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2085#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2086#[serde(deny_unknown_fields)]
2087pub struct HttpConfig {
2088 pub url: String,
2090 pub path: Option<String>,
2092 pub method: Option<String>,
2094 #[serde(default)]
2096 pub tls: TlsConfig,
2097 pub workers: Option<usize>,
2099 pub message_id_header: Option<String>,
2101 pub request_timeout_ms: Option<u64>,
2103 pub internal_buffer_size: Option<usize>,
2105 #[serde(default)]
2107 pub fire_and_forget: bool,
2108 #[serde(default)]
2110 pub receive_streamable: bool,
2111 #[serde(default, skip_serializing_if = "Option::is_none")]
2114 #[cfg_attr(
2115 feature = "schema",
2116 schemars(default = "default_inline_response_fast_path_schema")
2117 )]
2118 pub inline_response_fast_path: Option<bool>,
2119 #[serde(default)]
2123 pub server_protocol: HttpServerProtocol,
2124 #[serde(default, skip_serializing_if = "Option::is_none")]
2134 pub stream_response_to: Option<Box<Endpoint>>,
2135 #[serde(default, skip_serializing_if = "Option::is_none")]
2137 pub batch_concurrency: Option<usize>,
2138 #[serde(default, skip_serializing_if = "Option::is_none")]
2140 pub tcp_keepalive_ms: Option<u64>,
2141 #[serde(default, skip_serializing_if = "Option::is_none")]
2143 pub pool_idle_timeout_ms: Option<u64>,
2144 #[serde(default)]
2146 pub compression_enabled: bool,
2147 #[serde(default)]
2149 pub compression_threshold_bytes: Option<usize>,
2150 pub concurrency_limit: Option<usize>,
2153 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2154 #[serde(
2155 default,
2156 skip_serializing_if = "Option::is_none",
2157 deserialize_with = "deserialize_basic_auth"
2158 )]
2159 pub basic_auth: Option<(String, String)>,
2160 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2162 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
2163 pub custom_headers: HashMap<String, String>,
2164}
2165
2166#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2168#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2169#[serde(deny_unknown_fields)]
2170pub struct WebSocketConfig {
2171 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2173 pub url: String,
2174 pub path: Option<String>,
2176 pub message_id_header: Option<String>,
2178 pub routed_queue_capacity: Option<usize>,
2180 pub backlog: Option<u32>,
2184 #[serde(default)]
2186 pub execution_mode: WebSocketExecutionMode,
2187}
2188
2189fn deserialize_basic_auth<'de, D>(deserializer: D) -> Result<Option<(String, String)>, D::Error>
2190where
2191 D: Deserializer<'de>,
2192{
2193 let val = serde_json::Value::deserialize(deserializer)?;
2194 match val {
2195 serde_json::Value::Null => Ok(None),
2196 serde_json::Value::Array(arr) => {
2197 if arr.len() != 2 {
2198 return Err(serde::de::Error::custom("basic_auth must have 2 elements"));
2199 }
2200 let u = arr[0]
2201 .as_str()
2202 .ok_or_else(|| serde::de::Error::custom("basic_auth[0] must be string"))?
2203 .to_string();
2204 let p = arr[1]
2205 .as_str()
2206 .ok_or_else(|| serde::de::Error::custom("basic_auth[1] must be string"))?
2207 .to_string();
2208 Ok(Some((u, p)))
2209 }
2210 serde_json::Value::Object(map) => {
2211 let u = map
2212 .get("0")
2213 .and_then(|v| v.as_str())
2214 .ok_or_else(|| serde::de::Error::custom("basic_auth map missing '0'"))?
2215 .to_string();
2216 let p = map
2217 .get("1")
2218 .and_then(|v| v.as_str())
2219 .ok_or_else(|| serde::de::Error::custom("basic_auth map missing '1'"))?
2220 .to_string();
2221 Ok(Some((u, p)))
2222 }
2223 _ => Err(serde::de::Error::custom("invalid type for basic_auth")),
2224 }
2225}
2226
2227impl HttpConfig {
2228 pub fn new(url: impl Into<String>) -> Self {
2230 Self {
2231 url: url.into(),
2232 ..Default::default()
2233 }
2234 }
2235
2236 pub fn with_workers(mut self, workers: usize) -> Self {
2237 self.workers = Some(workers);
2238 self
2239 }
2240
2241 pub fn with_method(mut self, method: impl Into<String>) -> Self {
2242 self.method = Some(method.into());
2243 self
2244 }
2245
2246 pub fn with_path(mut self, path: impl Into<String>) -> Self {
2247 self.path = Some(path.into());
2248 self
2249 }
2250
2251 pub fn with_receive_streamable(mut self, receive_streamable: bool) -> Self {
2252 self.receive_streamable = receive_streamable;
2253 self
2254 }
2255
2256 pub fn with_inline_response_fast_path(mut self, inline_response_fast_path: bool) -> Self {
2257 self.inline_response_fast_path = Some(inline_response_fast_path);
2258 self
2259 }
2260
2261 pub fn with_server_protocol(mut self, server_protocol: HttpServerProtocol) -> Self {
2262 self.server_protocol = server_protocol;
2263 self
2264 }
2265
2266 pub fn inline_response_fast_path_enabled(&self) -> bool {
2267 self.inline_response_fast_path.unwrap_or(true)
2268 }
2269
2270 pub fn with_stream_response_to(mut self, endpoint: Endpoint) -> Self {
2271 self.stream_response_to = Some(Box::new(endpoint));
2272 self
2273 }
2274}
2275
2276impl WebSocketConfig {
2277 pub fn new(url: impl Into<String>) -> Self {
2279 Self {
2280 url: url.into(),
2281 ..Default::default()
2282 }
2283 }
2284
2285 pub fn with_path(mut self, path: impl Into<String>) -> Self {
2286 self.path = Some(path.into());
2287 self
2288 }
2289
2290 pub fn with_backlog(mut self, backlog: u32) -> Self {
2291 self.backlog = Some(backlog);
2292 self
2293 }
2294
2295 pub fn with_execution_mode(mut self, execution_mode: WebSocketExecutionMode) -> Self {
2296 self.execution_mode = execution_mode;
2297 self
2298 }
2299}
2300
2301#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2305#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2306#[serde(deny_unknown_fields)]
2307pub struct IbmMqConfig {
2308 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2310 pub url: String,
2311 pub queue: Option<String>,
2313 pub topic: Option<String>,
2315 pub queue_manager: String,
2317 pub channel: String,
2319 pub username: Option<String>,
2321 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2323 pub password: Option<String>,
2324 pub cipher_spec: Option<String>,
2326 #[serde(default)]
2328 pub tls: TlsConfig,
2329 #[serde(default = "default_max_message_size")]
2331 pub max_message_size: usize,
2332 #[serde(default = "default_wait_timeout_ms")]
2334 pub wait_timeout_ms: i32,
2335 #[serde(default)]
2337 pub internal_buffer_size: Option<usize>,
2338 #[serde(default)]
2340 pub disable_status_inq: bool,
2341}
2342
2343impl IbmMqConfig {
2344 pub fn new(
2346 url: impl Into<String>,
2347 queue_manager: impl Into<String>,
2348 channel: impl Into<String>,
2349 ) -> Self {
2350 Self {
2351 url: url.into(),
2352 queue_manager: queue_manager.into(),
2353 channel: channel.into(),
2354 disable_status_inq: false,
2355 ..Default::default()
2356 }
2357 }
2358
2359 pub fn with_queue(mut self, queue: impl Into<String>) -> Self {
2360 self.queue = Some(queue.into());
2361 self
2362 }
2363
2364 pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
2365 self.topic = Some(topic.into());
2366 self
2367 }
2368
2369 pub fn with_credentials(
2370 mut self,
2371 username: impl Into<String>,
2372 password: impl Into<String>,
2373 ) -> Self {
2374 self.username = Some(username.into());
2375 self.password = Some(password.into());
2376 self
2377 }
2378}
2379
2380fn default_max_message_size() -> usize {
2381 4 * 1024 * 1024 }
2383
2384fn default_wait_timeout_ms() -> i32 {
2385 1000 }
2387
2388#[derive(Debug, Deserialize, Serialize, Clone)]
2391#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2392#[serde(deny_unknown_fields)]
2393pub struct SwitchConfig {
2394 pub metadata_key: String,
2396 pub cases: HashMap<String, Endpoint>,
2398 pub default: Option<Box<Endpoint>>,
2400}
2401
2402#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2404#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2405#[serde(deny_unknown_fields)]
2406pub struct ResponseConfig {
2407 }
2409
2410#[derive(Debug, Deserialize, Serialize, Clone, Default)]
2414#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2415#[serde(deny_unknown_fields)]
2416pub struct SqlxConfig {
2417 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2419 pub url: String,
2420 #[serde(default)]
2422 pub username: Option<String>,
2423 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2425 #[serde(default)]
2426 pub password: Option<String>,
2427 pub table: String,
2429 pub insert_query: Option<String>,
2432 pub select_query: Option<String>,
2436 #[serde(default)]
2438 pub delete_after_read: bool,
2439 #[serde(default)]
2441 pub auto_create_table: bool,
2442 pub polling_interval_ms: Option<u64>,
2444 #[serde(default)]
2446 pub tls: TlsConfig,
2447 pub max_connections: Option<u32>,
2449 pub min_connections: Option<u32>,
2451 pub acquire_timeout_ms: Option<u64>,
2453 pub idle_timeout_ms: Option<u64>,
2455 pub max_lifetime_ms: Option<u64>,
2457}
2458
2459#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
2480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2481#[serde(deny_unknown_fields)]
2482pub struct TlsConfig {
2483 #[serde(default, deserialize_with = "deserialize_null_as_false")]
2485 pub required: bool,
2486 pub ca_file: Option<String>,
2488 pub cert_file: Option<String>,
2490 pub key_file: Option<String>,
2492 #[cfg_attr(feature = "schema", schemars(extend("format"="password")))]
2494 pub cert_password: Option<String>,
2495 #[serde(default)]
2497 pub accept_invalid_certs: bool,
2498}
2499
2500impl TlsConfig {
2501 pub fn new() -> Self {
2503 Self::default()
2504 }
2505
2506 pub fn with_ca_file(mut self, ca_file: impl Into<String>) -> Self {
2507 self.ca_file = Some(ca_file.into());
2508 self.required = true;
2509 self
2510 }
2511
2512 pub fn with_client_cert(
2513 mut self,
2514 cert_file: impl Into<String>,
2515 key_file: impl Into<String>,
2516 ) -> Self {
2517 self.cert_file = Some(cert_file.into());
2518 self.key_file = Some(key_file.into());
2519 self.required = true;
2520 self
2521 }
2522
2523 pub fn with_insecure(mut self, accept_invalid_certs: bool) -> Self {
2524 self.accept_invalid_certs = accept_invalid_certs;
2525 self
2526 }
2527
2528 pub fn is_mtls_client_configured(&self) -> bool {
2530 self.required && self.cert_file.is_some() && self.key_file.is_some()
2531 }
2532
2533 pub fn is_tls_server_configured(&self) -> bool {
2535 self.required && self.cert_file.is_some() && self.key_file.is_some()
2536 }
2537
2538 pub fn is_tls_client_configured(&self) -> bool {
2540 self.required
2541 || self.ca_file.is_some()
2542 || (self.cert_file.is_some() && self.key_file.is_some())
2543 }
2544
2545 pub fn normalize_url(&self, url: &str) -> String {
2547 if url
2548 .get(..7)
2549 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("http://"))
2550 || url
2551 .get(..8)
2552 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("https://"))
2553 {
2554 url.to_string()
2555 } else {
2556 let is_tls = self.required;
2557 let scheme = if is_tls { "https" } else { "http" };
2558 format!("{}://{}", scheme, url)
2559 }
2560 }
2561}
2562
2563pub trait SecretExtractor {
2565 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>);
2567}
2568
2569fn extract_sensitive_string_map_entries(
2570 values: &mut HashMap<String, String>,
2571 prefix: &str,
2572 field_name: &str,
2573 secrets: &mut HashMap<String, String>,
2574) {
2575 let secret_keys = values
2576 .keys()
2577 .filter(|key| {
2578 let key = key.to_ascii_lowercase();
2579 key.contains("key") || key.contains("token") || key.contains("auth")
2580 })
2581 .cloned()
2582 .collect::<Vec<_>>();
2583
2584 for key in secret_keys {
2585 if let Some(value) = values.remove(&key) {
2586 secrets.insert(
2587 sanitize_secret_key(&format!("{}__{}__{}", prefix, field_name, key)),
2588 value,
2589 );
2590 }
2591 }
2592}
2593
2594fn url_has_userinfo(url: &str) -> bool {
2595 let Some(authority_start) = url.find("://").map(|idx| idx + 3) else {
2596 return false;
2597 };
2598 let authority_end = url[authority_start..]
2599 .find(['/', '?', '#'])
2600 .map(|idx| authority_start + idx)
2601 .unwrap_or(url.len());
2602 url[authority_start..authority_end].contains('@')
2603}
2604
2605fn sanitize_secret_key(key: &str) -> String {
2606 key.chars()
2607 .map(|ch| {
2608 let ch = ch.to_ascii_uppercase();
2609 if ch.is_ascii_alphanumeric() || ch == '_' {
2610 ch
2611 } else {
2612 '_'
2613 }
2614 })
2615 .collect()
2616}
2617
2618fn extract_sensitive_url(
2619 url: &mut String,
2620 prefix: &str,
2621 field_name: &str,
2622 secrets: &mut HashMap<String, String>,
2623) {
2624 if !url.is_empty() && url_has_userinfo(url) {
2625 secrets.insert(
2626 sanitize_secret_key(&format!("{}__{}", prefix, field_name)),
2627 std::mem::take(url),
2628 );
2629 }
2630}
2631
2632fn extract_sensitive_optional_url(
2633 url: &mut Option<String>,
2634 prefix: &str,
2635 field_name: &str,
2636 secrets: &mut HashMap<String, String>,
2637) {
2638 if url.as_ref().is_some_and(|url| url_has_userinfo(url)) {
2639 if let Some(url) = url.take() {
2640 secrets.insert(
2641 sanitize_secret_key(&format!("{}__{}", prefix, field_name)),
2642 url,
2643 );
2644 }
2645 }
2646}
2647
2648impl SecretExtractor for Route {
2649 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2650 self.input
2651 .extract_secrets(&format!("{}__{}", prefix, "INPUT"), secrets);
2652 self.output
2653 .extract_secrets(&format!("{}__{}", prefix, "OUTPUT"), secrets);
2654 }
2655}
2656
2657impl SecretExtractor for Endpoint {
2658 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2659 for (i, middleware) in self.middlewares.iter_mut().enumerate() {
2660 middleware.extract_secrets(&format!("{}__{}__{}", prefix, "MIDDLEWARES", i), secrets);
2661 }
2662 self.endpoint_type.extract_secrets(prefix, secrets);
2663 }
2664}
2665
2666impl SecretExtractor for EndpointType {
2667 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2668 match self {
2669 EndpointType::Aws(cfg) => {
2670 cfg.extract_secrets(&format!("{}__{}", prefix, "AWS"), secrets)
2671 }
2672 EndpointType::Kafka(cfg) => {
2673 cfg.extract_secrets(&format!("{}__{}", prefix, "KAFKA"), secrets)
2674 }
2675 EndpointType::Nats(cfg) => {
2676 cfg.extract_secrets(&format!("{}__{}", prefix, "NATS"), secrets)
2677 }
2678 EndpointType::Amqp(cfg) => {
2679 cfg.extract_secrets(&format!("{}__{}", prefix, "AMQP"), secrets)
2680 }
2681 EndpointType::MongoDb(cfg) => {
2682 cfg.extract_secrets(&format!("{}__{}", prefix, "MONGODB"), secrets)
2683 }
2684 EndpointType::Mqtt(cfg) => {
2685 cfg.extract_secrets(&format!("{}__{}", prefix, "MQTT"), secrets)
2686 }
2687 EndpointType::Http(cfg) => {
2688 cfg.extract_secrets(&format!("{}__{}", prefix, "HTTP"), secrets)
2689 }
2690 EndpointType::WebSocket(cfg) => {
2691 cfg.extract_secrets(&format!("{}__{}", prefix, "WEBSOCKET"), secrets)
2692 }
2693 EndpointType::IbmMq(cfg) => {
2694 cfg.extract_secrets(&format!("{}__{}", prefix, "IBMMQ"), secrets)
2695 }
2696 EndpointType::ZeroMq(cfg) => {
2697 cfg.extract_secrets(&format!("{}__{}", prefix, "ZEROMQ"), secrets)
2698 }
2699 EndpointType::Sqlx(cfg) => {
2700 cfg.extract_secrets(&format!("{}__{}", prefix, "SQLX"), secrets)
2701 }
2702 EndpointType::Grpc(cfg) => {
2703 cfg.extract_secrets(&format!("{}__{}", prefix, "GRPC"), secrets)
2704 }
2705 EndpointType::Fanout(endpoints) => {
2706 for (i, ep) in endpoints.iter_mut().enumerate() {
2707 ep.extract_secrets(&format!("{}__{}__{}", prefix, "FANOUT", i), secrets);
2708 }
2709 }
2710 EndpointType::Switch(cfg) => {
2711 for (key, ep) in cfg.cases.iter_mut() {
2712 ep.extract_secrets(
2713 &format!(
2714 "{}__{}__{}",
2715 prefix,
2716 "SWITCH__CASES",
2717 sanitize_secret_key(key)
2718 ),
2719 secrets,
2720 );
2721 }
2722 if let Some(default) = &mut cfg.default {
2723 default.extract_secrets(&format!("{}__{}", prefix, "SWITCH__DEFAULT"), secrets);
2724 }
2725 }
2726 EndpointType::Reader(ep) => {
2727 ep.extract_secrets(&format!("{}__{}", prefix, "READER"), secrets)
2728 }
2729 _ => {}
2730 }
2731 }
2732}
2733
2734impl SecretExtractor for Middleware {
2735 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2736 if let Middleware::Dlq(cfg) = self {
2737 cfg.endpoint
2738 .extract_secrets(&format!("{}__{}__{}", prefix, "DLQ", "ENDPOINT"), secrets);
2739 }
2740 }
2741}
2742
2743impl SecretExtractor for AwsConfig {
2744 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2745 if let Some(val) = self.access_key.take() {
2746 secrets.insert(format!("{}__{}", prefix, "ACCESS_KEY"), val);
2747 }
2748 if let Some(val) = self.secret_key.take() {
2749 secrets.insert(format!("{}__{}", prefix, "SECRET_KEY"), val);
2750 }
2751 if let Some(val) = self.session_token.take() {
2752 secrets.insert(format!("{}__{}", prefix, "SESSION_TOKEN"), val);
2753 }
2754 extract_sensitive_optional_url(&mut self.queue_url, prefix, "QUEUE_URL", secrets);
2755 extract_sensitive_optional_url(&mut self.endpoint_url, prefix, "ENDPOINT_URL", secrets);
2756 }
2757}
2758
2759impl SecretExtractor for KafkaConfig {
2760 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2761 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2762 if let Some(val) = self.username.take() {
2763 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2764 }
2765 if let Some(val) = self.password.take() {
2766 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2767 }
2768 self.tls
2769 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2770 }
2771}
2772
2773impl SecretExtractor for NatsConfig {
2774 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2775 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2776 if let Some(val) = self.username.take() {
2777 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2778 }
2779 if let Some(val) = self.password.take() {
2780 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2781 }
2782 if let Some(val) = self.token.take() {
2783 secrets.insert(format!("{}__{}", prefix, "TOKEN"), val);
2784 }
2785 self.tls
2786 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2787 }
2788}
2789
2790impl SecretExtractor for AmqpConfig {
2791 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2792 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2793 if let Some(val) = self.username.take() {
2794 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2795 }
2796 if let Some(val) = self.password.take() {
2797 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2798 }
2799 self.tls
2800 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2801 }
2802}
2803
2804impl SecretExtractor for MongoDbConfig {
2805 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2806 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2807 if let Some(val) = self.username.take() {
2808 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2809 }
2810 if let Some(val) = self.password.take() {
2811 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2812 }
2813 self.tls
2814 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2815 }
2816}
2817
2818impl SecretExtractor for MqttConfig {
2819 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2820 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2821 if let Some(val) = self.username.take() {
2822 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2823 }
2824 if let Some(val) = self.password.take() {
2825 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2826 }
2827 self.tls
2828 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2829 }
2830}
2831
2832impl SecretExtractor for HttpConfig {
2833 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2834 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2835 if let Some((u, p)) = self.basic_auth.take() {
2836 secrets.insert(format!("{}__{}__{}", prefix, "BASIC_AUTH", 0), u);
2837 secrets.insert(format!("{}__{}__{}", prefix, "BASIC_AUTH", 1), p);
2838 }
2839 extract_sensitive_string_map_entries(
2840 &mut self.custom_headers,
2841 prefix,
2842 "CUSTOM_HEADERS",
2843 secrets,
2844 );
2845 self.tls
2846 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2847 if let Some(endpoint) = &mut self.stream_response_to {
2848 endpoint.extract_secrets(&format!("{}__{}", prefix, "STREAM_RESPONSE_TO"), secrets);
2849 }
2850 }
2851}
2852
2853impl SecretExtractor for WebSocketConfig {
2854 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2855 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2856 }
2857}
2858
2859impl SecretExtractor for IbmMqConfig {
2860 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2861 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2862 if let Some(val) = self.username.take() {
2863 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2864 }
2865 if let Some(val) = self.password.take() {
2866 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2867 }
2868 self.tls
2869 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2870 }
2871}
2872
2873impl SecretExtractor for ZeroMqConfig {
2874 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2875 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2876 }
2877}
2878
2879impl SecretExtractor for SqlxConfig {
2880 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2881 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2882 if let Some(val) = self.username.take() {
2883 secrets.insert(format!("{}__{}", prefix, "USERNAME"), val);
2884 }
2885 if let Some(val) = self.password.take() {
2886 secrets.insert(format!("{}__{}", prefix, "PASSWORD"), val);
2887 }
2888 self.tls
2889 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2890 }
2891}
2892
2893impl SecretExtractor for GrpcConfig {
2894 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2895 extract_sensitive_url(&mut self.url, prefix, "URL", secrets);
2896 self.tls
2897 .extract_secrets(&format!("{}__{}", prefix, "TLS"), secrets);
2898 }
2899}
2900
2901impl SecretExtractor for TlsConfig {
2902 fn extract_secrets(&mut self, prefix: &str, secrets: &mut HashMap<String, String>) {
2903 if let Some(val) = self.cert_password.take() {
2904 secrets.insert(format!("{}__{}", prefix, "CERT_PASSWORD"), val);
2905 }
2906 }
2907}
2908
2909pub fn extract_config_secrets(config: &mut Config) -> HashMap<String, String> {
2916 let mut secrets = HashMap::new();
2917 for (route_name, route) in config.iter_mut() {
2918 let prefix = sanitize_secret_key(&format!("MQB__{}", route_name));
2919 route.extract_secrets(&prefix, &mut secrets);
2920 }
2921 secrets
2922}
2923
2924#[cfg(test)]
2925mod tests {
2926 use super::*;
2927 use config::{Config as ConfigBuilder, Environment};
2928
2929 const TEST_YAML: &str = r#"
2930kafka_to_nats:
2931 concurrency: 10
2932 input:
2933 middlewares:
2934 - deduplication:
2935 sled_path: "/tmp/mq-bridge/dedup_db"
2936 ttl_seconds: 3600
2937 - metrics: {}
2938 - retry:
2939 max_attempts: 5
2940 initial_interval_ms: 200
2941 - random_panic:
2942 mode: nack
2943 - dlq:
2944 endpoint:
2945 nats:
2946 subject: "dlq-subject"
2947 url: "nats://localhost:4222"
2948 kafka:
2949 topic: "input-topic"
2950 url: "localhost:9092"
2951 group_id: "my-consumer-group"
2952 tls:
2953 required: true
2954 ca_file: "/path_to_ca"
2955 cert_file: "/path_to_cert"
2956 key_file: "/path_to_key"
2957 cert_password: "password"
2958 accept_invalid_certs: true
2959 output:
2960 middlewares:
2961 - metrics: {}
2962 - dlq:
2963 endpoint:
2964 file:
2965 path: "error.out"
2966 nats:
2967 subject: "output-subject"
2968 url: "nats://localhost:4222"
2969"#;
2970
2971 fn assert_config_values(config: &Config) {
2972 assert_eq!(config.len(), 1);
2973 let route = config.get("kafka_to_nats").expect("Route should exist");
2974
2975 assert_eq!(route.options.concurrency, 10);
2976
2977 let input = &route.input;
2979 assert_eq!(input.middlewares.len(), 5);
2980
2981 let mut has_dedup = false;
2982 let mut has_metrics = false;
2983 let mut has_dlq = false;
2984 let mut has_retry = false;
2985 let mut has_random_panic = false;
2986 for middleware in &input.middlewares {
2987 match middleware {
2988 Middleware::Deduplication(dedup) => {
2989 assert_eq!(dedup.sled_path, "/tmp/mq-bridge/dedup_db");
2990 assert_eq!(dedup.ttl_seconds, 3600);
2991 has_dedup = true;
2992 }
2993 Middleware::Metrics(_) => {
2994 has_metrics = true;
2995 }
2996 Middleware::Custom { .. } => {}
2997 Middleware::Dlq(dlq) => {
2998 assert!(dlq.endpoint.middlewares.is_empty());
2999 if let EndpointType::Nats(nats_cfg) = &dlq.endpoint.endpoint_type {
3000 assert_eq!(nats_cfg.subject, Some("dlq-subject".to_string()));
3001 assert_eq!(nats_cfg.url, "nats://localhost:4222");
3002 }
3003 has_dlq = true;
3004 }
3005 Middleware::Retry(retry) => {
3006 assert_eq!(retry.max_attempts, 5);
3007 assert_eq!(retry.initial_interval_ms, 200);
3008 has_retry = true;
3009 }
3010 Middleware::RandomPanic(rp) => {
3011 assert!(rp.mode == FaultMode::Nack);
3012 has_random_panic = true;
3013 }
3014 Middleware::Delay(_) => {}
3015 Middleware::WeakJoin(_) => {}
3016 Middleware::Limiter(_) => {}
3017 Middleware::Buffer(_) => {}
3018 Middleware::CookieJar(_) => {}
3019 }
3020 }
3021
3022 if let EndpointType::Kafka(kafka) = &input.endpoint_type {
3023 assert_eq!(kafka.topic, Some("input-topic".to_string()));
3024 assert_eq!(kafka.url, "localhost:9092");
3025 assert_eq!(kafka.group_id, Some("my-consumer-group".to_string()));
3026 let tls = &kafka.tls;
3027 assert!(tls.required);
3028 assert_eq!(tls.ca_file.as_deref(), Some("/path_to_ca"));
3029 assert!(tls.accept_invalid_certs);
3030 } else {
3031 panic!("Input endpoint should be Kafka");
3032 }
3033 assert!(has_dedup);
3034 assert!(has_metrics);
3035 assert!(has_dlq);
3036 assert!(has_retry);
3037 assert!(has_random_panic);
3038
3039 let output = &route.output;
3041 assert_eq!(output.middlewares.len(), 2);
3042 assert!(matches!(output.middlewares[0], Middleware::Metrics(_)));
3043
3044 if let EndpointType::Nats(nats) = &output.endpoint_type {
3045 assert_eq!(nats.subject, Some("output-subject".to_string()));
3046 assert_eq!(nats.url, "nats://localhost:4222");
3047 } else {
3048 panic!("Output endpoint should be NATS");
3049 }
3050 }
3051
3052 #[test]
3053 fn test_deserialize_from_yaml() {
3054 let result: Result<Config, _> = serde_yaml_ng::from_str(TEST_YAML);
3057 println!("Deserialized from YAML: {:#?}", result);
3058 let config = result.expect("Failed to deserialize TEST_YAML");
3059 assert_config_values(&config);
3060 }
3061
3062 #[test]
3063 fn test_deserialize_from_env() {
3064 unsafe {
3066 std::env::set_var("MQB__KAFKA_TO_NATS__CONCURRENCY", "10");
3067 std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__TOPIC", "input-topic");
3068 std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__URL", "localhost:9092");
3069 std::env::set_var(
3070 "MQB__KAFKA_TO_NATS__INPUT__KAFKA__GROUP_ID",
3071 "my-consumer-group",
3072 );
3073 std::env::set_var("MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__REQUIRED", "true");
3074 std::env::set_var(
3075 "MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__CA_FILE",
3076 "/path_to_ca",
3077 );
3078 std::env::set_var(
3079 "MQB__KAFKA_TO_NATS__INPUT__KAFKA__TLS__ACCEPT_INVALID_CERTS",
3080 "true",
3081 );
3082 std::env::set_var(
3083 "MQB__KAFKA_TO_NATS__OUTPUT__NATS__SUBJECT",
3084 "output-subject",
3085 );
3086 std::env::set_var(
3087 "MQB__KAFKA_TO_NATS__OUTPUT__NATS__URL",
3088 "nats://localhost:4222",
3089 );
3090 std::env::set_var(
3091 "MQB__KAFKA_TO_NATS__INPUT__MIDDLEWARES__0__DLQ__ENDPOINT__NATS__SUBJECT",
3092 "dlq-subject",
3093 );
3094 std::env::set_var(
3095 "MQB__KAFKA_TO_NATS__INPUT__MIDDLEWARES__0__DLQ__ENDPOINT__NATS__URL",
3096 "nats://localhost:4222",
3097 );
3098 }
3099
3100 let builder = ConfigBuilder::builder()
3101 .add_source(
3103 Environment::with_prefix("MQB")
3104 .separator("__")
3105 .try_parsing(true),
3106 );
3107
3108 let config: Config = builder
3109 .build()
3110 .expect("Failed to build config")
3111 .try_deserialize()
3112 .expect("Failed to deserialize config");
3113
3114 assert_eq!(config.get("kafka_to_nats").unwrap().options.concurrency, 10);
3116 if let EndpointType::Kafka(k) = &config.get("kafka_to_nats").unwrap().input.endpoint_type {
3117 assert_eq!(k.topic, Some("input-topic".to_string()));
3118 assert!(k.tls.required);
3119 } else {
3120 panic!("Expected Kafka endpoint");
3121 }
3122
3123 let input = &config.get("kafka_to_nats").unwrap().input;
3124 assert_eq!(input.middlewares.len(), 1);
3125 if let Middleware::Dlq(_) = &input.middlewares[0] {
3126 } else {
3128 panic!("Expected DLQ middleware");
3129 }
3130 }
3131
3132 #[test]
3133 fn test_extract_secrets() {
3134 let mut config = Config::new();
3135 let mut route = Route::default();
3136
3137 let mut kafka_config = KafkaConfig::new("kafka://user:pass@localhost:9092");
3139 kafka_config.username = Some("user".to_string());
3140 kafka_config.password = Some("pass".to_string());
3141 kafka_config.tls.cert_password = Some("certpass".to_string());
3142
3143 route.input = Endpoint {
3144 endpoint_type: EndpointType::Kafka(kafka_config),
3145 middlewares: vec![],
3146 handler: None,
3147 };
3148
3149 let mut http_config = HttpConfig::new("http://httpuser:httppass@localhost");
3151 http_config.basic_auth = Some(("httpuser".to_string(), "httppass".to_string()));
3152 http_config
3153 .custom_headers
3154 .insert("X-API-Key".to_string(), "http-api-key".to_string());
3155 http_config.custom_headers.insert(
3156 "X-Access-Token".to_string(),
3157 "http-access-token".to_string(),
3158 );
3159 http_config.custom_headers.insert(
3160 "X-Authentication".to_string(),
3161 "http-authentication".to_string(),
3162 );
3163 http_config.custom_headers.insert(
3164 "Authorization".to_string(),
3165 "Bearer secret-token".to_string(),
3166 );
3167 http_config
3168 .custom_headers
3169 .insert("X-Trace-Id".to_string(), "trace-value".to_string());
3170
3171 route.output = Endpoint {
3172 endpoint_type: EndpointType::Http(http_config),
3173 middlewares: vec![],
3174 handler: None,
3175 };
3176
3177 config.insert("test_route".to_string(), route);
3178
3179 let secrets = extract_config_secrets(&mut config);
3180
3181 assert_eq!(
3183 secrets
3184 .get("MQB__TEST_ROUTE__INPUT__KAFKA__URL")
3185 .map(|s| s.as_str()),
3186 Some("kafka://user:pass@localhost:9092")
3187 );
3188 assert_eq!(
3189 secrets
3190 .get("MQB__TEST_ROUTE__INPUT__KAFKA__USERNAME")
3191 .map(|s| s.as_str()),
3192 Some("user")
3193 );
3194 assert_eq!(
3195 secrets
3196 .get("MQB__TEST_ROUTE__INPUT__KAFKA__PASSWORD")
3197 .map(|s| s.as_str()),
3198 Some("pass")
3199 );
3200 assert_eq!(
3201 secrets
3202 .get("MQB__TEST_ROUTE__INPUT__KAFKA__TLS__CERT_PASSWORD")
3203 .map(|s| s.as_str()),
3204 Some("certpass")
3205 );
3206 assert_eq!(
3207 secrets
3208 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__URL")
3209 .map(|s| s.as_str()),
3210 Some("http://httpuser:httppass@localhost")
3211 );
3212 assert_eq!(
3213 secrets
3214 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__BASIC_AUTH__0")
3215 .map(|s| s.as_str()),
3216 Some("httpuser")
3217 );
3218 assert_eq!(
3219 secrets
3220 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__BASIC_AUTH__1")
3221 .map(|s| s.as_str()),
3222 Some("httppass")
3223 );
3224 assert_eq!(
3225 secrets
3226 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_API_KEY")
3227 .map(|s| s.as_str()),
3228 Some("http-api-key")
3229 );
3230 assert_eq!(
3231 secrets
3232 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_ACCESS_TOKEN")
3233 .map(|s| s.as_str()),
3234 Some("http-access-token")
3235 );
3236 assert_eq!(
3237 secrets
3238 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__X_AUTHENTICATION")
3239 .map(|s| s.as_str()),
3240 Some("http-authentication")
3241 );
3242 assert_eq!(
3243 secrets
3244 .get("MQB__TEST_ROUTE__OUTPUT__HTTP__CUSTOM_HEADERS__AUTHORIZATION")
3245 .map(|s| s.as_str()),
3246 Some("Bearer secret-token")
3247 );
3248
3249 let route = config.get("test_route").unwrap();
3251 if let EndpointType::Kafka(k) = &route.input.endpoint_type {
3252 assert!(k.url.is_empty());
3253 assert!(k.username.is_none());
3254 assert!(k.password.is_none());
3255 assert!(k.tls.cert_password.is_none());
3256 }
3257 if let EndpointType::Http(h) = &route.output.endpoint_type {
3258 assert!(h.url.is_empty());
3259 assert!(h.basic_auth.is_none());
3260 assert!(!h.custom_headers.contains_key("X-API-Key"));
3261 assert!(!h.custom_headers.contains_key("X-Access-Token"));
3262 assert!(!h.custom_headers.contains_key("X-Authentication"));
3263 assert!(!h.custom_headers.contains_key("Authorization"));
3264 assert_eq!(
3265 h.custom_headers.get("X-Trace-Id").map(|s| s.as_str()),
3266 Some("trace-value")
3267 );
3268 }
3269 }
3270
3271 #[test]
3272 fn test_extract_sensitive_url_only_strips_authority_credentials() {
3273 let mut config = Config::new();
3274 let path_at_route = Route {
3275 output: Endpoint {
3276 endpoint_type: EndpointType::Http(HttpConfig::new(
3277 "https://example.com/path/user@example.com?email=a@b.test",
3278 )),
3279 middlewares: vec![],
3280 handler: None,
3281 },
3282 ..Default::default()
3283 };
3284 config.insert("path_at_route".to_string(), path_at_route);
3285
3286 let credential_route = Route {
3287 output: Endpoint {
3288 endpoint_type: EndpointType::Http(HttpConfig::new(
3289 "https://user:pass@example.com/path",
3290 )),
3291 middlewares: vec![],
3292 handler: None,
3293 },
3294 ..Default::default()
3295 };
3296 config.insert("credential_route".to_string(), credential_route);
3297
3298 let query_at_route = Route {
3299 output: Endpoint {
3300 endpoint_type: EndpointType::Http(HttpConfig::new(
3301 "https://example.com?next=a@b.test",
3302 )),
3303 middlewares: vec![],
3304 handler: None,
3305 },
3306 ..Default::default()
3307 };
3308 config.insert("query_at_route".to_string(), query_at_route);
3309
3310 let fragment_at_route = Route {
3311 output: Endpoint {
3312 endpoint_type: EndpointType::Http(HttpConfig::new(
3313 "https://example.com#user@example.com",
3314 )),
3315 middlewares: vec![],
3316 handler: None,
3317 },
3318 ..Default::default()
3319 };
3320 config.insert("fragment_at_route".to_string(), fragment_at_route);
3321
3322 let secrets = extract_config_secrets(&mut config);
3323
3324 if let EndpointType::Http(http) = &config.get("path_at_route").unwrap().output.endpoint_type
3325 {
3326 assert_eq!(
3327 http.url,
3328 "https://example.com/path/user@example.com?email=a@b.test"
3329 );
3330 }
3331 if let EndpointType::Http(http) =
3332 &config.get("query_at_route").unwrap().output.endpoint_type
3333 {
3334 assert_eq!(http.url, "https://example.com?next=a@b.test");
3335 }
3336 if let EndpointType::Http(http) = &config
3337 .get("fragment_at_route")
3338 .unwrap()
3339 .output
3340 .endpoint_type
3341 {
3342 assert_eq!(http.url, "https://example.com#user@example.com");
3343 }
3344 if let EndpointType::Http(http) =
3345 &config.get("credential_route").unwrap().output.endpoint_type
3346 {
3347 assert!(http.url.is_empty());
3348 }
3349 assert_eq!(
3350 secrets
3351 .get("MQB__CREDENTIAL_ROUTE__OUTPUT__HTTP__URL")
3352 .map(String::as_str),
3353 Some("https://user:pass@example.com/path")
3354 );
3355 assert!(!secrets.contains_key("MQB__PATH_AT_ROUTE__OUTPUT__HTTP__URL"));
3356 assert!(!secrets.contains_key("MQB__QUERY_AT_ROUTE__OUTPUT__HTTP__URL"));
3357 assert!(!secrets.contains_key("MQB__FRAGMENT_AT_ROUTE__OUTPUT__HTTP__URL"));
3358 }
3359
3360 #[test]
3361 fn test_memory_config_requires_topic_or_url() {
3362 let err = serde_yaml_ng::from_str::<MemoryConfig>("{}").unwrap_err();
3363 assert!(err
3364 .to_string()
3365 .contains("MemoryConfig: 'topic' (or 'url' alias) is required."));
3366 }
3367
3368 #[test]
3369 fn test_file_config_inference() {
3370 let yaml = r#"
3371mode: group_subscribe
3372path: "/tmp/test"
3373group_id: "my_group"
3374"#;
3375 let config: FileConfig = serde_yaml_ng::from_str(yaml).unwrap();
3376 match config.mode {
3377 Some(FileConsumerMode::GroupSubscribe { group_id, .. }) => {
3378 assert_eq!(group_id, "my_group")
3379 }
3380 _ => panic!("Expected GroupSubscribe"),
3381 }
3382
3383 let yaml_queue = r#"
3384mode: consume
3385path: "/tmp/test"
3386"#;
3387 let config_queue: FileConfig = serde_yaml_ng::from_str(yaml_queue).unwrap();
3388 match config_queue.mode {
3389 Some(FileConsumerMode::Consume { delete }) => assert!(!delete),
3390 _ => panic!("Expected Consume"),
3391 }
3392 }
3393}
3394
3395#[cfg(all(test, feature = "schema"))]
3396mod schema_tests {
3397 use super::*;
3398
3399 #[test]
3400 fn generate_json_schema() {
3401 let schema = schemars::schema_for!(Config);
3402 let schema_json = serde_json::to_string_pretty(&schema).unwrap();
3403
3404 let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
3405 path.push("mq-bridge.schema.json");
3406 std::fs::write(path, schema_json).expect("Failed to write schema file");
3407 }
3408}