strut_rabbitmq/
config.rs

1use crate::{EgressLandscape, Handle, HandleCollection, IngressLandscape};
2use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
3use serde::{Deserialize, Deserializer};
4use serde_value::Value;
5use std::collections::BTreeMap;
6use std::fmt::Formatter;
7use strut_factory::impl_deserialize_field;
8
9/// Represents the application-level configuration section that covers everything
10/// related to RabbitMQ connectivity:
11///
12/// - server URL and credentials ([`Handle`]),
13/// - inbound message routing ([`Ingress`](crate::Ingress)),
14/// - outbound message routing ([`Egress`](crate::Egress)).
15///
16/// This config comes with a custom [`Deserialize`] implementation, to support more
17/// human-oriented textual configuration.
18#[derive(Debug, Default, Clone, PartialEq)]
19pub struct RabbitMqConfig {
20    default_handle: Handle,
21    extra_handles: HandleCollection,
22    ingress: IngressLandscape,
23    egress: EgressLandscape,
24}
25
26impl RabbitMqConfig {
27    /// Returns the default [`Handle`] for this configuration.
28    pub fn default_handle(&self) -> &Handle {
29        &self.default_handle
30    }
31
32    /// Returns the extra [`Handle`]s for this configuration.
33    pub fn extra_handles(&self) -> &HandleCollection {
34        &self.extra_handles
35    }
36
37    /// Returns the [`IngressLandscape`] for this configuration.
38    pub fn ingress(&self) -> &IngressLandscape {
39        &self.ingress
40    }
41
42    /// Returns the [`EgressLandscape`] for this configuration.
43    pub fn egress(&self) -> &EgressLandscape {
44        &self.egress
45    }
46}
47
48impl AsRef<RabbitMqConfig> for RabbitMqConfig {
49    fn as_ref(&self) -> &RabbitMqConfig {
50        self
51    }
52}
53
54const _: () = {
55    impl<'de> Deserialize<'de> for RabbitMqConfig {
56        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57        where
58            D: Deserializer<'de>,
59        {
60            deserializer.deserialize_map(RabbitMqConfigVisitor)
61        }
62    }
63
64    struct RabbitMqConfigVisitor;
65
66    impl<'de> Visitor<'de> for RabbitMqConfigVisitor {
67        type Value = RabbitMqConfig;
68
69        fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
70            formatter.write_str("a map of application RabbitMQ configuration")
71        }
72
73        fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
74        where
75            A: MapAccess<'de>,
76        {
77            let mut default_handle = None;
78            let mut extra_handles = None;
79            let mut ingress = None;
80            let mut egress = None;
81
82            let mut discarded = BTreeMap::new();
83
84            while let Some(key) = map.next_key::<Value>()? {
85                let field = RabbitMqConfigField::deserialize(key.clone()).map_err(Error::custom)?;
86
87                match field {
88                    RabbitMqConfigField::default_handle => {
89                        field.poll(&mut map, &mut default_handle)?
90                    }
91                    RabbitMqConfigField::extra_handles => {
92                        field.poll(&mut map, &mut extra_handles)?
93                    }
94                    RabbitMqConfigField::ingress => field.poll(&mut map, &mut ingress)?,
95                    RabbitMqConfigField::egress => field.poll(&mut map, &mut egress)?,
96                    RabbitMqConfigField::__ignore => {
97                        discarded.insert(key, map.next_value()?);
98                        IgnoredAny
99                    }
100                };
101            }
102
103            if default_handle.is_none() {
104                default_handle =
105                    Some(Handle::deserialize(Value::Map(discarded)).map_err(Error::custom)?);
106            }
107
108            Ok(RabbitMqConfig {
109                default_handle: default_handle.unwrap_or_default(),
110                extra_handles: extra_handles.unwrap_or_default(),
111                ingress: ingress.unwrap_or_default(),
112                egress: egress.unwrap_or_default(),
113            })
114        }
115    }
116
117    impl_deserialize_field!(
118        RabbitMqConfigField,
119        strut_deserialize::Slug::eq_as_slugs,
120        default_handle | default,
121        extra_handles | extra | extras,
122        ingress | inbound | incoming | subscriber | subscribers,
123        egress | outbound | outgoing | publisher | publishers,
124    );
125};
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::{DsnChunks, Egress, Exchange, Ingress};
131    use pretty_assertions::assert_eq;
132
133    #[test]
134    fn empty() {
135        // Given
136        let input = "";
137        let expected_output = RabbitMqConfig::default();
138
139        // When
140        let actual_output = serde_yml::from_str::<RabbitMqConfig>(input).unwrap();
141
142        // Then
143        assert_eq!(expected_output, actual_output);
144    }
145
146    #[test]
147    fn full() {
148        // Given
149        let input = r#"
150host: custom-domain.com
151port: 6879
152user: test_user
153vhost: /custom
154extra:
155  other_handle:
156    vhost: /other
157inbound:
158  in_route:
159    exchange: amq.topic
160    queue: inbound_queue
161    binding_key: inbound_binding_key
162outbound:
163  out_route:
164    exchange: amq.fanout
165"#;
166        let expected_output = RabbitMqConfig {
167            default_handle: Handle::new(
168                "default",
169                DsnChunks {
170                    host: "custom-domain.com",
171                    port: 6879,
172                    user: "test_user",
173                    vhost: "/custom",
174                    ..Default::default()
175                },
176            ),
177            extra_handles: HandleCollection::from([(
178                "other_handle",
179                Handle::new(
180                    "other_handle",
181                    DsnChunks {
182                        vhost: "/other",
183                        ..Default::default()
184                    },
185                ),
186            )]),
187            ingress: IngressLandscape::from([(
188                "in_route",
189                Ingress::builder()
190                    .with_name("in_route")
191                    .with_exchange(Exchange::AmqTopic)
192                    .with_queue_named("inbound_queue")
193                    .with_binding_key("inbound_binding_key")
194                    .build()
195                    .unwrap(),
196            )]),
197            egress: EgressLandscape::from([(
198                "out_route",
199                Egress::builder()
200                    .with_name("out_route")
201                    .with_exchange("amq.fanout")
202                    .with_routing_key("")
203                    .build()
204                    .unwrap(),
205            )]),
206        };
207
208        // When
209        let actual_output = serde_yml::from_str::<RabbitMqConfig>(input).unwrap();
210
211        // Then
212        assert_eq!(expected_output, actual_output);
213    }
214}