torrust_tracker_deployer_lib/presentation/cli/controllers/exists/handler.rs
1//! Exists Command Handler
2//!
3//! This module handles the exists command execution at the presentation layer,
4//! checking whether an environment exists and outputting a boolean result.
5
6use std::cell::RefCell;
7use std::sync::Arc;
8
9use parking_lot::ReentrantMutex;
10
11use crate::application::command_handlers::exists::{
12 ExistsCommandHandler, ExistsCommandHandlerError,
13};
14use crate::domain::environment::name::EnvironmentName;
15use crate::domain::environment::repository::EnvironmentRepository;
16use crate::presentation::cli::input::cli::OutputFormat;
17use crate::presentation::cli::views::commands::exists::{ExistsResult, JsonView, TextView};
18use crate::presentation::cli::views::Render;
19use crate::presentation::cli::views::UserOutput;
20
21use super::errors::ExistsSubcommandError;
22
23/// Presentation layer controller for exists command workflow
24///
25/// Checks whether an environment exists and outputs a boolean result.
26/// This is a read-only command that checks local data only.
27///
28/// ## Responsibilities
29///
30/// - Validate environment name format
31/// - Delegate to application layer for existence check
32/// - Output `true` or `false` to stdout
33///
34/// ## Output Contract
35///
36/// - **stdout**: `true` or `false` (bare value, valid JSON)
37/// - **exit code 0**: Command completed successfully (result is on stdout)
38/// - **exit code 1**: An error occurred (e.g., repository failure)
39///
40/// ## Architecture
41///
42/// This controller intentionally does NOT use `ProgressReporter` because
43/// the exists check is a sub-millisecond operation. Progress reporting
44/// would add noise without value.
45pub struct ExistsCommandController {
46 handler: ExistsCommandHandler,
47 user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
48}
49
50impl ExistsCommandController {
51 /// Create a new `ExistsCommandController` with dependencies
52 ///
53 /// # Arguments
54 ///
55 /// * `repository` - Environment repository for checking existence
56 /// * `user_output` - Shared output service for result display
57 #[allow(clippy::needless_pass_by_value)] // Arc parameters are moved to constructor for ownership
58 pub fn new(
59 repository: Arc<dyn EnvironmentRepository + Send + Sync>,
60 user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
61 ) -> Self {
62 let handler = ExistsCommandHandler::new(repository);
63
64 Self {
65 handler,
66 user_output,
67 }
68 }
69
70 /// Execute the exists command workflow
71 ///
72 /// This method orchestrates a simple workflow:
73 /// 1. Validate environment name
74 /// 2. Check existence via application layer
75 /// 3. Output boolean result to stdout
76 ///
77 /// # Arguments
78 ///
79 /// * `environment_name` - Name of the environment to check
80 /// * `output_format` - Output format (Text or Json)
81 ///
82 /// # Errors
83 ///
84 /// Returns `ExistsSubcommandError` if any step fails
85 pub fn execute(
86 &self,
87 environment_name: &str,
88 output_format: OutputFormat,
89 ) -> Result<(), ExistsSubcommandError> {
90 // Step 1: Validate environment name
91 let env_name = Self::validate_environment_name(environment_name)?;
92
93 // Step 2: Check existence via application layer
94 let result = self
95 .handler
96 .execute(&env_name)
97 .map_err(|e| Self::map_handler_error(e, &env_name))?;
98
99 // Step 3: Output boolean result
100 self.display_result(&result, output_format)?;
101
102 Ok(())
103 }
104
105 /// Step 1: Validate environment name format
106 fn validate_environment_name(name: &str) -> Result<EnvironmentName, ExistsSubcommandError> {
107 EnvironmentName::new(name.to_string()).map_err(|source| {
108 ExistsSubcommandError::InvalidEnvironmentName {
109 name: name.to_string(),
110 source,
111 }
112 })
113 }
114
115 /// Map application layer errors to presentation errors
116 fn map_handler_error(
117 error: ExistsCommandHandlerError,
118 env_name: &EnvironmentName,
119 ) -> ExistsSubcommandError {
120 match error {
121 ExistsCommandHandlerError::RepositoryError(e) => {
122 ExistsSubcommandError::ExistenceCheckFailed {
123 name: env_name.to_string(),
124 message: e.to_string(),
125 }
126 }
127 }
128 }
129
130 /// Step 3: Display boolean result
131 ///
132 /// Outputs `true` or `false` to stdout. The output is the same
133 /// for both Text and Json formats since bare `true`/`false` are
134 /// valid JSON values.
135 fn display_result(
136 &self,
137 result: &ExistsResult,
138 output_format: OutputFormat,
139 ) -> Result<(), ExistsSubcommandError> {
140 let output = match output_format {
141 OutputFormat::Text => TextView::render(result)?,
142 OutputFormat::Json => JsonView::render(result)?,
143 };
144
145 // Write result to stdout via UserOutput
146 self.user_output.lock().borrow_mut().result(&output);
147
148 Ok(())
149 }
150}