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}