Skip to main content

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

1//! Render Command Controller
2//!
3//! This module handles the render command execution at the presentation layer,
4//! including input validation, mode selection, and user feedback.
5
6use std::cell::RefCell;
7use std::net::Ipv4Addr;
8use std::path::Path;
9use std::sync::Arc;
10
11use parking_lot::ReentrantMutex;
12
13use crate::application::command_handlers::render::{
14    RenderCommandHandler, RenderInputMode, RenderResult,
15};
16use crate::domain::environment::repository::EnvironmentRepository;
17use crate::domain::EnvironmentName;
18use crate::presentation::cli::input::cli::OutputFormat;
19use crate::presentation::cli::views::commands::render::{JsonView, RenderDetailsData, TextView};
20use crate::presentation::cli::views::progress::ProgressReporter;
21use crate::presentation::cli::views::Render;
22use crate::presentation::cli::views::UserOutput;
23
24use super::errors::RenderCommandError;
25
26/// Steps in the render workflow
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28enum RenderStep {
29    ValidateInput,
30    LoadConfiguration,
31    GenerateArtifacts,
32}
33
34impl RenderStep {
35    /// All steps in execution order
36    const ALL: &'static [Self] = &[
37        Self::ValidateInput,
38        Self::LoadConfiguration,
39        Self::GenerateArtifacts,
40    ];
41
42    /// Total number of steps
43    const fn count() -> usize {
44        Self::ALL.len()
45    }
46
47    /// User-facing description for the step
48    fn description(self) -> &'static str {
49        match self {
50            Self::ValidateInput => "Validating input parameters",
51            Self::LoadConfiguration => "Loading configuration",
52            Self::GenerateArtifacts => "Generating deployment artifacts",
53        }
54    }
55}
56
57/// Presentation layer controller for render command workflow
58///
59/// Coordinates user interaction, progress reporting, and input validation
60/// for generating deployment artifacts without executing deployment.
61///
62/// # Responsibilities
63///
64/// - Validate input mode (env-name OR env-file)
65/// - Parse and validate IP address
66/// - Show progress updates to the user
67/// - Format output for display
68/// - Delegate artifact generation to application layer handler
69///
70/// # Architecture
71///
72/// This controller sits in the presentation layer and handles all user-facing
73/// concerns. Business logic is delegated to the application layer's
74/// `RenderCommandHandler`.
75pub struct RenderCommandController {
76    handler: RenderCommandHandler,
77    progress: ProgressReporter,
78}
79
80impl RenderCommandController {
81    /// Create a new render command controller
82    ///
83    /// Creates a `RenderCommandController` with repository and user output.
84    /// This follows the single container architecture pattern.
85    #[allow(clippy::needless_pass_by_value)] // Constructor takes ownership of Arc parameters
86    pub fn new(
87        repository: Arc<dyn EnvironmentRepository>,
88        user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
89    ) -> Self {
90        Self {
91            handler: RenderCommandHandler::new(repository),
92            progress: ProgressReporter::new(user_output, RenderStep::count()),
93        }
94    }
95
96    /// Execute the render command workflow
97    ///
98    /// This performs input validation and delegates to the application handler.
99    ///
100    /// # Arguments
101    ///
102    /// * `env_name` - Optional environment name (mutually exclusive with `env_file`)
103    /// * `env_file` - Optional config file path (mutually exclusive with `env_name`)
104    /// * `ip` - Target instance IP address (required)
105    /// * `output_dir` - Output directory for generated artifacts (required)
106    /// * `force` - Whether to overwrite existing output directory
107    /// * `working_dir` - Working directory for environment data (from --working-dir global arg)
108    /// * `output_format` - Output format (text or JSON)
109    ///
110    /// # Returns
111    ///
112    /// * `Ok(())` - Artifact generation succeeded
113    /// * `Err(RenderCommandError)` - Validation or generation failed
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if:
118    /// - Neither `env_name` nor `env_file` is provided
119    /// - IP address is invalid
120    /// - Output directory exists and force is false
121    /// - Config file doesn't exist
122    /// - Environment not found
123    /// - Template rendering fails
124    #[allow(clippy::too_many_arguments)] // Required parameters for render workflow - all are necessary
125    pub async fn execute(
126        &mut self,
127        env_name: Option<&str>,
128        env_file: Option<&Path>,
129        ip: &str,
130        output_dir: &Path,
131        force: bool,
132        working_dir: &Path,
133        output_format: OutputFormat,
134    ) -> Result<(), RenderCommandError> {
135        // Step 1: Validate input
136        self.progress
137            .start_step(RenderStep::ValidateInput.description())?;
138
139        // Validate IP address first (fail fast)
140        let _target_ip: Ipv4Addr = ip
141            .parse()
142            .map_err(|_| RenderCommandError::InvalidIpAddress { ip: ip.to_string() })?;
143
144        // Determine input mode and prepare handler parameters
145        let input_mode = match (env_name, env_file) {
146            (Some(name), None) => {
147                let env_name = EnvironmentName::new(name).map_err(|e| {
148                    RenderCommandError::InvalidEnvironmentName {
149                        value: name.to_string(),
150                        reason: e.to_string(),
151                    }
152                })?;
153                RenderInputMode::EnvironmentName(env_name)
154            }
155            (None, Some(path)) => {
156                // Validate file exists
157                if !path.exists() {
158                    return Err(RenderCommandError::ConfigFileNotFound {
159                        path: path.to_path_buf(),
160                    });
161                }
162                RenderInputMode::ConfigFile(path.to_path_buf())
163            }
164            (None, None) => return Err(RenderCommandError::NoInputMode),
165            (Some(_), Some(_)) => unreachable!("Clap ensures mutual exclusivity"),
166        };
167
168        self.progress.complete_step(None)?;
169
170        // Step 2: Load configuration and validate
171        self.progress
172            .start_step(RenderStep::LoadConfiguration.description())?;
173
174        // working_dir is now passed as a parameter from the router
175        // which gets it from context.working_dir() (the --working-dir global argument)
176
177        self.progress.complete_step(None)?;
178
179        // Step 3: Generate artifacts
180        self.progress
181            .start_step(RenderStep::GenerateArtifacts.description())?;
182
183        // Call application handler
184        let result = self
185            .handler
186            .execute(input_mode, ip, output_dir, force, working_dir)
187            .await
188            .map_err(RenderCommandError::from)?;
189
190        self.progress.complete_step(None)?;
191
192        // Render and display results
193        self.complete_workflow(&result, output_format)?;
194
195        Ok(())
196    }
197
198    /// Complete the workflow with render details output
199    ///
200    /// Renders the artifact generation summary using the chosen output format
201    /// (text or JSON) and displays it to the user.
202    fn complete_workflow(
203        &mut self,
204        result: &RenderResult,
205        output_format: OutputFormat,
206    ) -> Result<(), RenderCommandError> {
207        let data = RenderDetailsData::from_result(result);
208
209        match output_format {
210            OutputFormat::Text => {
211                self.progress.blank_line()?;
212                self.progress.complete(&TextView::render(&data)?)?;
213            }
214            OutputFormat::Json => {
215                self.progress.result(&JsonView::render(&data)?)?;
216            }
217        }
218
219        Ok(())
220    }
221}