torrust_tracker_deployer_lib/application/command_handlers/create/handler.rs
1//! Create Command Implementation
2//!
3//! This module implements the `CreateCommandHandler` that orchestrates environment
4//! creation business logic. It follows the Command Pattern with dependency
5//! injection and is delivery-agnostic.
6
7use std::convert::TryInto;
8use std::sync::Arc;
9use tracing::{info, instrument};
10
11use crate::application::command_handlers::create::config::EnvironmentCreationConfig;
12use crate::domain::environment::repository::EnvironmentRepository;
13use crate::domain::environment::{Created, Environment, EnvironmentParams};
14use crate::shared::Clock;
15
16use super::errors::CreateCommandHandlerError;
17
18/// Command to create a new deployment environment
19///
20/// This command is delivery-agnostic and can be used from CLI, REST API,
21/// GraphQL, or any other delivery mechanism. It orchestrates the business
22/// logic for environment creation without knowledge of how the configuration
23/// was obtained.
24///
25/// # Architecture
26///
27/// The command follows these design principles:
28///
29/// - **Synchronous**: No async/await, following existing patterns
30/// - **Dependency Injection**: Uses `Arc<dyn Trait>` for testability
31/// - **Repository Pattern**: Delegates persistence to repository
32/// - **Explicit Errors**: All failures return structured errors with `.help()`
33///
34/// # Business Logic Flow
35///
36/// 1. Convert configuration to domain objects
37/// 2. Check if environment already exists (prevent duplicates)
38/// 3. Create environment entity using `Environment::new()`
39/// 4. Persist via repository (repository handles directory creation)
40///
41/// # Examples
42///
43/// ```rust,no_run
44/// use std::sync::Arc;
45/// use torrust_tracker_deployer_lib::application::command_handlers::create::CreateCommandHandler;
46/// use torrust_tracker_deployer_lib::application::command_handlers::create::config::{
47/// EnvironmentCreationConfig, EnvironmentSection, LxdProviderSection, ProviderSection,
48/// SshCredentialsConfig,
49/// };
50/// use torrust_tracker_deployer_lib::application::command_handlers::create::config::tracker::TrackerSection;
51/// use torrust_tracker_deployer_lib::infrastructure::persistence::file_repository_factory::FileRepositoryFactory;
52/// use torrust_tracker_deployer_lib::shared::{SystemClock, Clock};
53///
54/// // Setup dependencies
55/// let file_repository_factory = FileRepositoryFactory::new(std::time::Duration::from_secs(30));
56/// let repository = file_repository_factory.create(std::path::PathBuf::from("."));
57/// let clock: Arc<dyn Clock> = Arc::new(SystemClock);
58///
59/// // Create command
60/// let command = CreateCommandHandler::new(repository, clock);
61///
62/// // Prepare configuration
63/// let config = EnvironmentCreationConfig::new(
64/// EnvironmentSection {
65/// name: "dev".to_string(),
66/// description: None,
67/// instance_name: None, // Auto-generate from environment name
68/// },
69/// SshCredentialsConfig::new(
70/// "fixtures/testing_rsa".to_string(),
71/// "fixtures/testing_rsa.pub".to_string(),
72/// "torrust".to_string(),
73/// 22,
74/// ),
75/// ProviderSection::Lxd(LxdProviderSection {
76/// profile_name: "lxd-dev".to_string(),
77/// }),
78/// TrackerSection::default(),
79/// None, // prometheus
80/// None, // grafana
81/// None, // https
82/// None, // backup
83/// );
84///
85/// // Execute command with working directory
86/// let working_dir = std::path::Path::new(".");
87/// let environment = command.execute(config, working_dir)?;
88/// println!("Created environment: {}", environment.name());
89/// # Ok::<(), Box<dyn std::error::Error>>(())
90/// ```
91pub struct CreateCommandHandler {
92 /// Repository for persisting environment state
93 pub(crate) environment_repository: Arc<dyn EnvironmentRepository>,
94
95 /// Clock for timestamp generation (injected for testability)
96 pub(crate) clock: Arc<dyn Clock>,
97}
98
99impl CreateCommandHandler {
100 /// Create a new `CreateCommandHandler` with required dependencies
101 ///
102 /// # Arguments
103 ///
104 /// * `environment_repository` - Repository for persisting environment state
105 /// * `clock` - Clock for timestamp generation (for future use)
106 ///
107 /// # Examples
108 ///
109 /// ```rust,no_run
110 /// use std::sync::Arc;
111 /// use torrust_tracker_deployer_lib::application::command_handlers::create::CreateCommandHandler;
112 /// use torrust_tracker_deployer_lib::infrastructure::persistence::file_repository_factory::FileRepositoryFactory;
113 /// use torrust_tracker_deployer_lib::shared::{SystemClock, Clock};
114 ///
115 /// let file_repository_factory = FileRepositoryFactory::new(std::time::Duration::from_secs(30));
116 /// let repository = file_repository_factory.create(std::path::PathBuf::from("."));
117 /// let clock: Arc<dyn Clock> = Arc::new(SystemClock);
118 ///
119 /// let command = CreateCommandHandler::new(repository, clock);
120 /// ```
121 #[must_use]
122 pub fn new(
123 environment_repository: Arc<dyn EnvironmentRepository>,
124 clock: Arc<dyn Clock>,
125 ) -> Self {
126 Self {
127 environment_repository,
128 clock,
129 }
130 }
131
132 /// Execute the create command with validated configuration
133 ///
134 /// This method orchestrates the complete environment creation workflow:
135 /// 1. Converts configuration to domain objects
136 /// 2. Validates environment uniqueness
137 /// 3. Creates the environment entity
138 /// 4. Persists the environment state
139 ///
140 /// # Arguments
141 ///
142 /// * `config` - Validated environment configuration from domain layer
143 ///
144 /// # Returns
145 ///
146 /// * `Ok(Environment<Created>)` - Successfully created environment
147 /// * `Err(CreateCommandHandlerError)` - Business logic or persistence failure
148 ///
149 /// # Business Rules
150 ///
151 /// 1. Configuration must convert to valid domain objects
152 /// 2. Environment name must be unique (no duplicates)
153 /// 3. Repository handles directory creation atomically during save
154 /// 4. Environment state must be persisted successfully
155 ///
156 /// # Errors
157 ///
158 /// Returns an error if:
159 /// - Configuration validation fails
160 /// - Environment with the same name already exists
161 /// - Repository persistence fails
162 ///
163 /// All errors implement `.help()` with detailed troubleshooting guidance.
164 ///
165 /// # Panics
166 ///
167 /// This function does not panic in practice. The internal `.expect()` call
168 /// when generating the profile name is theoretically unreachable because
169 /// valid environment names always produce valid profile names.
170 ///
171 /// # Examples
172 ///
173 /// ```rust,no_run
174 /// use torrust_tracker_deployer_lib::application::command_handlers::create::CreateCommandHandler;
175 /// use torrust_tracker_deployer_lib::application::command_handlers::create::config::{
176 /// EnvironmentCreationConfig, EnvironmentSection, LxdProviderSection, ProviderSection,
177 /// SshCredentialsConfig,
178 /// };
179 /// use torrust_tracker_deployer_lib::application::command_handlers::create::config::tracker::TrackerSection;
180 ///
181 /// # fn example(command: CreateCommandHandler) -> Result<(), Box<dyn std::error::Error>> {
182 /// let config = EnvironmentCreationConfig::new(
183 /// EnvironmentSection {
184 /// name: "staging".to_string(),
185 /// description: None,
186 /// instance_name: None, // Auto-generate from environment name
187 /// },
188 /// SshCredentialsConfig::new(
189 /// "keys/stage_key".to_string(),
190 /// "keys/stage_key.pub".to_string(),
191 /// "torrust".to_string(),
192 /// 22,
193 /// ),
194 /// ProviderSection::Lxd(LxdProviderSection {
195 /// profile_name: "lxd-staging".to_string(),
196 /// }),
197 /// TrackerSection::default(),
198 /// None, // prometheus
199 /// None, // grafana
200 /// None, // https
201 /// None, // backup
202 /// );
203 ///
204 /// let working_dir = std::path::Path::new(".");
205 /// let environment = command.execute(config, working_dir)?;
206 /// println!("Created: {}", environment.name());
207 /// # Ok(())
208 /// # }
209 /// ```
210 #[instrument(
211 name = "create_command",
212 skip_all,
213 fields(
214 command_type = "create",
215 environment = %config.environment.name
216 )
217 )]
218 pub fn execute(
219 &self,
220 config: EnvironmentCreationConfig,
221 working_dir: &std::path::Path,
222 ) -> Result<Environment<Created>, CreateCommandHandlerError> {
223 // Convert DTO to validated domain parameters
224 let params: EnvironmentParams = config
225 .try_into()
226 .map_err(CreateCommandHandlerError::InvalidConfiguration)?;
227
228 // Check for duplicate environment
229 if self
230 .environment_repository
231 .exists(¶ms.environment_name)
232 .map_err(|e| CreateCommandHandlerError::RepositoryError(e.into()))?
233 {
234 return Err(CreateCommandHandlerError::EnvironmentAlreadyExists {
235 name: params.environment_name.as_str().to_string(),
236 });
237 }
238
239 // Create environment aggregate from validated params
240 let environment = Environment::create(params, working_dir, self.clock.now())
241 .map_err(|e| CreateCommandHandlerError::InvalidConfiguration(e.into()))?;
242
243 self.environment_repository
244 .save(&environment.clone().into_any())
245 .map_err(|e| CreateCommandHandlerError::RepositoryError(e.into()))?;
246
247 info!(
248 command = "create",
249 environment = %environment.name(),
250 "Environment created successfully"
251 );
252
253 Ok(environment)
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn it_should_create_create_command_with_dependencies() {
263 use crate::infrastructure::persistence::file_repository_factory::FileRepositoryFactory;
264 use crate::shared::SystemClock;
265 use tempfile::TempDir;
266
267 let temp_dir = TempDir::new().unwrap();
268 let file_repository_factory =
269 FileRepositoryFactory::new(std::time::Duration::from_secs(30));
270 let repository = file_repository_factory.create(temp_dir.path().to_path_buf());
271 let clock: Arc<dyn Clock> = Arc::new(SystemClock);
272
273 let command = CreateCommandHandler::new(repository, clock);
274
275 // Verify the command was created (basic structure test)
276 assert_eq!(Arc::strong_count(&command.environment_repository), 1);
277 assert_eq!(Arc::strong_count(&command.clock), 1);
278 }
279}