1use crate::{
2 ExchangeKind, EXCHANGE_AMQ_DIRECT, EXCHANGE_AMQ_FANOUT, EXCHANGE_AMQ_HEADERS,
3 EXCHANGE_AMQ_MATCH, EXCHANGE_AMQ_TOPIC, EXCHANGE_DEFAULT,
4};
5use serde::de::{Error, MapAccess, Visitor};
6use serde::{Deserialize, Deserializer};
7use std::fmt::{Display, Formatter};
8use strut_factory::impl_deserialize_field;
9use thiserror::Error;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Exchange {
15 Default,
17
18 AmqDirect,
20
21 AmqFanout,
23
24 AmqHeaders,
26
27 AmqMatch,
29
30 AmqTopic,
32
33 Custom(CustomExchange),
35}
36
37impl Exchange {
38 pub const fn is_builtin(&self) -> bool {
40 match self {
41 Self::Default
42 | Self::AmqDirect
43 | Self::AmqFanout
44 | Self::AmqHeaders
45 | Self::AmqMatch
46 | Self::AmqTopic => true,
47 Self::Custom(_) => false,
48 }
49 }
50
51 pub const fn is_default(&self) -> bool {
54 matches!(self, Self::Default)
55 }
56
57 pub const fn is_custom(&self) -> bool {
60 !self.is_builtin()
61 }
62
63 pub fn name(&self) -> &str {
65 match self {
66 Self::Default => EXCHANGE_DEFAULT,
67 Self::AmqDirect => EXCHANGE_AMQ_DIRECT,
68 Self::AmqFanout => EXCHANGE_AMQ_FANOUT,
69 Self::AmqHeaders => EXCHANGE_AMQ_HEADERS,
70 Self::AmqMatch => EXCHANGE_AMQ_MATCH,
71 Self::AmqTopic => EXCHANGE_AMQ_TOPIC,
72 Exchange::Custom(custom_exchange) => custom_exchange.name(),
73 }
74 }
75
76 pub fn kind(&self) -> ExchangeKind {
78 match self {
79 Self::Default => ExchangeKind::Direct,
80 Self::AmqDirect => ExchangeKind::Direct,
81 Self::AmqFanout => ExchangeKind::Fanout,
82 Self::AmqHeaders => ExchangeKind::Headers,
83 Self::AmqMatch => ExchangeKind::Headers,
84 Self::AmqTopic => ExchangeKind::Topic,
85 Self::Custom(custom_exchange) => custom_exchange.kind(),
86 }
87 }
88
89 pub fn durable(&self) -> bool {
91 match self {
92 Self::Default
93 | Self::AmqDirect
94 | Self::AmqFanout
95 | Self::AmqHeaders
96 | Self::AmqMatch
97 | Self::AmqTopic => true, Self::Custom(custom_exchange) => custom_exchange.durable(),
99 }
100 }
101
102 pub fn auto_delete(&self) -> bool {
104 match self {
105 Self::Default
106 | Self::AmqDirect
107 | Self::AmqFanout
108 | Self::AmqHeaders
109 | Self::AmqMatch
110 | Self::AmqTopic => false, Self::Custom(custom_exchange) => custom_exchange.auto_delete(),
112 }
113 }
114}
115
116impl Display for Exchange {
117 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
118 f.write_str(self.name())
119 }
120}
121
122impl Default for Exchange {
123 fn default() -> Self {
124 Self::Default
125 }
126}
127
128impl Exchange {
129 pub fn builder() -> ExchangeBuilder {
131 ExchangeBuilder::new()
132 }
133
134 pub fn named(name: impl Into<String>) -> Result<Exchange, ExchangeError> {
137 Self::builder().with_name(name).build()
138 }
139
140 pub fn try_builtin_named(name: impl AsRef<str>) -> Option<Self> {
144 match name.as_ref() {
145 EXCHANGE_DEFAULT => Some(Self::Default),
146 EXCHANGE_AMQ_DIRECT => Some(Self::AmqDirect),
147 EXCHANGE_AMQ_FANOUT => Some(Self::AmqFanout),
148 EXCHANGE_AMQ_HEADERS => Some(Self::AmqHeaders),
149 EXCHANGE_AMQ_MATCH => Some(Self::AmqMatch),
150 EXCHANGE_AMQ_TOPIC => Some(Self::AmqTopic),
151 _ => None,
152 }
153 }
154}
155
156#[derive(Error, Debug, PartialEq, Eq)]
159pub enum ExchangeError {
160 #[error("invalid name for custom exchange: name '{0}' is reserved")]
162 CustomNameIsReserved(String),
163
164 #[error(
166 "invalid configuration for built-in exchange '{exchange}' ({exchange:?}): expected '{}', found '{given_kind}'",
167 .exchange.kind(),
168 )]
169 MismatchedKindForBuiltin {
170 exchange: Exchange,
172 given_kind: ExchangeKind,
174 },
175
176 #[error(
178 "invalid configuration for built-in exchange '{exchange}' ({exchange:?}): expected durable={:?}, found durable={given_durable:?}",
179 .exchange.durable(),
180 )]
181 MismatchedDurableForBuiltin {
182 exchange: Exchange,
184 given_durable: bool,
186 },
187
188 #[error(
190 "invalid configuration for built-in exchange '{exchange}' ({exchange:?}): expected auto_delete={:?}, found auto_delete={given_auto_delete:?}",
191 .exchange.auto_delete(),
192 )]
193 MismatchedAutoDeleteForBuiltin {
194 exchange: Exchange,
196 given_auto_delete: bool,
198 },
199}
200
201#[derive(Error, Debug, PartialEq, Eq)]
204pub enum CustomExchangeError {
205 #[error("invalid name for custom exchange: exchange '{0}' ({0:?}) is a built-in exchange")]
207 NameIsBuiltin(Exchange),
208
209 #[error("invalid name for custom exchange: name '{0}' is reserved")]
211 NameIsReserved(String),
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct CustomExchange {
218 name: String,
219 kind: ExchangeKind,
220 durable: bool,
221 auto_delete: bool,
222}
223
224impl CustomExchange {
225 pub fn builder() -> CustomExchangeBuilder {
227 CustomExchangeBuilder::new()
228 }
229
230 pub fn named(name: impl Into<String>) -> Result<Exchange, CustomExchangeError> {
235 Self::builder().with_name(name).build()
236 }
237}
238
239impl From<CustomExchange> for Exchange {
240 fn from(value: CustomExchange) -> Self {
241 Exchange::Custom(value)
242 }
243}
244
245impl CustomExchange {
246 pub fn name(&self) -> &str {
248 &self.name
249 }
250
251 pub fn kind(&self) -> ExchangeKind {
253 self.kind
254 }
255
256 pub fn durable(&self) -> bool {
258 self.durable
259 }
260
261 pub fn auto_delete(&self) -> bool {
263 self.auto_delete
264 }
265}
266
267#[derive(Debug)]
269pub struct ExchangeBuilder {
270 name: String,
271 kind: ExchangeKind,
272 durable: bool,
273 auto_delete: bool,
274}
275
276impl ExchangeBuilder {
277 pub fn new() -> Self {
279 Self {
280 name: "".into(),
281 kind: CustomExchange::default_kind(),
282 durable: CustomExchange::default_durable(),
283 auto_delete: CustomExchange::default_auto_delete(),
284 }
285 }
286
287 pub fn with_name(self, name: impl Into<String>) -> Self {
289 let name = name.into();
290
291 if let Some(builtin_exchange) = Exchange::try_builtin_named(&name) {
292 return Self {
293 name,
294 kind: builtin_exchange.kind(),
295 durable: builtin_exchange.durable(),
296 auto_delete: builtin_exchange.auto_delete(),
297 };
298 }
299
300 Self { name, ..self }
301 }
302
303 pub fn with_kind(self, kind: ExchangeKind) -> Self {
305 Self {
306 kind: kind.into(),
307 ..self
308 }
309 }
310
311 pub fn with_durable(self, durable: bool) -> Self {
313 Self { durable, ..self }
314 }
315
316 pub fn with_auto_delete(self, auto_delete: bool) -> Self {
318 Self {
319 auto_delete,
320 ..self
321 }
322 }
323
324 pub fn build(self) -> Result<Exchange, ExchangeError> {
327 self.validate()?;
328
329 if let Some(builtin_exchange) = Exchange::try_builtin_named(&self.name) {
330 return Ok(builtin_exchange);
331 }
332
333 Ok(Exchange::Custom(CustomExchange {
334 name: self.name,
335 kind: self.kind,
336 durable: self.durable,
337 auto_delete: self.auto_delete,
338 }))
339 }
340
341 fn validate(&self) -> Result<(), ExchangeError> {
342 if let Some(builtin_exchange) = Exchange::try_builtin_named(&self.name) {
343 if self.kind != builtin_exchange.kind() {
344 return Err(ExchangeError::MismatchedKindForBuiltin {
345 exchange: builtin_exchange,
346 given_kind: self.kind,
347 });
348 };
349 if self.durable != builtin_exchange.durable() {
350 return Err(ExchangeError::MismatchedDurableForBuiltin {
351 exchange: builtin_exchange,
352 given_durable: self.durable,
353 });
354 };
355 if self.auto_delete != builtin_exchange.auto_delete() {
356 return Err(ExchangeError::MismatchedAutoDeleteForBuiltin {
357 exchange: builtin_exchange,
358 given_auto_delete: self.auto_delete,
359 });
360 };
361 } else {
362 if self.name.starts_with("amq.") {
363 return Err(ExchangeError::CustomNameIsReserved(self.name.to_string()));
364 }
365 }
366
367 Ok(())
368 }
369}
370
371#[derive(Debug)]
374pub struct CustomExchangeBuilder {
375 name: String,
376 kind: ExchangeKind,
377 durable: bool,
378 auto_delete: bool,
379}
380
381impl CustomExchangeBuilder {
382 pub fn new() -> Self {
384 Self {
385 name: "".into(),
386 kind: CustomExchange::default_kind(),
387 durable: CustomExchange::default_durable(),
388 auto_delete: CustomExchange::default_auto_delete(),
389 }
390 }
391
392 pub fn with_name(self, name: impl Into<String>) -> Self {
394 Self {
395 name: name.into(),
396 ..self
397 }
398 }
399
400 pub fn with_kind(self, kind: ExchangeKind) -> Self {
402 Self {
403 kind: kind.into(),
404 ..self
405 }
406 }
407
408 pub fn with_durable(self, durable: bool) -> Self {
410 Self { durable, ..self }
411 }
412
413 pub fn with_auto_delete(self, auto_delete: bool) -> Self {
415 Self {
416 auto_delete,
417 ..self
418 }
419 }
420
421 pub fn build(self) -> Result<Exchange, CustomExchangeError> {
424 self.validate()?;
425
426 Ok(Exchange::Custom(CustomExchange {
427 name: self.name,
428 kind: self.kind,
429 durable: self.durable,
430 auto_delete: self.auto_delete,
431 }))
432 }
433
434 fn validate(&self) -> Result<(), CustomExchangeError> {
435 if let Some(builtin_exchange) = Exchange::try_builtin_named(&self.name) {
436 return Err(CustomExchangeError::NameIsBuiltin(builtin_exchange));
437 } else {
438 if self.name.starts_with("amq.") {
439 return Err(CustomExchangeError::NameIsReserved(self.name.to_string()));
440 }
441 }
442
443 Ok(())
444 }
445}
446
447impl CustomExchange {
448 fn default_kind() -> ExchangeKind {
449 ExchangeKind::Direct
450 }
451
452 fn default_durable() -> bool {
453 true
454 }
455
456 fn default_auto_delete() -> bool {
457 false
458 }
459}
460
461#[cfg(test)]
462impl Default for CustomExchange {
463 fn default() -> Self {
464 Self {
465 name: "".into(),
466 kind: Self::default_kind(),
467 durable: Self::default_durable(),
468 auto_delete: Self::default_auto_delete(),
469 }
470 }
471}
472
473const _: () = {
474 impl<'de> Deserialize<'de> for Exchange {
475 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
476 where
477 D: Deserializer<'de>,
478 {
479 deserializer.deserialize_any(ExchangeVisitor)
480 }
481 }
482
483 struct ExchangeVisitor;
484
485 impl<'de> Visitor<'de> for ExchangeVisitor {
486 type Value = Exchange;
487
488 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
489 formatter.write_str("a map of RabbitMQ exchange or a string exchange name")
490 }
491
492 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
493 where
494 E: Error,
495 {
496 Exchange::named(value).map_err(Error::custom)
497 }
498
499 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
500 where
501 E: Error,
502 {
503 Exchange::named(value).map_err(Error::custom)
504 }
505
506 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
507 where
508 A: MapAccess<'de>,
509 {
510 let mut name: Option<String> = None;
511 let mut kind = None;
512 let mut durable = None;
513 let mut auto_delete = None;
514
515 while let Some(key) = map.next_key()? {
516 match key {
517 ExchangeField::name => key.poll(&mut map, &mut name)?,
518 ExchangeField::kind => key.poll(&mut map, &mut kind)?,
519 ExchangeField::durable => key.poll(&mut map, &mut durable)?,
520 ExchangeField::auto_delete => key.poll(&mut map, &mut auto_delete)?,
521 ExchangeField::__ignore => map.next_value()?,
522 };
523 }
524
525 let name = ExchangeField::name.take(name)?;
526 let mut builder = Exchange::builder().with_name(name);
527
528 if let Some(kind) = kind {
529 builder = builder.with_kind(kind);
530 }
531
532 if let Some(durable) = durable {
533 builder = builder.with_durable(durable);
534 }
535
536 if let Some(auto_delete) = auto_delete {
537 builder = builder.with_auto_delete(auto_delete);
538 }
539
540 Ok(builder.build().map_err(Error::custom)?)
541 }
542 }
543
544 impl_deserialize_field!(
545 ExchangeField,
546 strut_deserialize::Slug::eq_as_slugs,
547 name,
548 kind,
549 durable,
550 auto_delete,
551 );
552};
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use pretty_assertions::assert_eq;
558
559 #[test]
560 fn deserialize_from_empty() {
561 let input = "";
563
564 let actual_output = serde_yml::from_str::<Exchange>(input);
566
567 assert!(actual_output.is_err());
569 }
570
571 #[test]
572 fn deserialize_from_string() {
573 let input = "\"test_exchange\"";
575 let expected_output = Exchange::Custom(CustomExchange {
576 name: "test_exchange".into(),
577 ..Default::default()
578 });
579
580 let actual_output = serde_json::from_str::<Exchange>(input).unwrap();
582
583 assert_eq!(expected_output, actual_output);
585 }
586
587 #[test]
588 fn deserialize_from_full() {
589 let input = r#"
591extra_field: ignored
592name: test_exchange
593kind: hash_key
594durable: false
595auto_delete: true
596"#;
597 let expected_output = Exchange::Custom(CustomExchange {
598 name: "test_exchange".into(),
599 kind: ExchangeKind::HashKey,
600 durable: false,
601 auto_delete: true,
602 ..Default::default()
603 });
604
605 let actual_output = serde_yml::from_str::<Exchange>(input).unwrap();
607
608 assert_eq!(expected_output, actual_output);
610 }
611
612 #[test]
613 fn deserialize_builtin() {
614 let input = r#"
616name: amq.topic
617"#;
618 let expected_output = Exchange::AmqTopic;
619
620 let actual_output = serde_yml::from_str::<Exchange>(input).unwrap();
622
623 assert_eq!(expected_output, actual_output);
625 }
626
627 #[test]
628 fn name_is_reserved() {
629 let input = "amq.custom";
631 let expected_output = ExchangeError::CustomNameIsReserved(input.into());
632
633 let actual_output = Exchange::named(input).unwrap_err();
635
636 assert_eq!(expected_output, actual_output);
638 }
639
640 #[test]
641 fn deserialize_name_is_reserved() {
642 let input = r#"
644name: amq.custom
645"#;
646 let expected_output = ExchangeError::CustomNameIsReserved("amq.custom".into());
647
648 let actual_output = serde_yml::from_str::<Exchange>(input).unwrap_err();
650
651 assert!(
653 actual_output
654 .to_string()
655 .starts_with(&expected_output.to_string()),
656 );
657 }
658
659 #[test]
660 fn mismatched_kind_for_builtin() {
661 let expected_output = ExchangeError::MismatchedKindForBuiltin {
663 exchange: Exchange::AmqFanout,
664 given_kind: ExchangeKind::Headers,
665 };
666
667 let actual_output = Exchange::builder()
669 .with_name(Exchange::AmqFanout.name())
670 .with_kind(ExchangeKind::Headers)
671 .build()
672 .unwrap_err();
673
674 assert_eq!(expected_output, actual_output);
676 }
677
678 #[test]
679 fn deserialize_mismatched_kind_for_builtin() {
680 let input = r#"
682name: amq.fanout
683kind: headers
684"#;
685 let expected_output = ExchangeError::MismatchedKindForBuiltin {
686 exchange: Exchange::AmqFanout,
687 given_kind: ExchangeKind::Headers,
688 };
689
690 let actual_output = serde_yml::from_str::<Exchange>(input).unwrap_err();
692
693 assert!(
695 actual_output
696 .to_string()
697 .starts_with(&expected_output.to_string()),
698 );
699 }
700
701 #[test]
702 fn mismatched_durable_for_builtin() {
703 let expected_output = ExchangeError::MismatchedDurableForBuiltin {
705 exchange: Exchange::AmqMatch,
706 given_durable: false,
707 };
708
709 let actual_output = Exchange::builder()
711 .with_name(Exchange::AmqMatch.name())
712 .with_durable(false)
713 .build()
714 .unwrap_err();
715
716 assert_eq!(expected_output, actual_output);
718 }
719
720 #[test]
721 fn deserialize_mismatched_durable_for_builtin() {
722 let input = r#"
724name: amq.match
725durable: false
726"#;
727 let expected_output = ExchangeError::MismatchedDurableForBuiltin {
728 exchange: Exchange::AmqMatch,
729 given_durable: false,
730 };
731
732 let actual_output = serde_yml::from_str::<Exchange>(input).unwrap_err();
734
735 assert!(
737 actual_output
738 .to_string()
739 .starts_with(&expected_output.to_string()),
740 );
741 }
742
743 #[test]
744 fn mismatched_auto_delete_for_builtin() {
745 let expected_output = ExchangeError::MismatchedAutoDeleteForBuiltin {
747 exchange: Exchange::AmqMatch,
748 given_auto_delete: true,
749 };
750
751 let actual_output = Exchange::builder()
753 .with_name(Exchange::AmqMatch.name())
754 .with_auto_delete(true)
755 .build()
756 .unwrap_err();
757
758 assert_eq!(expected_output, actual_output);
760 }
761
762 #[test]
763 fn deserialize_mismatched_auto_delete_for_builtin() {
764 let input = r#"
766name: amq.match
767auto_delete: true
768"#;
769 let expected_output = ExchangeError::MismatchedAutoDeleteForBuiltin {
770 exchange: Exchange::AmqMatch,
771 given_auto_delete: true,
772 };
773
774 let actual_output = serde_yml::from_str::<Exchange>(input).unwrap_err();
776
777 assert!(
779 actual_output
780 .to_string()
781 .starts_with(&expected_output.to_string()),
782 );
783 }
784
785 #[test]
786 fn custom_name_is_builtin() {
787 let expected_output = CustomExchangeError::NameIsBuiltin(Exchange::AmqHeaders);
789
790 let actual_output = CustomExchange::named(Exchange::AmqHeaders.name()).unwrap_err();
792
793 assert_eq!(expected_output, actual_output);
795 }
796
797 #[test]
798 fn custom_name_is_reserved() {
799 let expected_output = CustomExchangeError::NameIsReserved("amq.custom".to_string());
801
802 let actual_output = CustomExchange::named("amq.custom").unwrap_err();
804
805 assert_eq!(expected_output, actual_output);
807 }
808}