Skip to main content

torrust_tracker_deployer_lib/domain/topology/
enabled_services.rs

1//! Enabled Services
2//!
3//! Provides a set of enabled services in a Docker Compose topology.
4//! This is used by [`NetworkDerivation`](super::NetworkDerivation) implementations
5//! to determine network assignments based on inter-service relationships.
6
7use std::collections::HashSet;
8
9use super::service::Service;
10
11/// Set of enabled services in a topology
12///
13/// Provides information about which services are enabled in the topology.
14/// This is needed because network assignments depend on inter-service
15/// relationships (e.g., Tracker needs Metrics network only if Prometheus exists).
16///
17/// # Design
18///
19/// Uses a `HashSet<Service>` internally, following the Open/Closed principle:
20/// adding new services doesn't require adding new fields or methods.
21///
22/// # Examples
23///
24/// ```rust
25/// use torrust_tracker_deployer_lib::domain::topology::{EnabledServices, Service};
26///
27/// // Create with specific services enabled
28/// let enabled = EnabledServices::from(&[Service::Prometheus, Service::Grafana]);
29///
30/// assert!(enabled.has(Service::Prometheus));
31/// assert!(enabled.has(Service::Grafana));
32/// assert!(!enabled.has(Service::Caddy));
33/// assert!(!enabled.has(Service::MySQL));
34/// ```
35#[derive(Debug, Clone, Default)]
36pub struct EnabledServices {
37    /// Set of enabled services in this topology
38    services: HashSet<Service>,
39}
40
41impl EnabledServices {
42    /// Creates a new set of enabled services from a slice
43    ///
44    /// This is the primary constructor. The services slice represents
45    /// which optional services are present in the deployment.
46    ///
47    /// # Examples
48    ///
49    /// ```rust
50    /// use torrust_tracker_deployer_lib::domain::topology::{EnabledServices, Service};
51    ///
52    /// let enabled = EnabledServices::from(&[Service::Prometheus, Service::MySQL]);
53    ///
54    /// assert!(enabled.has(Service::Prometheus));
55    /// assert!(enabled.has(Service::MySQL));
56    /// ```
57    #[must_use]
58    pub fn from(services: &[Service]) -> Self {
59        Self {
60            services: services.iter().copied().collect(),
61        }
62    }
63
64    /// Checks if a specific service is enabled
65    ///
66    /// # Examples
67    ///
68    /// ```rust
69    /// use torrust_tracker_deployer_lib::domain::topology::{EnabledServices, Service};
70    ///
71    /// let enabled = EnabledServices::from(&[Service::Caddy]);
72    ///
73    /// assert!(enabled.has(Service::Caddy));
74    /// assert!(!enabled.has(Service::MySQL));
75    /// ```
76    #[must_use]
77    pub fn has(&self, service: Service) -> bool {
78        self.services.contains(&service)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    mod from_constructor {
87        use super::*;
88
89        #[test]
90        fn it_should_create_empty_set_when_given_empty_slice() {
91            let enabled = EnabledServices::from(&[]);
92
93            assert!(!enabled.has(Service::Prometheus));
94            assert!(!enabled.has(Service::Grafana));
95            assert!(!enabled.has(Service::Caddy));
96            assert!(!enabled.has(Service::MySQL));
97            assert!(!enabled.has(Service::Tracker));
98        }
99
100        #[test]
101        fn it_should_create_set_with_single_service_when_given_one_service() {
102            let enabled = EnabledServices::from(&[Service::Prometheus]);
103
104            assert!(enabled.has(Service::Prometheus));
105            assert!(!enabled.has(Service::Grafana));
106        }
107
108        #[test]
109        fn it_should_create_set_with_multiple_services_when_given_multiple_services() {
110            let enabled =
111                EnabledServices::from(&[Service::Prometheus, Service::Grafana, Service::MySQL]);
112
113            assert!(enabled.has(Service::Prometheus));
114            assert!(enabled.has(Service::Grafana));
115            assert!(enabled.has(Service::MySQL));
116            assert!(!enabled.has(Service::Caddy));
117            assert!(!enabled.has(Service::Tracker));
118        }
119
120        #[test]
121        fn it_should_deduplicate_when_given_duplicate_services() {
122            let enabled = EnabledServices::from(&[
123                Service::Prometheus,
124                Service::Prometheus,
125                Service::Grafana,
126            ]);
127
128            assert!(enabled.has(Service::Prometheus));
129            assert!(enabled.has(Service::Grafana));
130        }
131
132        #[test]
133        fn it_should_include_all_service_types_when_given_all_services() {
134            let enabled = EnabledServices::from(Service::all());
135
136            for service in Service::all() {
137                assert!(enabled.has(*service), "Expected {service:?} to be present");
138            }
139        }
140    }
141
142    mod has_method {
143        use super::*;
144
145        #[test]
146        fn it_should_return_true_when_service_is_present() {
147            let enabled = EnabledServices::from(&[Service::Caddy]);
148
149            assert!(enabled.has(Service::Caddy));
150        }
151
152        #[test]
153        fn it_should_return_false_when_service_is_absent() {
154            let enabled = EnabledServices::from(&[Service::Prometheus]);
155
156            assert!(!enabled.has(Service::Caddy));
157        }
158
159        #[test]
160        fn it_should_return_false_for_all_services_when_empty() {
161            let enabled = EnabledServices::from(&[]);
162
163            for service in Service::all() {
164                assert!(!enabled.has(*service), "Expected {service:?} to be absent");
165            }
166        }
167    }
168
169    mod default_trait {
170        use super::*;
171
172        #[test]
173        fn it_should_create_empty_set_when_using_default() {
174            let enabled = EnabledServices::default();
175
176            for service in Service::all() {
177                assert!(!enabled.has(*service), "Expected {service:?} to be absent");
178            }
179        }
180    }
181
182    mod clone_trait {
183        use super::*;
184
185        #[test]
186        fn it_should_create_independent_copy_when_cloned() {
187            let original = EnabledServices::from(&[Service::Prometheus, Service::Grafana]);
188            let cloned = original.clone();
189
190            assert!(cloned.has(Service::Prometheus));
191            assert!(cloned.has(Service::Grafana));
192            assert!(!cloned.has(Service::MySQL));
193        }
194    }
195}