Skip to main content

torrust_tracker_deployer_lib/domain/provider/
config.rs

1//! Provider Configuration Domain Types
2//!
3//! This module contains the `ProviderConfig` enum that aggregates all
4//! provider-specific configurations. Individual provider configurations
5//! are defined in their own modules (`lxd`, `hetzner`).
6//!
7//! These types use validated domain types (like `ProfileName`) and represent
8//! the semantic meaning of provider configuration.
9//!
10//! For config types used in JSON deserialization, see
11//! `application::command_handlers::create::config::provider`.
12//!
13//! # Layer Separation
14//!
15//! - **Domain types** (this module): `ProviderConfig`, `LxdConfig`, `HetznerConfig`
16//!   - Use validated domain types (e.g., `ProfileName`)
17//!   - Represent semantic meaning of configuration
18//!
19//! - **Application config types** (`application::command_handlers::create::config::provider`):
20//!   - `ProviderSection`, `LxdProviderSection`, `HetznerProviderSection`
21//!   - Use raw primitives (e.g., `String`)
22//!   - Handle JSON deserialization and conversion to domain types
23
24use serde::{Deserialize, Serialize};
25
26use super::hetzner::HetznerConfig;
27use super::lxd::LxdConfig;
28use super::Provider;
29
30/// Provider-specific configuration (Domain Type)
31///
32/// Each variant contains the configuration fields specific to that provider
33/// using **validated domain types** (e.g., `ProfileName` instead of `String`).
34///
35/// This is a tagged enum that serializes/deserializes based on the `"provider"` field.
36///
37/// # Note on Layer Placement
38///
39/// This is a **domain type** with validated fields. For JSON deserialization,
40/// use `ProviderSection` in the application layer, then convert to this type.
41///
42/// # Examples
43///
44/// ```rust
45/// use torrust_tracker_deployer_lib::domain::provider::{ProviderConfig, LxdConfig, Provider};
46/// use torrust_tracker_deployer_lib::domain::ProfileName;
47///
48/// let lxd_config = ProviderConfig::Lxd(LxdConfig {
49///     profile_name: ProfileName::new("torrust-profile").unwrap(),
50/// });
51///
52/// assert_eq!(lxd_config.provider(), Provider::Lxd);
53/// assert_eq!(lxd_config.provider_name(), "lxd");
54/// ```
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(tag = "provider")]
57pub enum ProviderConfig {
58    /// LXD provider configuration
59    #[serde(rename = "lxd")]
60    Lxd(LxdConfig),
61
62    /// Hetzner provider configuration
63    #[serde(rename = "hetzner")]
64    Hetzner(HetznerConfig),
65}
66
67impl ProviderConfig {
68    /// Returns the provider type.
69    ///
70    /// # Examples
71    ///
72    /// ```rust
73    /// use torrust_tracker_deployer_lib::domain::provider::{ProviderConfig, LxdConfig, Provider};
74    /// use torrust_tracker_deployer_lib::domain::ProfileName;
75    ///
76    /// let config = ProviderConfig::Lxd(LxdConfig {
77    ///     profile_name: ProfileName::new("test").unwrap(),
78    /// });
79    /// assert_eq!(config.provider(), Provider::Lxd);
80    /// ```
81    #[must_use]
82    pub fn provider(&self) -> Provider {
83        match self {
84            Self::Lxd(_) => Provider::Lxd,
85            Self::Hetzner(_) => Provider::Hetzner,
86        }
87    }
88
89    /// Returns the provider name as used in directory paths.
90    ///
91    /// This is a convenience method that delegates to `self.provider().as_str()`.
92    ///
93    /// # Examples
94    ///
95    /// ```rust
96    /// use torrust_tracker_deployer_lib::domain::provider::{ProviderConfig, LxdConfig};
97    /// use torrust_tracker_deployer_lib::domain::ProfileName;
98    ///
99    /// let config = ProviderConfig::Lxd(LxdConfig {
100    ///     profile_name: ProfileName::new("test").unwrap(),
101    /// });
102    /// assert_eq!(config.provider_name(), "lxd");
103    /// ```
104    #[must_use]
105    pub fn provider_name(&self) -> &'static str {
106        self.provider().as_str()
107    }
108
109    /// Returns a human-readable display name for the provider.
110    ///
111    /// This method converts the internal provider identifier to a user-friendly
112    /// format suitable for display in CLI output, logs, or documentation.
113    ///
114    /// # Examples
115    ///
116    /// ```rust
117    /// use torrust_tracker_deployer_lib::domain::provider::{ProviderConfig, LxdConfig, HetznerConfig};
118    /// use torrust_tracker_deployer_lib::domain::ProfileName;
119    /// use torrust_tracker_deployer_lib::shared::secrets::ApiToken;
120    ///
121    /// let lxd_config = ProviderConfig::Lxd(LxdConfig {
122    ///     profile_name: ProfileName::new("test").unwrap(),
123    /// });
124    /// assert_eq!(lxd_config.provider_display_name(), "LXD");
125    ///
126    /// let hetzner_config = ProviderConfig::Hetzner(HetznerConfig {
127    ///     api_token: ApiToken::from("token"),
128    ///     server_type: "cx22".to_string(),
129    ///     location: "nbg1".to_string(),
130    ///     image: "ubuntu-24.04".to_string(),
131    /// });
132    /// assert_eq!(hetzner_config.provider_display_name(), "Hetzner Cloud");
133    /// ```
134    #[must_use]
135    pub fn provider_display_name(&self) -> &'static str {
136        match self {
137            Self::Lxd(_) => "LXD",
138            Self::Hetzner(_) => "Hetzner Cloud",
139        }
140    }
141
142    /// Returns a reference to the LXD configuration if this is an LXD provider.
143    ///
144    /// # Returns
145    ///
146    /// - `Some(&LxdConfig)` if the provider is LXD
147    /// - `None` otherwise
148    ///
149    /// # Examples
150    ///
151    /// ```rust
152    /// use torrust_tracker_deployer_lib::domain::provider::{ProviderConfig, LxdConfig, HetznerConfig};
153    /// use torrust_tracker_deployer_lib::domain::ProfileName;
154    /// use torrust_tracker_deployer_lib::shared::secrets::ApiToken;
155    ///
156    /// let lxd_config = ProviderConfig::Lxd(LxdConfig {
157    ///     profile_name: ProfileName::new("test").unwrap(),
158    /// });
159    /// assert!(lxd_config.as_lxd().is_some());
160    ///
161    /// let hetzner_config = ProviderConfig::Hetzner(HetznerConfig {
162    ///     api_token: ApiToken::from("token"),
163    ///     server_type: "cx22".to_string(),
164    ///     location: "nbg1".to_string(),
165    ///     image: "ubuntu-24.04".to_string(),
166    /// });
167    /// assert!(hetzner_config.as_lxd().is_none());
168    /// ```
169    #[must_use]
170    pub fn as_lxd(&self) -> Option<&LxdConfig> {
171        match self {
172            Self::Lxd(config) => Some(config),
173            Self::Hetzner(_) => None,
174        }
175    }
176
177    /// Returns a reference to the Hetzner configuration if this is a Hetzner provider.
178    ///
179    /// # Returns
180    ///
181    /// - `Some(&HetznerConfig)` if the provider is Hetzner
182    /// - `None` otherwise
183    #[must_use]
184    pub fn as_hetzner(&self) -> Option<&HetznerConfig> {
185        match self {
186            Self::Lxd(_) => None,
187            Self::Hetzner(config) => Some(config),
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use crate::domain::ProfileName;
196
197    fn create_lxd_config() -> ProviderConfig {
198        ProviderConfig::Lxd(LxdConfig {
199            profile_name: ProfileName::new("torrust-profile").unwrap(),
200        })
201    }
202
203    fn create_hetzner_config() -> ProviderConfig {
204        use crate::shared::ApiToken;
205
206        ProviderConfig::Hetzner(HetznerConfig {
207            api_token: ApiToken::from("test-token"),
208            server_type: "cx22".to_string(),
209            location: "nbg1".to_string(),
210            image: "ubuntu-24.04".to_string(),
211        })
212    }
213
214    #[test]
215    fn it_should_return_lxd_provider_when_lxd_config_queried() {
216        let config = create_lxd_config();
217        assert_eq!(config.provider(), Provider::Lxd);
218        assert_eq!(config.provider_name(), "lxd");
219    }
220
221    #[test]
222    fn it_should_return_hetzner_provider_when_hetzner_config_queried() {
223        let config = create_hetzner_config();
224        assert_eq!(config.provider(), Provider::Hetzner);
225        assert_eq!(config.provider_name(), "hetzner");
226    }
227
228    #[test]
229    fn it_should_return_some_lxd_config_when_as_lxd_called_on_lxd_variant() {
230        let config = create_lxd_config();
231        assert!(config.as_lxd().is_some());
232        assert!(config.as_hetzner().is_none());
233    }
234
235    #[test]
236    fn it_should_return_some_hetzner_config_when_as_hetzner_called_on_hetzner_variant() {
237        let config = create_hetzner_config();
238        assert!(config.as_hetzner().is_some());
239        assert!(config.as_lxd().is_none());
240    }
241
242    #[test]
243    fn it_should_serialize_lxd_config_to_json_with_provider_tag() {
244        let config = create_lxd_config();
245        let json = serde_json::to_string(&config).unwrap();
246
247        assert!(json.contains("\"provider\":\"lxd\""));
248        assert!(json.contains("\"profile_name\":\"torrust-profile\""));
249    }
250
251    #[test]
252    fn it_should_serialize_hetzner_config_to_json_with_provider_tag() {
253        let config = create_hetzner_config();
254        let json = serde_json::to_string(&config).unwrap();
255
256        assert!(json.contains("\"provider\":\"hetzner\""));
257        assert!(json.contains("\"api_token\":\"test-token\""));
258        assert!(json.contains("\"server_type\":\"cx22\""));
259        assert!(json.contains("\"location\":\"nbg1\""));
260    }
261
262    #[test]
263    fn it_should_deserialize_lxd_config_from_json_with_provider_tag() {
264        let json = r#"{"provider":"lxd","profile_name":"torrust-profile"}"#;
265        let config: ProviderConfig = serde_json::from_str(json).unwrap();
266
267        assert_eq!(config.provider(), Provider::Lxd);
268        assert_eq!(
269            config.as_lxd().unwrap().profile_name.as_str(),
270            "torrust-profile"
271        );
272    }
273
274    #[test]
275    fn it_should_deserialize_hetzner_config_from_json_with_provider_tag() {
276        let json = r#"{"provider":"hetzner","api_token":"token","server_type":"cx22","location":"nbg1","image":"ubuntu-24.04"}"#;
277        let config: ProviderConfig = serde_json::from_str(json).unwrap();
278
279        assert_eq!(config.provider(), Provider::Hetzner);
280        let hetzner = config.as_hetzner().unwrap();
281        assert_eq!(hetzner.api_token.expose_secret(), "token");
282        assert_eq!(hetzner.server_type, "cx22");
283        assert_eq!(hetzner.location, "nbg1");
284        assert_eq!(hetzner.image, "ubuntu-24.04");
285    }
286
287    #[test]
288    fn it_should_be_cloneable_when_cloned() {
289        let config = create_lxd_config();
290        let cloned = config.clone();
291        assert_eq!(config, cloned);
292    }
293
294    #[test]
295    fn it_should_implement_debug_trait_when_formatted() {
296        let config = create_lxd_config();
297        let debug = format!("{config:?}");
298        assert!(debug.contains("Lxd"));
299        assert!(debug.contains("profile_name"));
300    }
301}