torrust_tracker_deployer_lib/presentation/cli/controllers/configure/
handler.rs1use std::cell::RefCell;
7use std::sync::Arc;
8
9use parking_lot::ReentrantMutex;
10
11use crate::application::command_handlers::ConfigureCommandHandler;
12use crate::domain::environment::name::EnvironmentName;
13use crate::domain::environment::repository::EnvironmentRepository;
14use crate::domain::environment::state::Configured;
15use crate::domain::environment::Environment;
16use crate::presentation::cli::input::cli::OutputFormat;
17use crate::presentation::cli::views::commands::configure::{
18 ConfigureDetailsData, JsonView, TextView,
19};
20use crate::presentation::cli::views::progress::ProgressReporter;
21use crate::presentation::cli::views::progress::VerboseProgressListener;
22use crate::presentation::cli::views::Render;
23use crate::presentation::cli::views::UserOutput;
24use crate::shared::clock::Clock;
25
26use super::errors::ConfigureSubcommandError;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum ConfigureStep {
31 ValidateEnvironment,
32 CreateCommandHandler,
33 ConfigureInfrastructure,
34}
35
36impl ConfigureStep {
37 const ALL: &'static [Self] = &[
39 Self::ValidateEnvironment,
40 Self::CreateCommandHandler,
41 Self::ConfigureInfrastructure,
42 ];
43
44 const fn count() -> usize {
46 Self::ALL.len()
47 }
48
49 fn description(self) -> &'static str {
51 match self {
52 Self::ValidateEnvironment => "Validating environment",
53 Self::CreateCommandHandler => "Creating command handler",
54 Self::ConfigureInfrastructure => "Configuring infrastructure",
55 }
56 }
57}
58
59pub struct ConfigureCommandController {
77 repository: Arc<dyn EnvironmentRepository + Send + Sync>,
78 clock: Arc<dyn Clock>,
79 progress: ProgressReporter,
80}
81
82impl ConfigureCommandController {
83 #[allow(clippy::needless_pass_by_value)] pub fn new(
89 repository: Arc<dyn EnvironmentRepository + Send + Sync>,
90 clock: Arc<dyn Clock>,
91 user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
92 ) -> Self {
93 let progress = ProgressReporter::new(user_output, ConfigureStep::count());
94
95 Self {
96 repository,
97 clock,
98 progress,
99 }
100 }
101
102 #[allow(clippy::result_large_err)]
130 pub fn execute(
131 &mut self,
132 environment_name: &str,
133 output_format: OutputFormat,
134 ) -> Result<Environment<Configured>, ConfigureSubcommandError> {
135 let env_name = self.validate_environment_name(environment_name)?;
136
137 let handler = self.create_command_handler()?;
138
139 let configured = self.configure_infrastructure(&handler, &env_name)?;
140
141 self.complete_workflow(environment_name)?;
142
143 self.display_configure_results(&configured, output_format)?;
144
145 Ok(configured)
146 }
147
148 #[allow(clippy::result_large_err)]
153 fn validate_environment_name(
154 &mut self,
155 name: &str,
156 ) -> Result<EnvironmentName, ConfigureSubcommandError> {
157 self.progress
158 .start_step(ConfigureStep::ValidateEnvironment.description())?;
159
160 let env_name = EnvironmentName::new(name.to_string()).map_err(|source| {
161 ConfigureSubcommandError::InvalidEnvironmentName {
162 name: name.to_string(),
163 source,
164 }
165 })?;
166
167 self.progress
168 .complete_step(Some(&format!("Environment name validated: {name}")))?;
169
170 Ok(env_name)
171 }
172
173 #[allow(clippy::result_large_err)]
178 fn create_command_handler(
179 &mut self,
180 ) -> Result<ConfigureCommandHandler, ConfigureSubcommandError> {
181 self.progress
182 .start_step(ConfigureStep::CreateCommandHandler.description())?;
183
184 let handler = ConfigureCommandHandler::new(self.clock.clone(), self.repository.clone());
185 self.progress.complete_step(None)?;
186
187 Ok(handler)
188 }
189
190 #[allow(clippy::result_large_err)]
201 fn configure_infrastructure(
202 &mut self,
203 handler: &ConfigureCommandHandler,
204 env_name: &EnvironmentName,
205 ) -> Result<Environment<Configured>, ConfigureSubcommandError> {
206 self.progress
207 .start_step(ConfigureStep::ConfigureInfrastructure.description())?;
208
209 let listener = VerboseProgressListener::new(self.progress.output().clone());
213
214 let configured = handler
215 .execute(env_name, Some(&listener))
216 .map_err(
217 |source| ConfigureSubcommandError::ConfigureOperationFailed {
218 name: env_name.to_string(),
219 source: Box::new(source),
220 },
221 )?;
222
223 self.progress
224 .complete_step(Some("Infrastructure configured"))?;
225 Ok(configured)
226 }
227
228 #[allow(clippy::result_large_err)]
232 fn complete_workflow(&mut self, name: &str) -> Result<(), ConfigureSubcommandError> {
233 self.progress
234 .complete(&format!("Environment '{name}' configured successfully"))?;
235 Ok(())
236 }
237
238 #[allow(clippy::result_large_err)]
257 fn display_configure_results(
258 &mut self,
259 configured: &Environment<Configured>,
260 output_format: OutputFormat,
261 ) -> Result<(), ConfigureSubcommandError> {
262 self.progress.blank_line()?;
263 let details = ConfigureDetailsData::from(configured);
264 let output = match output_format {
265 OutputFormat::Text => TextView::render(&details)?,
266 OutputFormat::Json => JsonView::render(&details)?,
267 };
268 self.progress.result(&output)?;
269 Ok(())
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::presentation::cli::controllers::constants::DEFAULT_LOCK_TIMEOUT;
277 use crate::presentation::cli::views::testing::TestUserOutput;
278 use crate::presentation::cli::views::VerbosityLevel;
279 use crate::shared::SystemClock;
280
281 #[allow(clippy::type_complexity)] fn create_test_dependencies(
289 temp_dir: &tempfile::TempDir,
290 ) -> (
291 Arc<ReentrantMutex<RefCell<UserOutput>>>,
292 Arc<dyn EnvironmentRepository + Send + Sync>,
293 Arc<dyn Clock>,
294 ) {
295 use crate::infrastructure::persistence::file_repository_factory::FileRepositoryFactory;
296 let (user_output, _, _) =
297 TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped();
298 let data_dir = temp_dir.path().join("data");
299 let file_repository_factory = FileRepositoryFactory::new(DEFAULT_LOCK_TIMEOUT);
300 let repository = file_repository_factory.create(data_dir);
301 let clock = Arc::new(SystemClock);
302
303 (user_output, repository, clock)
304 }
305
306 #[tokio::test]
307 async fn it_should_return_error_for_invalid_environment_name() {
308 use tempfile::TempDir;
309
310 let temp_dir = TempDir::new().unwrap();
311
312 let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
313
314 let result = ConfigureCommandController::new(repository, clock, user_output.clone())
316 .execute("invalid_name", OutputFormat::Text);
317
318 assert!(result.is_err());
319 match result.unwrap_err() {
320 ConfigureSubcommandError::InvalidEnvironmentName { name, .. } => {
321 assert_eq!(name, "invalid_name");
322 }
323 other => panic!("Expected InvalidEnvironmentName, got: {other:?}"),
324 }
325 }
326
327 #[tokio::test]
328 async fn it_should_return_error_for_empty_environment_name() {
329 use tempfile::TempDir;
330
331 let temp_dir = TempDir::new().unwrap();
332
333 let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
334
335 let result = ConfigureCommandController::new(repository, clock, user_output.clone())
336 .execute("", OutputFormat::Text);
337
338 assert!(result.is_err());
339 match result.unwrap_err() {
340 ConfigureSubcommandError::InvalidEnvironmentName { name, .. } => {
341 assert_eq!(name, "");
342 }
343 other => panic!("Expected InvalidEnvironmentName, got: {other:?}"),
344 }
345 }
346
347 #[tokio::test]
348 async fn it_should_return_error_for_nonexistent_environment() {
349 use tempfile::TempDir;
350
351 let temp_dir = TempDir::new().unwrap();
352
353 let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
354
355 let result = ConfigureCommandController::new(repository, clock, user_output.clone())
357 .execute("nonexistent-env", OutputFormat::Text);
358
359 assert!(result.is_err());
360 match result.unwrap_err() {
362 ConfigureSubcommandError::ConfigureOperationFailed { name, .. } => {
363 assert_eq!(name, "nonexistent-env");
364 }
365 other => panic!("Expected ConfigureOperationFailed, got: {other:?}"),
366 }
367 }
368
369 #[tokio::test]
370 async fn it_should_accept_valid_environment_name() {
371 use std::fs;
372 use tempfile::TempDir;
373
374 let temp_dir = TempDir::new().unwrap();
375 let working_dir = temp_dir.path();
376
377 let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
378
379 let env_dir = working_dir.join("test-env");
381 fs::create_dir_all(&env_dir).unwrap();
382
383 let result = ConfigureCommandController::new(repository, clock, user_output.clone())
386 .execute("test-env", OutputFormat::Text);
387
388 if let Err(ConfigureSubcommandError::InvalidEnvironmentName { .. }) = result {
390 panic!("Should not fail at name validation for 'test-env'");
391 }
392 }
394}