strut_rabbitmq/routing/
exchange.rs

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/// Defines a RabbitMQ exchange to be used in definitions related to RabbitMQ
12/// routing.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Exchange {
15    /// The RabbitMQ built-in default exchange (the empty-name exchange `""`).
16    Default,
17
18    /// The RabbitMQ built-in `"amq.direct"` exchange.
19    AmqDirect,
20
21    /// The RabbitMQ built-in `"amq.fanout"` exchange.
22    AmqFanout,
23
24    /// The RabbitMQ built-in `"amq.headers"` exchange.
25    AmqHeaders,
26
27    /// The RabbitMQ built-in `"amq.match"` exchange.
28    AmqMatch,
29
30    /// The RabbitMQ built-in `"amq.topic"` exchange.
31    AmqTopic,
32
33    /// A custom, non-built-in RabbitMQ exchange.
34    Custom(CustomExchange),
35}
36
37impl Exchange {
38    /// Reports whether the exchange is one of the built-ins for this definition.
39    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    /// Reports whether the exchange is [`default`](Exchange::Default)
52    /// for this definition.
53    pub const fn is_default(&self) -> bool {
54        matches!(self, Self::Default)
55    }
56
57    /// Reports whether the exchange is [`custom`](Exchange::Custom) for
58    /// this definition.
59    pub const fn is_custom(&self) -> bool {
60        !self.is_builtin()
61    }
62
63    /// Reports the exchange name for this definition.
64    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    /// Reports the exchange kind for this definition.
77    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    /// Reports whether the exchange is durable for this definition.
90    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, // all built-in exchanges are durable
98            Self::Custom(custom_exchange) => custom_exchange.durable(),
99        }
100    }
101
102    /// Reports whether the exchange is auto-deleted for this definition.
103    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, // built-in exchanges are never auto-deleted
111            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    /// Creates a new [`ExchangeBuilder`].
130    pub fn builder() -> ExchangeBuilder {
131        ExchangeBuilder::new()
132    }
133
134    /// Creates an [`Exchange`] with the given name. This can fail if a reserved name
135    /// is used (all names starting with `amq.*` are reserved in RabbitMQ).
136    pub fn named(name: impl Into<String>) -> Result<Exchange, ExchangeError> {
137        Self::builder().with_name(name).build()
138    }
139
140    /// Returns a **built-in** exchange variant with the given name. If the given
141    /// name doesn’t match any known built-in RabbitMQ exchange — [`None`] is
142    /// returned.
143    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/// Represents the various error states that may arise when a RabbitMQ exchange
157/// definition becomes invalid.
158#[derive(Error, Debug, PartialEq, Eq)]
159pub enum ExchangeError {
160    /// Indicates a reserved name for a custom exchange.
161    #[error("invalid name for custom exchange: name '{0}' is reserved")]
162    CustomNameIsReserved(String),
163
164    /// Indicates a mismatched kind for a built-in exchange.
165    #[error(
166        "invalid configuration for built-in exchange '{exchange}' ({exchange:?}): expected '{}', found '{given_kind}'",
167        .exchange.kind(),
168    )]
169    MismatchedKindForBuiltin {
170        /// Built-in exchange
171        exchange: Exchange,
172        /// Given (mismatched) kind
173        given_kind: ExchangeKind,
174    },
175
176    /// Indicates a mismatched `durable` flag for a built-in exchange.
177    #[error(
178        "invalid configuration for built-in exchange '{exchange}' ({exchange:?}): expected durable={:?}, found durable={given_durable:?}",
179        .exchange.durable(),
180    )]
181    MismatchedDurableForBuiltin {
182        /// Built-in exchange
183        exchange: Exchange,
184        /// Given (mismatched) `durable` flag
185        given_durable: bool,
186    },
187
188    /// Indicates a mismatched `auto_delete` flag for a built-in exchange.
189    #[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        /// Built-in exchange
195        exchange: Exchange,
196        /// Given (mismatched) `auto_delete` flag
197        given_auto_delete: bool,
198    },
199}
200
201/// Represents the various error states that may arise when a RabbitMQ **custom**
202/// exchange definition becomes invalid.
203#[derive(Error, Debug, PartialEq, Eq)]
204pub enum CustomExchangeError {
205    /// Indicates an invalid name for a custom exchange.
206    #[error("invalid name for custom exchange: exchange '{0}' ({0:?}) is a built-in exchange")]
207    NameIsBuiltin(Exchange),
208
209    /// Indicates a reserved name for a custom exchange.
210    #[error("invalid name for custom exchange: name '{0}' is reserved")]
211    NameIsReserved(String),
212}
213
214/// Defines a [**custom**](Exchange::Custom) RabbitMQ exchange, as opposed
215/// to any built-in variants.
216#[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    /// Creates a new [`CustomExchangeBuilder`].
226    pub fn builder() -> CustomExchangeBuilder {
227        CustomExchangeBuilder::new()
228    }
229
230    /// Creates a **custom** [`Exchange`] with the given name. This can fail if
231    /// the given name happens to match a known built-in RabbitMQ exchange, or if
232    /// a reserved name is used (all names starting with `amq.*` are reserved in
233    /// RabbitMQ).
234    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    /// Reports the exchange name for this definition.
247    pub fn name(&self) -> &str {
248        &self.name
249    }
250
251    /// Reports the exchange kind for this definition.
252    pub fn kind(&self) -> ExchangeKind {
253        self.kind
254    }
255
256    /// Reports whether the exchange is durable for this definition.
257    pub fn durable(&self) -> bool {
258        self.durable
259    }
260
261    /// Reports whether the exchange is auto-deleted for this definition.
262    pub fn auto_delete(&self) -> bool {
263        self.auto_delete
264    }
265}
266
267/// Builds an [`Exchange`] incrementally and validates it on the final stage.
268#[derive(Debug)]
269pub struct ExchangeBuilder {
270    name: String,
271    kind: ExchangeKind,
272    durable: bool,
273    auto_delete: bool,
274}
275
276impl ExchangeBuilder {
277    /// Creates a new [`Exchange`] builder.
278    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    /// Recreates this [`Exchange`] builder with the given name.
288    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    /// Recreates this [`Exchange`] builder with the given kind.
304    pub fn with_kind(self, kind: ExchangeKind) -> Self {
305        Self {
306            kind: kind.into(),
307            ..self
308        }
309    }
310
311    /// Recreates this [`Exchange`] builder with the given `durable` flag.
312    pub fn with_durable(self, durable: bool) -> Self {
313        Self { durable, ..self }
314    }
315
316    /// Recreates this [`Exchange`] builder with the given `auto_delete` flag.
317    pub fn with_auto_delete(self, auto_delete: bool) -> Self {
318        Self {
319            auto_delete,
320            ..self
321        }
322    }
323
324    /// Finalizes the builder, validates its state, and, assuming valid state,
325    /// returns the [`Exchange`].
326    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/// Builds a [`CustomExchange`] incrementally and validates it on the final
372/// stage.
373#[derive(Debug)]
374pub struct CustomExchangeBuilder {
375    name: String,
376    kind: ExchangeKind,
377    durable: bool,
378    auto_delete: bool,
379}
380
381impl CustomExchangeBuilder {
382    /// Creates a new [`CustomExchange`] builder.
383    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    /// Recreates this [`Exchange`] builder with the given name.
393    pub fn with_name(self, name: impl Into<String>) -> Self {
394        Self {
395            name: name.into(),
396            ..self
397        }
398    }
399
400    /// Recreates this [`Exchange`] builder with the given kind.
401    pub fn with_kind(self, kind: ExchangeKind) -> Self {
402        Self {
403            kind: kind.into(),
404            ..self
405        }
406    }
407
408    /// Recreates this [`Exchange`] builder with the given `durable` flag.
409    pub fn with_durable(self, durable: bool) -> Self {
410        Self { durable, ..self }
411    }
412
413    /// Recreates this [`Exchange`] builder with the given `auto_delete` flag.
414    pub fn with_auto_delete(self, auto_delete: bool) -> Self {
415        Self {
416            auto_delete,
417            ..self
418        }
419    }
420
421    /// Finalizes the builder, validates its state, and, assuming valid state,
422    /// returns the custom [`Exchange`].
423    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        // Given
562        let input = "";
563
564        // When
565        let actual_output = serde_yml::from_str::<Exchange>(input);
566
567        // Then
568        assert!(actual_output.is_err());
569    }
570
571    #[test]
572    fn deserialize_from_string() {
573        // Given
574        let input = "\"test_exchange\"";
575        let expected_output = Exchange::Custom(CustomExchange {
576            name: "test_exchange".into(),
577            ..Default::default()
578        });
579
580        // When
581        let actual_output = serde_json::from_str::<Exchange>(input).unwrap();
582
583        // Then
584        assert_eq!(expected_output, actual_output);
585    }
586
587    #[test]
588    fn deserialize_from_full() {
589        // Given
590        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        // When
606        let actual_output = serde_yml::from_str::<Exchange>(input).unwrap();
607
608        // Then
609        assert_eq!(expected_output, actual_output);
610    }
611
612    #[test]
613    fn deserialize_builtin() {
614        // Given
615        let input = r#"
616name: amq.topic
617"#;
618        let expected_output = Exchange::AmqTopic;
619
620        // When
621        let actual_output = serde_yml::from_str::<Exchange>(input).unwrap();
622
623        // Then
624        assert_eq!(expected_output, actual_output);
625    }
626
627    #[test]
628    fn name_is_reserved() {
629        // Given
630        let input = "amq.custom";
631        let expected_output = ExchangeError::CustomNameIsReserved(input.into());
632
633        // When
634        let actual_output = Exchange::named(input).unwrap_err();
635
636        // Then
637        assert_eq!(expected_output, actual_output);
638    }
639
640    #[test]
641    fn deserialize_name_is_reserved() {
642        // Given
643        let input = r#"
644name: amq.custom
645"#;
646        let expected_output = ExchangeError::CustomNameIsReserved("amq.custom".into());
647
648        // When
649        let actual_output = serde_yml::from_str::<Exchange>(input).unwrap_err();
650
651        // Then
652        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        // Given
662        let expected_output = ExchangeError::MismatchedKindForBuiltin {
663            exchange: Exchange::AmqFanout,
664            given_kind: ExchangeKind::Headers,
665        };
666
667        // When
668        let actual_output = Exchange::builder()
669            .with_name(Exchange::AmqFanout.name())
670            .with_kind(ExchangeKind::Headers)
671            .build()
672            .unwrap_err();
673
674        // Then
675        assert_eq!(expected_output, actual_output);
676    }
677
678    #[test]
679    fn deserialize_mismatched_kind_for_builtin() {
680        // Given
681        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        // When
691        let actual_output = serde_yml::from_str::<Exchange>(input).unwrap_err();
692
693        // Then
694        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        // Given
704        let expected_output = ExchangeError::MismatchedDurableForBuiltin {
705            exchange: Exchange::AmqMatch,
706            given_durable: false,
707        };
708
709        // When
710        let actual_output = Exchange::builder()
711            .with_name(Exchange::AmqMatch.name())
712            .with_durable(false)
713            .build()
714            .unwrap_err();
715
716        // Then
717        assert_eq!(expected_output, actual_output);
718    }
719
720    #[test]
721    fn deserialize_mismatched_durable_for_builtin() {
722        // Given
723        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        // When
733        let actual_output = serde_yml::from_str::<Exchange>(input).unwrap_err();
734
735        // Then
736        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        // Given
746        let expected_output = ExchangeError::MismatchedAutoDeleteForBuiltin {
747            exchange: Exchange::AmqMatch,
748            given_auto_delete: true,
749        };
750
751        // When
752        let actual_output = Exchange::builder()
753            .with_name(Exchange::AmqMatch.name())
754            .with_auto_delete(true)
755            .build()
756            .unwrap_err();
757
758        // Then
759        assert_eq!(expected_output, actual_output);
760    }
761
762    #[test]
763    fn deserialize_mismatched_auto_delete_for_builtin() {
764        // Given
765        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        // When
775        let actual_output = serde_yml::from_str::<Exchange>(input).unwrap_err();
776
777        // Then
778        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        // Given
788        let expected_output = CustomExchangeError::NameIsBuiltin(Exchange::AmqHeaders);
789
790        // When
791        let actual_output = CustomExchange::named(Exchange::AmqHeaders.name()).unwrap_err();
792
793        // Then
794        assert_eq!(expected_output, actual_output);
795    }
796
797    #[test]
798    fn custom_name_is_reserved() {
799        // Given
800        let expected_output = CustomExchangeError::NameIsReserved("amq.custom".to_string());
801
802        // When
803        let actual_output = CustomExchange::named("amq.custom").unwrap_err();
804
805        // Then
806        assert_eq!(expected_output, actual_output);
807    }
808}