Skip to main content

torrust_tracker_deployer_lib/presentation/cli/controllers/purge/
handler.rs

1//! Purge Command Handler
2//!
3//! This module handles the purge command execution at the presentation layer,
4//! including environment validation, confirmation prompts, and user interaction.
5
6use std::cell::RefCell;
7use std::sync::Arc;
8
9use parking_lot::ReentrantMutex;
10
11use crate::application::command_handlers::purge::handler::PurgeCommandHandler;
12use crate::domain::environment::name::EnvironmentName;
13use crate::presentation::cli::input::cli::OutputFormat;
14use crate::presentation::cli::views::commands::purge::{JsonView, PurgeDetailsData, TextView};
15use crate::presentation::cli::views::progress::ProgressReporter;
16use crate::presentation::cli::views::Render;
17use crate::presentation::cli::views::UserOutput;
18
19use super::errors::PurgeSubcommandError;
20
21/// Steps in the purge workflow
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23enum PurgeStep {
24    ValidateEnvironment,
25    ConfirmOperation,
26    PurgeLocalData,
27}
28
29impl PurgeStep {
30    /// All steps in execution order
31    const ALL: &'static [Self] = &[
32        Self::ValidateEnvironment,
33        Self::ConfirmOperation,
34        Self::PurgeLocalData,
35    ];
36
37    /// Total number of steps
38    const fn count() -> usize {
39        Self::ALL.len()
40    }
41
42    /// User-facing description for the step
43    fn description(self) -> &'static str {
44        match self {
45            Self::ValidateEnvironment => "Validating environment",
46            Self::ConfirmOperation => "Confirming operation",
47            Self::PurgeLocalData => "Purging local data",
48        }
49    }
50}
51
52/// Presentation layer controller for purge command workflow
53///
54/// Coordinates user interaction, progress reporting, and input validation
55/// before delegating to the application layer `PurgeCommandHandler`.
56///
57/// # Responsibilities
58///
59/// - Validate user input (environment name format)
60/// - Show progress updates to the user
61/// - Handle confirmation prompts (unless --force is provided)
62/// - Format success/error messages for display
63/// - Delegate business logic to application layer
64///
65/// # Architecture
66///
67/// This controller sits in the presentation layer and handles all user-facing
68/// concerns. It delegates actual business logic to the application layer's
69/// `PurgeCommandHandler`, maintaining clear separation of concerns.
70pub struct PurgeCommandController {
71    handler: PurgeCommandHandler,
72    progress: ProgressReporter,
73}
74
75impl PurgeCommandController {
76    /// Create a new purge command controller
77    ///
78    /// Creates a `PurgeCommandController` with the application handler.
79    /// This follows the single container architecture pattern.
80    #[allow(clippy::needless_pass_by_value)] // Constructor takes ownership of Arc parameters
81    pub fn new(
82        handler: PurgeCommandHandler,
83        user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
84    ) -> Self {
85        let progress = ProgressReporter::new(user_output, PurgeStep::count());
86
87        Self { handler, progress }
88    }
89
90    /// Execute the complete purge workflow
91    ///
92    /// Orchestrates all steps of the purge command:
93    /// 1. Validate environment name
94    /// 2. Confirm operation (unless --force is provided)
95    /// 3. Purge local data
96    /// 4. Complete with success message
97    ///
98    /// # Arguments
99    ///
100    /// * `environment_name` - The name of the environment to purge
101    /// * `force` - Skip confirmation prompt if true
102    /// * `output_format` - Output format (text or JSON)
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if:
107    /// - Environment name is invalid (format validation fails)
108    /// - Environment cannot be loaded from repository
109    /// - User cancels operation at confirmation prompt
110    /// - Purge operation fails
111    /// - Progress reporting encounters a poisoned mutex
112    ///
113    /// # Returns
114    ///
115    /// Returns `Ok(())` on success, or a `PurgeSubcommandError` if any step fails.
116    #[allow(clippy::result_large_err)]
117    #[allow(clippy::unused_async)] // Part of uniform async presentation layer interface
118    pub async fn execute(
119        &mut self,
120        environment_name: &str,
121        force: bool,
122        output_format: OutputFormat,
123    ) -> Result<(), PurgeSubcommandError> {
124        let env_name = self.validate_environment_name(environment_name)?;
125
126        // Handle confirmation unless --force flag provided
127        if !force {
128            self.progress
129                .start_step(PurgeStep::ConfirmOperation.description())?;
130
131            // Show warning and prompt for confirmation
132            self.show_confirmation_prompt(environment_name);
133
134            // Read user response
135            if !Self::read_user_confirmation()? {
136                self.progress.complete_step(None)?;
137                return Err(PurgeSubcommandError::UserCancelled);
138            }
139
140            self.progress.complete_step(None)?;
141        }
142
143        // Execute purge via application handler
144        self.progress
145            .start_step(PurgeStep::PurgeLocalData.description())?;
146        self.handler.execute(&env_name).map_err(|source| {
147            PurgeSubcommandError::PurgeOperationFailed {
148                name: environment_name.to_string(),
149                source,
150            }
151        })?;
152        self.progress.complete_step(None)?;
153
154        self.complete_workflow(environment_name, output_format)?;
155
156        Ok(())
157    }
158
159    /// Validate the environment name format
160    ///
161    /// Shows progress to user and validates that the environment name
162    /// meets domain requirements (1-63 chars, alphanumeric + hyphens).
163    #[allow(clippy::result_large_err)]
164    fn validate_environment_name(
165        &mut self,
166        name: &str,
167    ) -> Result<EnvironmentName, PurgeSubcommandError> {
168        self.progress
169            .start_step(PurgeStep::ValidateEnvironment.description())?;
170
171        let env_name = EnvironmentName::new(name.to_string()).map_err(|source| {
172            PurgeSubcommandError::InvalidEnvironmentName {
173                name: name.to_string(),
174                source,
175            }
176        })?;
177
178        self.progress.complete_step(None)?;
179
180        Ok(env_name)
181    }
182
183    /// Complete the workflow with success message
184    ///
185    /// Shows final success message to the user with workflow summary.
186    /// Dispatches to `TextView` or `JsonView` based on `output_format`.
187    #[allow(clippy::result_large_err)]
188    fn complete_workflow(
189        &mut self,
190        environment_name: &str,
191        output_format: OutputFormat,
192    ) -> Result<(), PurgeSubcommandError> {
193        let data = PurgeDetailsData::from_environment_name(environment_name);
194        match output_format {
195            OutputFormat::Text => {
196                self.progress.complete(&TextView::render(&data)?)?;
197            }
198            OutputFormat::Json => {
199                self.progress.result(&JsonView::render(&data)?)?;
200            }
201        }
202        Ok(())
203    }
204
205    /// Show confirmation prompt with warning message
206    ///
207    /// Displays a warning about the irreversible nature of the purge operation
208    /// and prompts the user to confirm.
209    fn show_confirmation_prompt(&mut self, environment_name: &str) {
210        let warning = format!(
211            "⚠️  WARNING: This will permanently delete all local data for '{environment_name}':\n\
212             • data/{environment_name}/ directory\n\
213             • build/{environment_name}/ directory\n\
214             • Environment registry entry\n\
215             \n\
216             This operation CANNOT be undone!\n"
217        );
218
219        self.progress.output().lock().borrow_mut().warn(&warning);
220
221        self.progress
222            .output()
223            .lock()
224            .borrow_mut()
225            .progress("Are you sure you want to continue? (y/N): ");
226    }
227
228    /// Read user confirmation from stdin
229    ///
230    /// Returns `true` if user confirms (enters 'y' or 'Y'), `false` otherwise.
231    #[allow(clippy::result_large_err)]
232    fn read_user_confirmation() -> Result<bool, PurgeSubcommandError> {
233        use std::io::{self, BufRead};
234
235        let stdin = io::stdin();
236        let mut line = String::new();
237
238        stdin
239            .lock()
240            .read_line(&mut line)
241            .map_err(|source| PurgeSubcommandError::IoError {
242                operation: "reading user confirmation".to_string(),
243                source,
244            })?;
245
246        let response = line.trim().to_lowercase();
247        Ok(response == "y" || response == "yes")
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    // TODO: Add unit tests in Phase 3 when implementing actual purge logic
254    // Tests should cover:
255    // - Valid environment name validation
256    // - Invalid environment name rejection
257    // - Force flag behavior
258    // - Error handling for non-existent environments
259}