Skip to main content

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

1//! Validate Command Handler
2//!
3//! This module handles the validate command execution at the presentation layer,
4//! including file validation and user feedback.
5
6use std::cell::RefCell;
7use std::path::Path;
8use std::sync::Arc;
9
10use parking_lot::ReentrantMutex;
11
12use crate::application::command_handlers::validate::{ValidateCommandHandler, ValidationResult};
13use crate::presentation::cli::input::cli::OutputFormat;
14use crate::presentation::cli::views::commands::validate::{
15    JsonView, TextView, ValidateDetailsData,
16};
17use crate::presentation::cli::views::progress::ProgressReporter;
18use crate::presentation::cli::views::Render;
19use crate::presentation::cli::views::UserOutput;
20
21use super::errors::ValidateSubcommandError;
22
23/// Steps in the validate workflow
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25enum ValidateStep {
26    LoadConfiguration,
27    ValidateSchema,
28    ValidateFields,
29}
30
31impl ValidateStep {
32    /// All steps in execution order
33    const ALL: &'static [Self] = &[
34        Self::LoadConfiguration,
35        Self::ValidateSchema,
36        Self::ValidateFields,
37    ];
38
39    /// Total number of steps
40    const fn count() -> usize {
41        Self::ALL.len()
42    }
43
44    /// User-facing description for the step
45    fn description(self) -> &'static str {
46        match self {
47            Self::LoadConfiguration => "Loading configuration file",
48            Self::ValidateSchema => "Validating JSON schema",
49            Self::ValidateFields => "Validating configuration fields",
50        }
51    }
52}
53
54/// Presentation layer controller for validate command workflow
55///
56/// Coordinates user interaction, progress reporting, and input validation
57/// for validating environment configuration files without deployment.
58///
59/// # Responsibilities
60///
61/// - Validate file path exists and is readable
62/// - Show progress updates to the user
63/// - Format validation results for display
64/// - Delegate validation logic to application layer
65///
66/// # Architecture
67///
68/// This controller sits in the presentation layer and handles all user-facing
69/// concerns. Business logic is delegated to the application layer's
70/// `ValidateCommandHandler`.
71pub struct ValidateCommandController {
72    progress: ProgressReporter,
73    handler: ValidateCommandHandler,
74}
75
76impl ValidateCommandController {
77    /// Create a new validate command controller
78    ///
79    /// Creates a `ValidateCommandController` with user output.
80    /// This follows the single container architecture pattern.
81    #[allow(clippy::needless_pass_by_value)] // Constructor takes ownership of Arc parameters
82    pub fn new(user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>) -> Self {
83        Self {
84            progress: ProgressReporter::new(user_output, ValidateStep::count()),
85            handler: ValidateCommandHandler::new(),
86        }
87    }
88
89    /// Execute the validate command workflow
90    ///
91    /// Main entry point for the validate command. Orchestrates the validation
92    /// workflow: loads configuration, validates schema and fields.
93    ///
94    /// # Arguments
95    ///
96    /// * `env_file` - Path to the environment configuration file
97    ///
98    /// # Returns
99    ///
100    /// Returns `Ok(())` on successful validation, or a `ValidateSubcommandError`
101    /// if validation fails.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if:
106    /// - File path does not exist
107    /// - File is not readable
108    /// - Configuration validation fails
109    pub fn execute(
110        &mut self,
111        env_file: &Path,
112        output_format: OutputFormat,
113    ) -> Result<(), ValidateSubcommandError> {
114        // Step 1: Load Configuration (file existence check)
115        self.progress
116            .start_step(ValidateStep::LoadConfiguration.description())?;
117        Self::validate_file_exists(env_file)?;
118        self.progress
119            .complete_step(Some("Configuration file loaded"))?;
120
121        // Step 2: Validate Schema (JSON parsing)
122        self.progress
123            .start_step(ValidateStep::ValidateSchema.description())?;
124
125        // Delegate actual validation to application layer
126        let result = self.handler.validate(env_file).map_err(|source| {
127            ValidateSubcommandError::ValidationFailed {
128                path: env_file.to_path_buf(),
129                source,
130            }
131        })?;
132
133        self.progress
134            .complete_step(Some("Schema validation passed"))?;
135
136        // Step 3: Validate Fields (domain rules)
137        self.progress
138            .start_step(ValidateStep::ValidateFields.description())?;
139        self.progress
140            .complete_step(Some("Field validation passed"))?;
141
142        // Complete workflow with detailed results
143        self.complete_workflow(env_file, &result, output_format)?;
144
145        Ok(())
146    }
147
148    /// Validate that the configuration file exists and is readable
149    fn validate_file_exists(env_file: &Path) -> Result<(), ValidateSubcommandError> {
150        if !env_file.exists() {
151            return Err(ValidateSubcommandError::ConfigFileNotFound {
152                path: env_file.to_path_buf(),
153            });
154        }
155
156        if !env_file.is_file() {
157            return Err(ValidateSubcommandError::ConfigPathNotFile {
158                path: env_file.to_path_buf(),
159            });
160        }
161
162        Ok(())
163    }
164
165    /// Complete the workflow with validation details output
166    ///
167    /// Renders the validation details using the chosen output format
168    /// (text or JSON) and displays them to the user.
169    fn complete_workflow(
170        &mut self,
171        env_file: &Path,
172        result: &ValidationResult,
173        output_format: OutputFormat,
174    ) -> Result<(), ValidateSubcommandError> {
175        let data = ValidateDetailsData::from_result(env_file, result);
176
177        match output_format {
178            OutputFormat::Text => {
179                self.progress.blank_line()?;
180                self.progress.complete(&TextView::render(&data)?)?;
181            }
182            OutputFormat::Json => {
183                self.progress.result(&JsonView::render(&data)?)?;
184            }
185        }
186
187        Ok(())
188    }
189}