Skip to content

Commit 5f7c776

Browse files
Copilotjosecelano
andcommitted
feat: add CommandHandlerFactory for centralized handler creation
Co-authored-by: josecelano <[email protected]>
1 parent a2420b2 commit 5f7c776

File tree

3 files changed

+378
-0
lines changed

3 files changed

+378
-0
lines changed

src/presentation/commands/context.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,45 @@ impl CommandContext {
128128
}
129129
}
130130

131+
/// Create a command context using an existing repository factory
132+
///
133+
/// This constructor allows creating a context with a pre-configured repository factory,
134+
/// useful when consistent repository configuration (like lock timeout) needs to be
135+
/// shared across multiple contexts.
136+
///
137+
/// # Arguments
138+
///
139+
/// * `repository_factory` - Pre-configured repository factory
140+
/// * `working_dir` - Root directory for environment data storage
141+
///
142+
/// # Examples
143+
///
144+
/// ```rust
145+
/// use std::path::PathBuf;
146+
/// use std::time::Duration;
147+
/// use torrust_tracker_deployer_lib::presentation::commands::context::CommandContext;
148+
/// use torrust_tracker_deployer_lib::infrastructure::persistence::repository_factory::RepositoryFactory;
149+
///
150+
/// let factory = RepositoryFactory::new(Duration::from_secs(30));
151+
/// let working_dir = PathBuf::from("./data");
152+
/// let ctx = CommandContext::new_with_factory(&factory, working_dir);
153+
/// ```
154+
#[must_use]
155+
pub fn new_with_factory(
156+
repository_factory: &RepositoryFactory,
157+
working_dir: PathBuf,
158+
) -> Self {
159+
let repository = repository_factory.create(working_dir);
160+
let clock = Arc::new(SystemClock);
161+
let output = UserOutput::new(DEFAULT_VERBOSITY);
162+
163+
Self {
164+
repository,
165+
clock,
166+
output,
167+
}
168+
}
169+
131170
/// Create a command context for testing with injected dependencies
132171
///
133172
/// This constructor allows tests to inject mock implementations for better isolation
@@ -296,6 +335,19 @@ mod tests {
296335
ctx.output().success("Test success");
297336
}
298337

338+
#[test]
339+
fn it_should_create_context_with_factory() {
340+
let temp_dir = TempDir::new().unwrap();
341+
let working_dir = temp_dir.path().to_path_buf();
342+
343+
let repository_factory = RepositoryFactory::new(DEFAULT_LOCK_TIMEOUT);
344+
let ctx = CommandContext::new_with_factory(&repository_factory, working_dir);
345+
346+
// Verify we can access all dependencies
347+
let _repo = ctx.repository();
348+
let _clock = ctx.clock();
349+
}
350+
299351
#[test]
300352
fn it_should_allow_creating_context_for_testing() {
301353
let temp_dir = TempDir::new().unwrap();
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
//! Command Handler Factory
2+
//!
3+
//! Provides centralized creation of command handlers with consistent
4+
//! dependency injection and configuration management.
5+
//!
6+
//! ## Purpose
7+
//!
8+
//! Previously, each presentation command handler manually created application
9+
//! command handlers with explicit dependency setup:
10+
//!
11+
//! ```rust,ignore
12+
//! // Duplicate code in every handler:
13+
//! let command_handler = CreateCommandHandler::new(
14+
//! ctx.repository().clone(),
15+
//! ctx.clock().clone()
16+
//! );
17+
//! ```
18+
//!
19+
//! `CommandHandlerFactory` eliminates this duplication by providing a single place to:
20+
//! - Create application layer command handlers consistently
21+
//! - Manage shared configuration (lock timeout)
22+
//! - Support testing with custom factory configuration
23+
//!
24+
//! ## Benefits
25+
//!
26+
//! - **Consistency**: All command handlers created with same configuration
27+
//! - **Maintainability**: Changes to handler creation logic in one place
28+
//! - **Testability**: Easy to inject test configuration via `new_for_testing()`
29+
//! - **Simplicity**: Presentation handlers focus on workflow, not setup
30+
//!
31+
//! ## Usage Example
32+
//!
33+
//! ```rust
34+
//! use std::path::Path;
35+
//! use torrust_tracker_deployer_lib::presentation::commands::factory::CommandHandlerFactory;
36+
//!
37+
//! fn handle_command(working_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
38+
//! // Create factory with default configuration
39+
//! let factory = CommandHandlerFactory::new();
40+
//!
41+
//! // Create command context
42+
//! let context = factory.create_context(working_dir.to_path_buf());
43+
//!
44+
//! // Create command handler
45+
//! let handler = factory.create_create_handler(&context);
46+
//!
47+
//! // Use handler...
48+
//! Ok(())
49+
//! }
50+
//! ```
51+
52+
use std::path::PathBuf;
53+
54+
use crate::application::command_handlers::{CreateCommandHandler, DestroyCommandHandler};
55+
use crate::infrastructure::persistence::repository_factory::RepositoryFactory;
56+
57+
use super::constants::DEFAULT_LOCK_TIMEOUT;
58+
use super::context::CommandContext;
59+
60+
/// Factory for creating command handlers with consistent configuration
61+
///
62+
/// This factory centralizes the creation of application layer command handlers,
63+
/// ensuring consistent dependency injection and configuration across all commands.
64+
///
65+
/// The factory uses `RepositoryFactory` to configure repository lock timeouts,
66+
/// and delegates context creation to `CommandContext` for managing output,
67+
/// repository, and clock dependencies.
68+
///
69+
/// # Examples
70+
///
71+
/// ```rust
72+
/// use std::path::PathBuf;
73+
/// use torrust_tracker_deployer_lib::presentation::commands::factory::CommandHandlerFactory;
74+
///
75+
/// let factory = CommandHandlerFactory::new();
76+
/// let context = factory.create_context(PathBuf::from("."));
77+
/// let handler = factory.create_create_handler(&context);
78+
/// ```
79+
pub struct CommandHandlerFactory {
80+
/// Repository factory for creating environment repositories
81+
repository_factory: RepositoryFactory,
82+
}
83+
84+
impl CommandHandlerFactory {
85+
/// Create a new factory with default configuration
86+
///
87+
/// This constructor initializes the factory with production defaults:
88+
/// - Repository lock timeout from `DEFAULT_LOCK_TIMEOUT`
89+
///
90+
/// # Examples
91+
///
92+
/// ```rust
93+
/// use torrust_tracker_deployer_lib::presentation::commands::factory::CommandHandlerFactory;
94+
///
95+
/// let factory = CommandHandlerFactory::new();
96+
/// ```
97+
#[must_use]
98+
pub fn new() -> Self {
99+
let repository_factory = RepositoryFactory::new(DEFAULT_LOCK_TIMEOUT);
100+
Self { repository_factory }
101+
}
102+
103+
/// Create a command context for the given working directory
104+
///
105+
/// This method creates a `CommandContext` with all shared dependencies:
106+
/// - Repository configured with the factory's settings
107+
/// - System clock
108+
/// - User output with default verbosity
109+
///
110+
/// # Arguments
111+
///
112+
/// * `working_dir` - Root directory for environment data storage
113+
///
114+
/// # Returns
115+
///
116+
/// A `CommandContext` ready for use with command handlers
117+
///
118+
/// # Examples
119+
///
120+
/// ```rust
121+
/// use std::path::PathBuf;
122+
/// use torrust_tracker_deployer_lib::presentation::commands::factory::CommandHandlerFactory;
123+
///
124+
/// let factory = CommandHandlerFactory::new();
125+
/// let context = factory.create_context(PathBuf::from("./data"));
126+
/// ```
127+
#[must_use]
128+
pub fn create_context(&self, working_dir: PathBuf) -> CommandContext {
129+
CommandContext::new_with_factory(&self.repository_factory, working_dir)
130+
}
131+
132+
/// Create a create command handler
133+
///
134+
/// This method creates a `CreateCommandHandler` with dependencies from the context.
135+
///
136+
/// # Arguments
137+
///
138+
/// * `context` - Command context containing repository and clock
139+
///
140+
/// # Returns
141+
///
142+
/// A `CreateCommandHandler` ready to execute create operations
143+
///
144+
/// # Examples
145+
///
146+
/// ```rust
147+
/// use std::path::PathBuf;
148+
/// use torrust_tracker_deployer_lib::presentation::commands::factory::CommandHandlerFactory;
149+
///
150+
/// let factory = CommandHandlerFactory::new();
151+
/// let context = factory.create_context(PathBuf::from("."));
152+
/// let handler = factory.create_create_handler(&context);
153+
/// ```
154+
#[must_use]
155+
pub fn create_create_handler(&self, context: &CommandContext) -> CreateCommandHandler {
156+
CreateCommandHandler::new(context.repository().clone(), context.clock().clone())
157+
}
158+
159+
/// Create a destroy command handler
160+
///
161+
/// This method creates a `DestroyCommandHandler` with dependencies from the context.
162+
///
163+
/// # Arguments
164+
///
165+
/// * `context` - Command context containing repository and clock
166+
///
167+
/// # Returns
168+
///
169+
/// A `DestroyCommandHandler` ready to execute destroy operations
170+
///
171+
/// # Examples
172+
///
173+
/// ```rust
174+
/// use std::path::PathBuf;
175+
/// use torrust_tracker_deployer_lib::presentation::commands::factory::CommandHandlerFactory;
176+
///
177+
/// let factory = CommandHandlerFactory::new();
178+
/// let context = factory.create_context(PathBuf::from("."));
179+
/// let handler = factory.create_destroy_handler(&context);
180+
/// ```
181+
#[must_use]
182+
pub fn create_destroy_handler(&self, context: &CommandContext) -> DestroyCommandHandler {
183+
DestroyCommandHandler::new(context.repository().clone(), context.clock().clone())
184+
}
185+
}
186+
187+
impl Default for CommandHandlerFactory {
188+
fn default() -> Self {
189+
Self::new()
190+
}
191+
}
192+
193+
#[cfg(test)]
194+
impl CommandHandlerFactory {
195+
/// Create a factory for testing with custom repository factory
196+
///
197+
/// This constructor allows tests to inject custom configuration, such as
198+
/// different lock timeouts for testing timeout scenarios.
199+
///
200+
/// # Arguments
201+
///
202+
/// * `repository_factory` - Custom repository factory for testing
203+
///
204+
/// # Examples
205+
///
206+
/// ```rust
207+
/// use std::time::Duration;
208+
/// use torrust_tracker_deployer_lib::presentation::commands::factory::CommandHandlerFactory;
209+
/// use torrust_tracker_deployer_lib::infrastructure::persistence::repository_factory::RepositoryFactory;
210+
///
211+
/// // Create factory with custom lock timeout for testing
212+
/// let repository_factory = RepositoryFactory::new(Duration::from_millis(100));
213+
/// let factory = CommandHandlerFactory::new_for_testing(repository_factory);
214+
/// ```
215+
#[must_use]
216+
pub fn new_for_testing(repository_factory: RepositoryFactory) -> Self {
217+
Self { repository_factory }
218+
}
219+
}
220+
221+
#[cfg(test)]
222+
mod tests {
223+
use super::*;
224+
use tempfile::TempDir;
225+
226+
#[test]
227+
fn it_should_create_factory_with_default_configuration() {
228+
let factory = CommandHandlerFactory::new();
229+
230+
// Verify factory is created (basic structure test)
231+
// The internal repository_factory is private, so we just verify
232+
// the factory can be created
233+
let _ = factory;
234+
}
235+
236+
#[test]
237+
fn it_should_create_factory_via_default_trait() {
238+
let factory = CommandHandlerFactory::default();
239+
240+
// Verify default trait works
241+
let _ = factory;
242+
}
243+
244+
#[test]
245+
fn it_should_create_context_with_factory() {
246+
let factory = CommandHandlerFactory::new();
247+
let temp_dir = TempDir::new().unwrap();
248+
let working_dir = temp_dir.path().to_path_buf();
249+
250+
let context = factory.create_context(working_dir);
251+
252+
// Verify context is created with dependencies
253+
let _ = context.repository();
254+
let _ = context.clock();
255+
}
256+
257+
#[test]
258+
fn it_should_create_create_handler() {
259+
let factory = CommandHandlerFactory::new();
260+
let temp_dir = TempDir::new().unwrap();
261+
let working_dir = temp_dir.path().to_path_buf();
262+
263+
let context = factory.create_context(working_dir);
264+
let _handler = factory.create_create_handler(&context);
265+
266+
// Verify handler is created (basic structure test)
267+
}
268+
269+
#[test]
270+
fn it_should_create_destroy_handler() {
271+
let factory = CommandHandlerFactory::new();
272+
let temp_dir = TempDir::new().unwrap();
273+
let working_dir = temp_dir.path().to_path_buf();
274+
275+
let context = factory.create_context(working_dir);
276+
let _handler = factory.create_destroy_handler(&context);
277+
278+
// Verify handler is created (basic structure test)
279+
}
280+
281+
#[test]
282+
fn it_should_create_multiple_contexts_from_same_factory() {
283+
let factory = CommandHandlerFactory::new();
284+
let temp_dir = TempDir::new().unwrap();
285+
let working_dir = temp_dir.path().to_path_buf();
286+
287+
// Should be able to create multiple contexts
288+
let context1 = factory.create_context(working_dir.clone());
289+
let context2 = factory.create_context(working_dir);
290+
291+
// Both contexts should be functional
292+
let _ = context1.repository();
293+
let _ = context2.repository();
294+
}
295+
296+
#[test]
297+
fn it_should_create_multiple_handlers_from_same_context() {
298+
let factory = CommandHandlerFactory::new();
299+
let temp_dir = TempDir::new().unwrap();
300+
let working_dir = temp_dir.path().to_path_buf();
301+
302+
let context = factory.create_context(working_dir);
303+
304+
// Should be able to create multiple handlers from same context
305+
let _create_handler = factory.create_create_handler(&context);
306+
let _destroy_handler = factory.create_destroy_handler(&context);
307+
308+
// Both handlers should be functional
309+
}
310+
311+
#[test]
312+
fn it_should_create_factory_for_testing() {
313+
use std::time::Duration;
314+
315+
let repository_factory = RepositoryFactory::new(Duration::from_millis(100));
316+
let factory = CommandHandlerFactory::new_for_testing(repository_factory);
317+
318+
let temp_dir = TempDir::new().unwrap();
319+
let working_dir = temp_dir.path().to_path_buf();
320+
321+
// Should be able to create context with custom factory
322+
let context = factory.create_context(working_dir);
323+
let _ = context.repository();
324+
}
325+
}

src/presentation/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod constants;
1212
pub mod context;
1313
pub mod create;
1414
pub mod destroy;
15+
pub mod factory;
1516

1617
// Shared test utilities
1718
#[cfg(test)]

0 commit comments

Comments
 (0)