Skip to main content

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

1//! Register Command Handler
2//!
3//! This module handles the register command execution at the presentation layer,
4//! including environment validation, IP parsing, and user interaction.
5
6use std::cell::RefCell;
7use std::net::IpAddr;
8use std::sync::Arc;
9
10use parking_lot::ReentrantMutex;
11
12use crate::application::command_handlers::RegisterCommandHandler;
13use crate::domain::environment::name::EnvironmentName;
14use crate::domain::environment::repository::EnvironmentRepository;
15use crate::domain::environment::state::Provisioned;
16use crate::domain::environment::Environment;
17use crate::presentation::cli::input::cli::OutputFormat;
18use crate::presentation::cli::views::commands::register::{
19    JsonView, RegisterDetailsData, TextView,
20};
21use crate::presentation::cli::views::progress::ProgressReporter;
22use crate::presentation::cli::views::Render;
23use crate::presentation::cli::views::UserOutput;
24use crate::shared::clock::Clock;
25
26use super::errors::RegisterSubcommandError;
27
28/// Steps in the register workflow
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum RegisterStep {
31    ValidateInput,
32    CreateCommandHandler,
33    RegisterInstance,
34}
35
36impl RegisterStep {
37    /// All steps in execution order
38    const ALL: &'static [Self] = &[
39        Self::ValidateInput,
40        Self::CreateCommandHandler,
41        Self::RegisterInstance,
42    ];
43
44    /// Total number of steps
45    const fn count() -> usize {
46        Self::ALL.len()
47    }
48
49    /// User-facing description for the step
50    fn description(self) -> &'static str {
51        match self {
52            Self::ValidateInput => "Validating input",
53            Self::CreateCommandHandler => "Creating command handler",
54            Self::RegisterInstance => "Registering instance",
55        }
56    }
57}
58
59/// Presentation layer controller for register command workflow
60///
61/// Coordinates user interaction, progress reporting, and input validation
62/// before delegating to the application layer `RegisterCommandHandler`.
63///
64/// # Responsibilities
65///
66/// - Validate user input (environment name format, IP address format)
67/// - Show progress updates to the user
68/// - Format success/error messages for display
69/// - Delegate business logic to application layer
70///
71/// # Architecture
72///
73/// This controller sits in the presentation layer and handles all user-facing
74/// concerns. It delegates actual business logic to the application layer's
75/// `RegisterCommandHandler`, maintaining clear separation of concerns.
76pub struct RegisterCommandController {
77    repository: Arc<dyn EnvironmentRepository + Send + Sync>,
78    clock: Arc<dyn Clock>,
79    progress: ProgressReporter,
80}
81
82impl RegisterCommandController {
83    /// Create a new register command controller
84    #[allow(clippy::needless_pass_by_value)] // Constructor takes ownership of Arc parameters
85    pub fn new(
86        repository: Arc<dyn EnvironmentRepository + Send + Sync>,
87        clock: Arc<dyn Clock>,
88        user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
89    ) -> Self {
90        let progress = ProgressReporter::new(user_output, RegisterStep::count());
91
92        Self {
93            repository,
94            clock,
95            progress,
96        }
97    }
98
99    /// Execute the complete register workflow
100    ///
101    /// Orchestrates all steps of the register command:
102    /// 1. Validate environment name
103    /// 2. Parse and validate IP address
104    /// 3. Create command handler
105    /// 4. Register the instance
106    /// 5. Complete with success message
107    ///
108    /// # Arguments
109    ///
110    /// * `environment_name` - The name of the environment to register the instance with
111    /// * `instance_ip_str` - The IP address string of the existing instance
112    /// * `ssh_port` - Optional SSH port (overrides environment config if provided)
113    /// * `output_format` - Output format (text or JSON)
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if:
118    /// - Environment name is invalid
119    /// - IP address format is invalid
120    /// - Environment is not found or not in Created state
121    /// - SSH connectivity validation fails
122    #[allow(clippy::result_large_err)]
123    pub async fn execute(
124        &mut self,
125        environment_name: &str,
126        instance_ip_str: &str,
127        ssh_port: Option<u16>,
128        output_format: OutputFormat,
129    ) -> Result<Environment<Provisioned>, RegisterSubcommandError> {
130        let (env_name, instance_ip) = self.validate_input(environment_name, instance_ip_str)?;
131
132        let handler = self.create_command_handler()?;
133
134        let provisioned = self
135            .register_instance(&handler, &env_name, instance_ip, ssh_port)
136            .await?;
137
138        self.complete_workflow(&provisioned, output_format)?;
139
140        Ok(provisioned)
141    }
142
143    /// Validate input: environment name and IP address
144    #[allow(clippy::result_large_err)]
145    fn validate_input(
146        &mut self,
147        name: &str,
148        ip_str: &str,
149    ) -> Result<(EnvironmentName, IpAddr), RegisterSubcommandError> {
150        self.progress
151            .start_step(RegisterStep::ValidateInput.description())?;
152
153        let env_name = EnvironmentName::new(name.to_string()).map_err(|source| {
154            RegisterSubcommandError::InvalidEnvironmentName {
155                name: name.to_string(),
156                source,
157            }
158        })?;
159
160        let instance_ip: IpAddr = ip_str.parse().map_err(|e: std::net::AddrParseError| {
161            RegisterSubcommandError::InvalidIpAddress {
162                value: ip_str.to_string(),
163                reason: e.to_string(),
164            }
165        })?;
166
167        self.progress.complete_step(None)?;
168
169        Ok((env_name, instance_ip))
170    }
171
172    /// Create the application layer command handler
173    #[allow(clippy::result_large_err)]
174    fn create_command_handler(
175        &mut self,
176    ) -> Result<RegisterCommandHandler, RegisterSubcommandError> {
177        self.progress
178            .start_step(RegisterStep::CreateCommandHandler.description())?;
179
180        let handler = RegisterCommandHandler::new(
181            self.clock.clone(),
182            Arc::clone(&self.repository) as Arc<dyn EnvironmentRepository>,
183        );
184
185        self.progress.complete_step(None)?;
186
187        Ok(handler)
188    }
189
190    /// Register the instance using the command handler
191    #[allow(clippy::result_large_err)]
192    async fn register_instance(
193        &mut self,
194        handler: &RegisterCommandHandler,
195        env_name: &EnvironmentName,
196        instance_ip: IpAddr,
197        ssh_port: Option<u16>,
198    ) -> Result<Environment<Provisioned>, RegisterSubcommandError> {
199        self.progress
200            .start_step(RegisterStep::RegisterInstance.description())?;
201
202        let provisioned = handler
203            .execute(env_name, instance_ip, ssh_port)
204            .await
205            .map_err(|source| RegisterSubcommandError::RegisterOperationFailed {
206                name: env_name.to_string(),
207                source: Box::new(source),
208            })?;
209
210        self.progress.complete_step(None)?;
211
212        Ok(provisioned)
213    }
214
215    /// Complete the workflow with success message
216    ///
217    /// Dispatches to `TextView` or `JsonView` based on `output_format`.
218    #[allow(clippy::result_large_err)]
219    fn complete_workflow(
220        &mut self,
221        provisioned: &Environment<Provisioned>,
222        output_format: OutputFormat,
223    ) -> Result<(), RegisterSubcommandError> {
224        let data = RegisterDetailsData::from_environment(provisioned);
225        match output_format {
226            OutputFormat::Text => {
227                self.progress.complete(&TextView::render(&data)?)?;
228            }
229            OutputFormat::Json => {
230                self.progress.result(&JsonView::render(&data)?)?;
231            }
232        }
233        Ok(())
234    }
235}