Skip to main content

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}