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}