Skip to content

Commit 4e1da6f

Browse files
committed
refactor: improve Container API with explicit verbosity control
- Replace separate for_testing() constructor with single Container::new(verbosity_level) approach - Use VerbosityLevel::Silent for test contexts to eliminate unwanted output - Use VerbosityLevel::Normal as default for production contexts - Add repository_factory() and clock() accessor methods to ExecutionContext - Update all Container usage sites across application and test code - Maintain backward compatibility while providing cleaner API design This change provides explicit control over output verbosity rather than implicit test-specific constructors, improving API clarity and maintainability.
1 parent 6c2f549 commit 4e1da6f

File tree

9 files changed

+329
-49
lines changed

9 files changed

+329
-49
lines changed

src/bootstrap/app.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ pub fn run() {
5858
);
5959

6060
// Initialize service container for dependency injection
61-
let container = Arc::new(bootstrap::Container::new());
61+
let container = Arc::new(bootstrap::Container::new(
62+
crate::presentation::commands::constants::DEFAULT_VERBOSITY,
63+
));
6264
let context = presentation::dispatch::ExecutionContext::new(container);
6365

6466
match cli.command {

src/bootstrap/container.rs

Lines changed: 166 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,70 @@
55
66
use std::sync::{Arc, Mutex};
77

8-
use crate::presentation::commands::constants::DEFAULT_VERBOSITY;
9-
use crate::presentation::user_output::UserOutput;
8+
use crate::infrastructure::persistence::repository_factory::RepositoryFactory;
9+
use crate::presentation::commands::constants::DEFAULT_LOCK_TIMEOUT;
10+
use crate::presentation::user_output::{UserOutput, VerbosityLevel};
11+
use crate::shared::clock::Clock;
12+
use crate::shared::SystemClock;
1013

1114
/// Application service container
1215
///
1316
/// Holds shared services initialized during application bootstrap.
14-
/// Services are wrapped in `Arc<Mutex<T>>` for thread-safe shared ownership
15-
/// with interior mutability across the application.
17+
/// Services are wrapped in `Arc<T>` for thread-safe shared ownership
18+
/// across the application.
1619
///
1720
/// # Example
1821
///
1922
/// ```rust
2023
/// use torrust_tracker_deployer_lib::bootstrap::container::Container;
24+
/// use torrust_tracker_deployer_lib::presentation::user_output::VerbosityLevel;
2125
///
22-
/// let container = Container::new();
26+
/// let container = Container::new(VerbosityLevel::Normal);
2327
/// let user_output = container.user_output();
2428
/// user_output.lock().unwrap().success("Operation completed");
2529
/// ```
2630
#[derive(Clone)]
2731
pub struct Container {
2832
user_output: Arc<Mutex<UserOutput>>,
33+
repository_factory: Arc<RepositoryFactory>,
34+
clock: Arc<dyn Clock>,
2935
}
3036

3137
impl Container {
3238
/// Create a new container with initialized services
3339
///
34-
/// Uses `DEFAULT_VERBOSITY` for user output. In the future, this may
35-
/// accept a verbosity parameter from CLI flags.
40+
/// Initializes all services with specified verbosity level:
41+
/// - `UserOutput` with provided `verbosity_level`
42+
/// - `RepositoryFactory` with `DEFAULT_LOCK_TIMEOUT`
43+
/// - `SystemClock` for time operations
44+
///
45+
/// # Arguments
46+
///
47+
/// * `verbosity_level` - Controls how verbose the user output will be
48+
///
49+
/// # Examples
50+
///
51+
/// ```rust
52+
/// use torrust_tracker_deployer_lib::bootstrap::container::Container;
53+
/// use torrust_tracker_deployer_lib::presentation::user_output::VerbosityLevel;
54+
///
55+
/// // For normal application use
56+
/// let container = Container::new(VerbosityLevel::Normal);
57+
///
58+
/// // For completely silent testing
59+
/// let container = Container::new(VerbosityLevel::Silent);
60+
/// ```
3661
#[must_use]
37-
pub fn new() -> Self {
38-
let user_output = Arc::new(Mutex::new(UserOutput::new(DEFAULT_VERBOSITY)));
62+
pub fn new(verbosity_level: VerbosityLevel) -> Self {
63+
let user_output = Arc::new(Mutex::new(UserOutput::new(verbosity_level)));
64+
let repository_factory = Arc::new(RepositoryFactory::new(DEFAULT_LOCK_TIMEOUT));
65+
let clock: Arc<dyn Clock> = Arc::new(SystemClock);
3966

40-
Self { user_output }
67+
Self {
68+
user_output,
69+
repository_factory,
70+
clock,
71+
}
4172
}
4273

4374
/// Get shared reference to user output service
@@ -49,20 +80,61 @@ impl Container {
4980
///
5081
/// ```rust
5182
/// use torrust_tracker_deployer_lib::bootstrap::container::Container;
83+
/// use torrust_tracker_deployer_lib::presentation::user_output::VerbosityLevel;
5284
///
53-
/// let container = Container::new();
85+
/// let container = Container::new(VerbosityLevel::Normal);
5486
/// let user_output = container.user_output();
5587
/// user_output.lock().unwrap().success("Operation completed");
5688
/// ```
5789
#[must_use]
5890
pub fn user_output(&self) -> Arc<Mutex<UserOutput>> {
5991
Arc::clone(&self.user_output)
6092
}
93+
94+
/// Get shared reference to repository factory service
95+
///
96+
/// Returns an `Arc<RepositoryFactory>` that can be cheaply cloned and shared
97+
/// across threads and function calls.
98+
///
99+
/// # Example
100+
///
101+
/// ```rust
102+
/// use torrust_tracker_deployer_lib::bootstrap::container::Container;
103+
/// use torrust_tracker_deployer_lib::presentation::user_output::VerbosityLevel;
104+
///
105+
/// let container = Container::new(VerbosityLevel::Normal);
106+
/// let repository_factory = container.repository_factory();
107+
/// // Use repository_factory to create repositories
108+
/// ```
109+
#[must_use]
110+
pub fn repository_factory(&self) -> Arc<RepositoryFactory> {
111+
Arc::clone(&self.repository_factory)
112+
}
113+
114+
/// Get shared reference to clock service
115+
///
116+
/// Returns an `Arc<dyn Clock>` that can be cheaply cloned and shared
117+
/// across threads and function calls.
118+
///
119+
/// # Example
120+
///
121+
/// ```rust
122+
/// use torrust_tracker_deployer_lib::bootstrap::container::Container;
123+
/// use torrust_tracker_deployer_lib::presentation::user_output::VerbosityLevel;
124+
///
125+
/// let container = Container::new(VerbosityLevel::Normal);
126+
/// let clock = container.clock();
127+
/// // Use clock for time operations
128+
/// ```
129+
#[must_use]
130+
pub fn clock(&self) -> Arc<dyn Clock> {
131+
Arc::clone(&self.clock)
132+
}
61133
}
62134

63135
impl Default for Container {
64136
fn default() -> Self {
65-
Self::new()
137+
Self::new(VerbosityLevel::Normal)
66138
}
67139
}
68140

@@ -71,17 +143,42 @@ mod tests {
71143
use super::*;
72144

73145
#[test]
74-
fn it_should_create_container_with_user_output() {
75-
let container = Container::new();
146+
fn it_should_create_container_with_all_services() {
147+
let container = Container::new(VerbosityLevel::Normal);
148+
149+
// Verify we can get all services
76150
let user_output = container.user_output();
151+
let repository_factory = container.repository_factory();
152+
let clock = container.clock();
77153

78-
// Verify we can get the user_output service
79154
assert!(Arc::strong_count(&user_output) >= 1);
155+
assert!(Arc::strong_count(&repository_factory) >= 1);
156+
assert!(Arc::strong_count(&clock) >= 1);
157+
}
158+
159+
#[test]
160+
fn it_should_return_cloned_arc_on_repository_factory_access() {
161+
let container = Container::new(VerbosityLevel::Normal);
162+
let factory1 = container.repository_factory();
163+
let factory2 = container.repository_factory();
164+
165+
// Both should point to the same RepositoryFactory instance
166+
assert!(Arc::ptr_eq(&factory1, &factory2));
167+
}
168+
169+
#[test]
170+
fn it_should_return_cloned_arc_on_clock_access() {
171+
let container = Container::new(VerbosityLevel::Normal);
172+
let clock1 = container.clock();
173+
let clock2 = container.clock();
174+
175+
// Both should point to the same Clock instance
176+
assert!(Arc::ptr_eq(&clock1, &clock2));
80177
}
81178

82179
#[test]
83180
fn it_should_return_cloned_arc_on_user_output_access() {
84-
let container = Container::new();
181+
let container = Container::new(VerbosityLevel::Normal);
85182
let user_output1 = container.user_output();
86183
let user_output2 = container.user_output();
87184

@@ -91,13 +188,63 @@ mod tests {
91188

92189
#[test]
93190
fn it_should_be_clonable() {
94-
let container1 = Container::new();
191+
let container1 = Container::new(VerbosityLevel::Normal);
95192
let container2 = container1.clone();
96193

194+
// Cloned containers should share all services
97195
let user_output1 = container1.user_output();
98196
let user_output2 = container2.user_output();
99-
100-
// Cloned containers should share the same UserOutput
101197
assert!(Arc::ptr_eq(&user_output1, &user_output2));
198+
199+
let factory1 = container1.repository_factory();
200+
let factory2 = container2.repository_factory();
201+
assert!(Arc::ptr_eq(&factory1, &factory2));
202+
203+
let clock1 = container1.clock();
204+
let clock2 = container2.clock();
205+
assert!(Arc::ptr_eq(&clock1, &clock2));
206+
}
207+
208+
#[test]
209+
fn it_should_create_container_with_silent_verbosity_for_tests() {
210+
let container = Container::new(VerbosityLevel::Silent);
211+
212+
// All services should be available
213+
let user_output = container.user_output();
214+
let repository_factory = container.repository_factory();
215+
let clock = container.clock();
216+
217+
assert!(Arc::strong_count(&user_output) >= 1);
218+
assert!(Arc::strong_count(&repository_factory) >= 1);
219+
assert!(Arc::strong_count(&clock) >= 1);
220+
221+
// The container should work with any verbosity level, including Silent for tests
222+
// Silent mode will suppress all output, making tests clean
223+
}
224+
225+
#[test]
226+
fn it_should_create_container_with_different_verbosity_levels() {
227+
// Test all available verbosity levels
228+
let levels = [
229+
VerbosityLevel::Silent,
230+
VerbosityLevel::Quiet,
231+
VerbosityLevel::Normal,
232+
VerbosityLevel::Verbose,
233+
VerbosityLevel::VeryVerbose,
234+
VerbosityLevel::Debug,
235+
];
236+
237+
for level in &levels {
238+
let container = Container::new(*level);
239+
240+
// All services should be available regardless of verbosity level
241+
let user_output = container.user_output();
242+
let repository_factory = container.repository_factory();
243+
let clock = container.clock();
244+
245+
assert!(Arc::strong_count(&user_output) >= 1);
246+
assert!(Arc::strong_count(&repository_factory) >= 1);
247+
assert!(Arc::strong_count(&clock) >= 1);
248+
}
102249
}
103250
}

src/presentation/controllers/create/subcommands/environment.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ mod tests {
260260
_working_dir: &Path,
261261
_user_output: Arc<Mutex<UserOutput>>,
262262
) -> ExecutionContext {
263-
let container = Container::new();
263+
let container = Container::new(VerbosityLevel::Silent);
264264
ExecutionContext::new(Arc::new(container))
265265
}
266266

src/presentation/controllers/destroy/errors.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,28 @@ impl DestroySubcommandError {
109109
///
110110
/// # Example
111111
///
112-
/// ```rust,no_run
112+
/// Using with Container and `ExecutionContext` (recommended):
113+
///
114+
/// ```rust
115+
/// use std::path::Path;
116+
/// use std::sync::Arc;
117+
/// use torrust_tracker_deployer_lib::bootstrap::Container;
118+
/// use torrust_tracker_deployer_lib::presentation::dispatch::ExecutionContext;
119+
/// use torrust_tracker_deployer_lib::presentation::controllers::destroy;
120+
/// use torrust_tracker_deployer_lib::presentation::user_output::VerbosityLevel;
121+
///
122+
/// let container = Container::new(VerbosityLevel::Normal);
123+
/// let context = ExecutionContext::new(Arc::new(container));
124+
///
125+
/// if let Err(e) = destroy::handle("test-env", Path::new("."), &context) {
126+
/// eprintln!("Error: {e}");
127+
/// eprintln!("\nTroubleshooting:\n{}", e.help());
128+
/// }
129+
/// ```
130+
///
131+
/// Direct usage (for testing):
132+
///
133+
/// ```rust
113134
/// use std::path::Path;
114135
/// use std::sync::{Arc, Mutex};
115136
/// use std::time::Duration;

src/presentation/controllers/destroy/handler.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@ use crate::domain::environment::repository::EnvironmentRepository;
1111
use crate::domain::environment::state::Destroyed;
1212
use crate::domain::environment::Environment;
1313
use crate::infrastructure::persistence::repository_factory::RepositoryFactory;
14-
use crate::presentation::commands::constants::DEFAULT_LOCK_TIMEOUT;
1514
use crate::presentation::progress::ProgressReporter;
1615
use crate::presentation::user_output::UserOutput;
1716
use crate::shared::clock::Clock;
18-
use crate::shared::SystemClock;
1917

2018
use super::errors::DestroySubcommandError;
2119

@@ -214,6 +212,27 @@ impl DestroyCommandController {
214212
///
215213
/// # Example
216214
///
215+
/// Using with Container and `ExecutionContext` (recommended):
216+
///
217+
/// ```rust
218+
/// use std::path::Path;
219+
/// use std::sync::Arc;
220+
/// use torrust_tracker_deployer_lib::bootstrap::Container;
221+
/// use torrust_tracker_deployer_lib::presentation::dispatch::ExecutionContext;
222+
/// use torrust_tracker_deployer_lib::presentation::controllers::destroy;
223+
/// use torrust_tracker_deployer_lib::presentation::user_output::VerbosityLevel;
224+
///
225+
/// let container = Container::new(VerbosityLevel::Normal);
226+
/// let context = ExecutionContext::new(Arc::new(container));
227+
///
228+
/// if let Err(e) = destroy::handle("test-env", Path::new("."), &context) {
229+
/// eprintln!("Destroy failed: {e}");
230+
/// eprintln!("Help: {}", e.help());
231+
/// }
232+
/// ```
233+
///
234+
/// Direct usage (for testing or specialized scenarios):
235+
///
217236
/// ```rust
218237
/// use std::path::Path;
219238
/// use std::sync::{Arc, Mutex};
@@ -286,9 +305,10 @@ pub fn handle_destroy_command(
286305
/// use torrust_tracker_deployer_lib::presentation::controllers::destroy;
287306
/// use torrust_tracker_deployer_lib::presentation::dispatch::context::ExecutionContext;
288307
/// use torrust_tracker_deployer_lib::bootstrap::container::Container;
308+
/// use torrust_tracker_deployer_lib::presentation::user_output::VerbosityLevel;
289309
///
290310
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
291-
/// let container = Arc::new(Container::new());
311+
/// let container = Arc::new(Container::new(VerbosityLevel::Normal));
292312
/// let context = ExecutionContext::new(container);
293313
/// let working_dir = Path::new("./test");
294314
///
@@ -302,14 +322,11 @@ pub fn handle(
302322
working_dir: &std::path::Path,
303323
context: &crate::presentation::dispatch::context::ExecutionContext,
304324
) -> Result<(), DestroySubcommandError> {
305-
let repository_factory = Arc::new(RepositoryFactory::new(DEFAULT_LOCK_TIMEOUT));
306-
let clock = Arc::new(SystemClock);
307-
308325
handle_destroy_command(
309326
environment_name,
310327
working_dir,
311-
repository_factory,
312-
clock,
328+
context.repository_factory(),
329+
context.clock(),
313330
&context.user_output(),
314331
)
315332
}
@@ -321,8 +338,10 @@ pub fn handle(
321338
#[cfg(test)]
322339
mod tests {
323340
use super::*;
341+
use crate::presentation::commands::constants::DEFAULT_LOCK_TIMEOUT;
324342
use crate::presentation::user_output::test_support::TestUserOutput;
325343
use crate::presentation::user_output::VerbosityLevel;
344+
use crate::shared::SystemClock;
326345
use std::fs;
327346
use tempfile::TempDir;
328347

0 commit comments

Comments
 (0)