Skip to content

Commit 75af108

Browse files
committed
refactor: [#107] extract DestroyCommandController with private step methods
- Create DestroyCommandController struct to encapsulate workflow orchestration - Extract private step methods: validate_environment_name, initialize_dependencies, tear_down_infrastructure, complete_workflow - Add DESTROY_WORKFLOW_STEPS constant for step count - Simplify handle_destroy_command to be a thin wrapper over controller - Improve testability by separating concerns between presentation and application layers - Add comprehensive documentation with # Errors sections for public methods This refactor improves code clarity, maintainability, and testability while maintaining backward compatibility with existing tests.
1 parent 9730bd0 commit 75af108

File tree

1 file changed

+183
-55
lines changed

1 file changed

+183
-55
lines changed

src/presentation/commands/destroy/handler.rs

Lines changed: 183 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,205 @@
33
//! This module handles the destroy command execution at the presentation layer,
44
//! including environment validation, repository initialization, and user interaction.
55
6+
use std::path::PathBuf;
67
use std::sync::{Arc, Mutex};
78

9+
use crate::application::command_handlers::DestroyCommandHandler;
810
use crate::domain::environment::name::EnvironmentName;
11+
use crate::domain::environment::state::Destroyed;
12+
use crate::domain::Environment;
13+
use crate::presentation::commands::context::CommandContext;
914
use crate::presentation::commands::factory::CommandHandlerFactory;
1015
use crate::presentation::progress::ProgressReporter;
1116
use crate::presentation::user_output::UserOutput;
1217

1318
use super::errors::DestroySubcommandError;
1419

15-
/// Handle the destroy command
20+
// ============================================================================
21+
// CONSTANTS
22+
// ============================================================================
23+
24+
/// Number of main steps in the destroy workflow
25+
const DESTROY_WORKFLOW_STEPS: usize = 3;
26+
27+
// ============================================================================
28+
// PRESENTATION LAYER CONTROLLER
29+
// ============================================================================
30+
31+
/// Presentation layer controller for destroy command workflow
32+
///
33+
/// Coordinates user interaction, progress reporting, and input validation
34+
/// before delegating to the application layer `DestroyCommandHandler`.
35+
///
36+
/// # Responsibilities
1637
///
17-
/// This function orchestrates the environment destruction workflow with progress reporting:
18-
/// 1. Validating the environment name
19-
/// 2. Tearing down infrastructure
20-
/// 3. Cleaning up resources
38+
/// - Validate user input (environment name format)
39+
/// - Show progress updates to the user
40+
/// - Format success/error messages for display
41+
/// - Delegate business logic to application layer
42+
///
43+
/// # Architecture
44+
///
45+
/// This controller sits in the presentation layer and handles all user-facing
46+
/// concerns. It delegates actual business logic to the application layer's
47+
/// `DestroyCommandHandler`, maintaining clear separation of concerns.
48+
pub struct DestroyCommandController {
49+
factory: CommandHandlerFactory,
50+
ctx: CommandContext,
51+
progress: ProgressReporter,
52+
}
53+
54+
impl DestroyCommandController {
55+
/// Create a new destroy command controller
56+
///
57+
/// # Arguments
58+
///
59+
/// * `working_dir` - Root directory for environment data storage
60+
/// * `user_output` - Shared user output service for consistent formatting
61+
pub fn new(working_dir: PathBuf, user_output: Arc<Mutex<UserOutput>>) -> Self {
62+
let factory = CommandHandlerFactory::new();
63+
let ctx = factory.create_context(working_dir, user_output.clone());
64+
let progress = ProgressReporter::new(user_output, DESTROY_WORKFLOW_STEPS);
65+
66+
Self {
67+
factory,
68+
ctx,
69+
progress,
70+
}
71+
}
72+
73+
/// Execute the complete destroy workflow
74+
///
75+
/// Orchestrates all steps of the destroy command:
76+
/// 1. Validate environment name
77+
/// 2. Initialize dependencies
78+
/// 3. Tear down infrastructure
79+
/// 4. Complete with success message
80+
///
81+
/// # Arguments
82+
///
83+
/// * `environment_name` - The name of the environment to destroy
84+
///
85+
/// # Errors
86+
///
87+
/// Returns an error if:
88+
/// - Environment name is invalid (format validation fails)
89+
/// - Environment cannot be loaded from repository
90+
/// - Infrastructure teardown fails
91+
/// - Progress reporting encounters a poisoned mutex
92+
///
93+
/// # Returns
94+
///
95+
/// Returns `Ok(())` on success, or a `DestroySubcommandError` if any step fails.
96+
#[allow(clippy::result_large_err)]
97+
pub fn execute(&mut self, environment_name: &str) -> Result<(), DestroySubcommandError> {
98+
let env_name = self.validate_environment_name(environment_name)?;
99+
let handler = self.initialize_dependencies()?;
100+
let _destroyed = self.tear_down_infrastructure(&handler, &env_name)?;
101+
self.complete_workflow(environment_name)?;
102+
Ok(())
103+
}
104+
105+
/// Validate the environment name format
106+
///
107+
/// Shows progress to user and validates that the environment name
108+
/// meets domain requirements (1-63 chars, alphanumeric + hyphens).
109+
#[allow(clippy::result_large_err)]
110+
fn validate_environment_name(
111+
&mut self,
112+
name: &str,
113+
) -> Result<EnvironmentName, DestroySubcommandError> {
114+
self.progress.start_step("Validating environment")?;
115+
116+
let env_name = EnvironmentName::new(name.to_string()).map_err(|source| {
117+
DestroySubcommandError::InvalidEnvironmentName {
118+
name: name.to_string(),
119+
source,
120+
}
121+
})?;
122+
123+
self.progress
124+
.complete_step(Some(&format!("Environment name validated: {name}")))?;
125+
126+
Ok(env_name)
127+
}
128+
129+
/// Initialize application layer dependencies
130+
///
131+
/// Creates the application layer command handler with all required
132+
/// dependencies (repository, clock, etc.).
133+
#[allow(clippy::result_large_err)]
134+
fn initialize_dependencies(&mut self) -> Result<DestroyCommandHandler, DestroySubcommandError> {
135+
self.progress.start_step("Initializing dependencies")?;
136+
let handler = self.factory.create_destroy_handler(&self.ctx);
137+
self.progress.complete_step(None)?;
138+
Ok(handler)
139+
}
140+
141+
/// Execute infrastructure teardown via application layer
142+
///
143+
/// Delegates to the application layer `DestroyCommandHandler` to
144+
/// orchestrate the actual infrastructure destruction workflow.
145+
#[allow(clippy::result_large_err)]
146+
fn tear_down_infrastructure(
147+
&mut self,
148+
handler: &DestroyCommandHandler,
149+
env_name: &EnvironmentName,
150+
) -> Result<Environment<Destroyed>, DestroySubcommandError> {
151+
self.progress.start_step("Tearing down infrastructure")?;
152+
153+
let destroyed = handler.execute(env_name).map_err(|source| {
154+
DestroySubcommandError::DestroyOperationFailed {
155+
name: env_name.to_string(),
156+
source,
157+
}
158+
})?;
159+
160+
self.progress
161+
.complete_step(Some("Infrastructure torn down"))?;
162+
Ok(destroyed)
163+
}
164+
165+
/// Complete the workflow with success message
166+
///
167+
/// Shows final success message to the user with workflow summary.
168+
#[allow(clippy::result_large_err)]
169+
fn complete_workflow(&mut self, name: &str) -> Result<(), DestroySubcommandError> {
170+
self.progress
171+
.complete(&format!("Environment '{name}' destroyed successfully"))?;
172+
Ok(())
173+
}
174+
}
175+
176+
// ============================================================================
177+
// PUBLIC ENTRY POINT
178+
// ============================================================================
179+
180+
/// Handle the destroy command
21181
///
22-
/// Each step is tracked and timed using `ProgressReporter` for clear user feedback.
182+
/// This is a thin wrapper over `DestroyCommandController` that serves as
183+
/// the public entry point for the destroy command.
23184
///
24185
/// # Arguments
25186
///
26187
/// * `environment_name` - The name of the environment to destroy
27188
/// * `working_dir` - Root directory for environment data storage
28189
/// * `user_output` - Shared user output service for consistent output formatting
29190
///
30-
/// # Returns
31-
///
32-
/// Returns `Ok(())` on success, or a `DestroySubcommandError` if:
33-
/// - Environment name is invalid
34-
/// - Environment cannot be loaded
35-
/// - Destruction fails
36-
///
37191
/// # Errors
38192
///
39-
/// This function will return an error if the environment name is invalid,
40-
/// the environment cannot be loaded, or the destruction process fails.
193+
/// Returns an error if:
194+
/// - Environment name is invalid (format validation fails)
195+
/// - Environment cannot be loaded from repository
196+
/// - Infrastructure teardown fails
197+
/// - Progress reporting encounters a poisoned mutex
198+
///
41199
/// All errors include detailed context and actionable troubleshooting guidance.
42200
///
201+
/// # Returns
202+
///
203+
/// Returns `Ok(())` on success, or a `DestroySubcommandError` on failure.
204+
///
43205
/// # Example
44206
///
45207
/// ```rust
@@ -60,48 +222,14 @@ pub fn handle_destroy_command(
60222
working_dir: &std::path::Path,
61223
user_output: &Arc<Mutex<UserOutput>>,
62224
) -> Result<(), DestroySubcommandError> {
63-
// Create factory and context with all shared dependencies
64-
let factory = CommandHandlerFactory::new();
65-
let ctx = factory.create_context(working_dir.to_path_buf(), user_output.clone());
66-
67-
// Create progress reporter for 3 main steps
68-
let mut progress = ProgressReporter::new(ctx.user_output().clone(), 3);
69-
70-
// Step 1: Validate environment name
71-
progress.start_step("Validating environment")?;
72-
let env_name = EnvironmentName::new(environment_name.to_string()).map_err(|source| {
73-
DestroySubcommandError::InvalidEnvironmentName {
74-
name: environment_name.to_string(),
75-
source,
76-
}
77-
})?;
78-
progress.complete_step(Some(&format!(
79-
"Environment name validated: {environment_name}"
80-
)))?;
81-
82-
// Step 2: Initialize dependencies
83-
progress.start_step("Initializing dependencies")?;
84-
let command_handler = factory.create_destroy_handler(&ctx);
85-
progress.complete_step(None)?;
86-
87-
// Step 3: Execute destroy command (tear down infrastructure)
88-
progress.start_step("Tearing down infrastructure")?;
89-
let _destroyed_env = command_handler.execute(&env_name).map_err(|source| {
90-
DestroySubcommandError::DestroyOperationFailed {
91-
name: environment_name.to_string(),
92-
source,
93-
}
94-
})?;
95-
progress.complete_step(Some("Infrastructure torn down"))?;
96-
97-
// Complete with summary
98-
progress.complete(&format!(
99-
"Environment '{environment_name}' destroyed successfully"
100-
))?;
101-
102-
Ok(())
225+
DestroyCommandController::new(working_dir.to_path_buf(), user_output.clone())
226+
.execute(environment_name)
103227
}
104228

229+
// ============================================================================
230+
// TESTS
231+
// ============================================================================
232+
105233
#[cfg(test)]
106234
mod tests {
107235
use super::*;

0 commit comments

Comments
 (0)