Skip to main content

torrust_tracker_deployer_lib/bootstrap/
container.rs

1//! Application Service Container
2//!
3//! This module provides centralized initialization of application-wide services
4//! that need consistent configuration across the entire application.
5
6use std::cell::RefCell;
7use std::path::Path;
8use std::sync::Arc;
9
10use parking_lot::ReentrantMutex;
11
12use crate::application::command_handlers::PurgeCommandHandler;
13use crate::application::traits::RepositoryProvider;
14use crate::domain::environment::repository::EnvironmentRepository;
15use crate::infrastructure::persistence::file_repository_factory::FileRepositoryFactory;
16use crate::presentation::cli::controllers::configure::ConfigureCommandController;
17use crate::presentation::cli::controllers::constants::DEFAULT_LOCK_TIMEOUT;
18use crate::presentation::cli::controllers::create::subcommands::environment::CreateEnvironmentCommandController;
19use crate::presentation::cli::controllers::create::subcommands::schema::CreateSchemaCommandController;
20use crate::presentation::cli::controllers::create::subcommands::template::CreateTemplateCommandController;
21use crate::presentation::cli::controllers::destroy::DestroyCommandController;
22use crate::presentation::cli::controllers::docs::DocsCommandController;
23use crate::presentation::cli::controllers::exists::ExistsCommandController;
24use crate::presentation::cli::controllers::list::ListCommandController;
25use crate::presentation::cli::controllers::provision::ProvisionCommandController;
26use crate::presentation::cli::controllers::purge::PurgeCommandController;
27use crate::presentation::cli::controllers::register::RegisterCommandController;
28use crate::presentation::cli::controllers::release::ReleaseCommandController;
29use crate::presentation::cli::controllers::render::RenderCommandController;
30use crate::presentation::cli::controllers::run::RunCommandController;
31use crate::presentation::cli::controllers::show::ShowCommandController;
32use crate::presentation::cli::controllers::test::handler::TestCommandController;
33use crate::presentation::cli::controllers::validate::ValidateCommandController;
34use crate::presentation::cli::views::{UserOutput, VerbosityLevel};
35use crate::shared::clock::Clock;
36use crate::shared::SystemClock;
37
38/// Application service container
39///
40/// Holds shared services initialized during application bootstrap.
41/// Services are wrapped in `Arc<T>` for thread-safe shared ownership
42/// across the application.
43///
44/// # Example
45///
46/// ```rust
47/// use std::path::Path;
48/// use torrust_tracker_deployer_lib::bootstrap::container::Container;
49/// use torrust_tracker_deployer_lib::presentation::cli::views::VerbosityLevel;
50///
51/// let working_dir = Path::new(".");
52/// let container = Container::new(VerbosityLevel::Normal, working_dir);
53/// let user_output = container.user_output();
54/// user_output.lock().borrow_mut().success("Operation completed");
55/// ```
56#[derive(Clone)]
57pub struct Container {
58    user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
59    file_repository_factory: Arc<FileRepositoryFactory>,
60    repository: Arc<dyn EnvironmentRepository + Send + Sync>,
61    clock: Arc<dyn Clock>,
62    data_directory: Arc<Path>,
63}
64
65impl Container {
66    /// Create a new container with initialized services
67    ///
68    /// Initializes all services with specified verbosity level and working directory:
69    /// - `UserOutput` with provided `verbosity_level`
70    /// - `FileRepositoryFactory` with `DEFAULT_LOCK_TIMEOUT`
71    /// - `EnvironmentRepository` using `working_dir/data` as base directory
72    /// - `SystemClock` for time operations
73    ///
74    /// # Arguments
75    ///
76    /// * `verbosity_level` - Controls how verbose the user output will be
77    /// * `working_dir` - Base working directory for the application (repository uses `working_dir/data`)
78    ///
79    /// # Examples
80    ///
81    /// ```rust
82    /// use std::path::Path;
83    /// use torrust_tracker_deployer_lib::bootstrap::container::Container;
84    /// use torrust_tracker_deployer_lib::presentation::cli::views::VerbosityLevel;
85    ///
86    /// // For normal application use
87    /// let container = Container::new(VerbosityLevel::Normal, Path::new("."));
88    ///
89    /// // For completely silent testing
90    /// let container = Container::new(VerbosityLevel::Silent, Path::new("/tmp/test"));
91    /// ```
92    #[must_use]
93    pub fn new(verbosity_level: VerbosityLevel, working_dir: &Path) -> Self {
94        let user_output = Arc::new(ReentrantMutex::new(RefCell::new(UserOutput::new(
95            verbosity_level,
96        ))));
97        let file_repository_factory = Arc::new(FileRepositoryFactory::new(DEFAULT_LOCK_TIMEOUT));
98
99        // Create repository once for the entire application
100        let data_dir = working_dir.join("data");
101        let data_directory: Arc<Path> = Arc::from(data_dir.as_path());
102        let repository = file_repository_factory.create(data_dir);
103
104        let clock: Arc<dyn Clock> = Arc::new(SystemClock);
105
106        Self {
107            user_output,
108            file_repository_factory,
109            repository,
110            clock,
111            data_directory,
112        }
113    }
114
115    /// Get shared reference to user output service
116    ///
117    /// Returns an `Arc<ReentrantMutex<RefCell<UserOutput>>>` that can be safely cloned and shared
118    /// across threads and function calls. Use the reentrant lock to acquire access, then `borrow_mut()`
119    /// to get mutable access to the user output. The reentrant mutex prevents deadlocks when the
120    /// same thread needs to acquire the lock multiple times.
121    ///
122    /// # Example
123    ///
124    /// ```rust
125    /// use std::path::Path;
126    /// use torrust_tracker_deployer_lib::bootstrap::container::Container;
127    /// use torrust_tracker_deployer_lib::presentation::cli::views::VerbosityLevel;
128    ///
129    /// let container = Container::new(VerbosityLevel::Normal, Path::new("."));
130    /// let user_output = container.user_output();
131    /// user_output.lock().borrow_mut().success("Operation completed");
132    /// ```
133    #[must_use]
134    pub fn user_output(&self) -> Arc<ReentrantMutex<RefCell<UserOutput>>> {
135        Arc::clone(&self.user_output)
136    }
137
138    /// Get shared reference to repository factory service
139    ///
140    /// Returns an `Arc<FileRepositoryFactory>` that can be cheaply cloned and shared
141    /// across threads and function calls.
142    ///
143    /// # Example
144    ///
145    /// ```rust
146    /// use std::path::Path;
147    /// use torrust_tracker_deployer_lib::bootstrap::container::Container;
148    /// use torrust_tracker_deployer_lib::presentation::cli::views::VerbosityLevel;
149    ///
150    /// let container = Container::new(VerbosityLevel::Normal, Path::new("."));
151    /// let file_repository_factory = container.file_repository_factory();
152    /// // Use file_repository_factory to create repositories
153    /// ```
154    #[must_use]
155    pub fn file_repository_factory(&self) -> Arc<FileRepositoryFactory> {
156        Arc::clone(&self.file_repository_factory)
157    }
158
159    /// Get shared reference to repository provider
160    ///
161    /// Returns an `Arc<dyn RepositoryProvider>` that can be passed to application-layer
162    /// handlers without exposing the concrete infrastructure type.
163    #[must_use]
164    pub fn repository_provider(&self) -> Arc<dyn RepositoryProvider> {
165        Arc::clone(&self.file_repository_factory) as Arc<dyn RepositoryProvider>
166    }
167
168    /// Get shared reference to environment repository
169    ///
170    /// Returns an `Arc<dyn EnvironmentRepository>` that can be cheaply cloned and shared
171    /// across threads and function calls. The repository is initialized with the
172    /// application's base data directory during container creation.
173    ///
174    /// # Example
175    ///
176    /// ```rust
177    /// use std::path::Path;
178    /// use torrust_tracker_deployer_lib::bootstrap::container::Container;
179    /// use torrust_tracker_deployer_lib::presentation::cli::views::VerbosityLevel;
180    ///
181    /// let container = Container::new(VerbosityLevel::Normal, Path::new("."));
182    /// let repository = container.repository();
183    /// // Use repository to load/save environment state
184    /// ```
185    #[must_use]
186    pub fn repository(&self) -> Arc<dyn EnvironmentRepository + Send + Sync> {
187        Arc::clone(&self.repository)
188    }
189
190    /// Get shared reference to clock service
191    ///
192    /// Returns an `Arc<dyn Clock>` that can be cheaply cloned and shared
193    /// across threads and function calls.
194    ///
195    /// # Example
196    ///
197    /// ```rust
198    /// use std::path::Path;
199    /// use torrust_tracker_deployer_lib::bootstrap::container::Container;
200    /// use torrust_tracker_deployer_lib::presentation::cli::views::VerbosityLevel;
201    ///
202    /// let container = Container::new(VerbosityLevel::Normal, Path::new("."));
203    /// let clock = container.clock();
204    /// // Use clock for time operations
205    /// ```
206    #[must_use]
207    pub fn clock(&self) -> Arc<dyn Clock> {
208        Arc::clone(&self.clock)
209    }
210
211    /// Create a new `CreateEnvironmentCommandController`
212    #[must_use]
213    pub fn create_environment_controller(&self) -> CreateEnvironmentCommandController {
214        CreateEnvironmentCommandController::new(
215            self.repository(),
216            self.clock(),
217            &self.user_output(),
218        )
219    }
220
221    /// Create a new `CreateTemplateCommandController`
222    #[must_use]
223    pub fn create_template_controller(&self) -> CreateTemplateCommandController {
224        CreateTemplateCommandController::new(&self.user_output())
225    }
226
227    /// Create a new `CreateSchemaCommandController`
228    #[must_use]
229    pub fn create_schema_controller(&self) -> CreateSchemaCommandController {
230        CreateSchemaCommandController::new(&self.user_output())
231    }
232
233    /// Create a new `DocsCommandController`
234    #[must_use]
235    pub fn create_docs_controller(&self) -> DocsCommandController {
236        DocsCommandController::new(&self.user_output())
237    }
238
239    /// Create a new `ProvisionCommandController`
240    #[must_use]
241    pub fn create_provision_controller(&self) -> ProvisionCommandController {
242        ProvisionCommandController::new(self.repository(), self.clock(), self.user_output())
243    }
244
245    /// Create a new `DestroyCommandController`
246    #[must_use]
247    pub fn create_destroy_controller(&self) -> DestroyCommandController {
248        DestroyCommandController::new(self.repository(), self.clock(), self.user_output())
249    }
250
251    /// Create a new `PurgeCommandController`
252    #[must_use]
253    pub fn create_purge_controller(&self) -> PurgeCommandController {
254        let handler =
255            PurgeCommandHandler::new(self.repository(), (*self.data_directory).to_path_buf());
256        PurgeCommandController::new(handler, self.user_output())
257    }
258
259    /// Create a new `ConfigureCommandController`
260    #[must_use]
261    pub fn create_configure_controller(&self) -> ConfigureCommandController {
262        ConfigureCommandController::new(self.repository(), self.clock(), self.user_output())
263    }
264
265    /// Create a new `TestCommandController`
266    #[must_use]
267    pub fn create_test_controller(&self) -> TestCommandController {
268        TestCommandController::new(self.repository(), self.user_output())
269    }
270
271    /// Create a new `ValidateCommandController`
272    #[must_use]
273    pub fn create_validate_controller(&self) -> ValidateCommandController {
274        ValidateCommandController::new(self.user_output())
275    }
276
277    /// Create a new `RegisterCommandController`
278    #[must_use]
279    pub fn create_register_controller(&self) -> RegisterCommandController {
280        RegisterCommandController::new(self.repository(), self.clock(), self.user_output())
281    }
282
283    /// Create a new `ReleaseCommandController`
284    #[must_use]
285    pub fn create_release_controller(&self) -> ReleaseCommandController {
286        ReleaseCommandController::new(self.repository(), self.clock(), self.user_output())
287    }
288
289    /// Create a new `RenderCommandController`
290    #[must_use]
291    pub fn create_render_controller(&self) -> RenderCommandController {
292        RenderCommandController::new(self.repository(), self.user_output())
293    }
294
295    /// Create a new `RunCommandController`
296    #[must_use]
297    pub fn create_run_controller(&self) -> RunCommandController {
298        RunCommandController::new(self.repository(), self.clock(), self.user_output())
299    }
300
301    /// Create a new `ShowCommandController`
302    #[must_use]
303    pub fn create_show_controller(&self) -> ShowCommandController {
304        ShowCommandController::new(self.repository(), self.user_output())
305    }
306
307    /// Create a new `ExistsCommandController`
308    #[must_use]
309    pub fn create_exists_controller(&self) -> ExistsCommandController {
310        ExistsCommandController::new(self.repository(), self.user_output())
311    }
312
313    /// Create a new `ListCommandController`
314    #[must_use]
315    pub fn create_list_controller(&self) -> ListCommandController {
316        ListCommandController::new(
317            self.repository_provider(),
318            self.data_directory(),
319            self.user_output(),
320        )
321    }
322
323    /// Get shared reference to data directory path
324    ///
325    /// Returns an `Arc<Path>` pointing to the data directory where
326    /// environment state files are stored.
327    #[must_use]
328    pub fn data_directory(&self) -> Arc<Path> {
329        Arc::clone(&self.data_directory)
330    }
331}
332
333impl Default for Container {
334    fn default() -> Self {
335        Self::new(VerbosityLevel::Normal, Path::new("."))
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use tempfile::TempDir;
343
344    #[test]
345    fn it_should_create_container_with_all_services() {
346        let temp_dir = TempDir::new().unwrap();
347        let container = Container::new(VerbosityLevel::Normal, temp_dir.path());
348
349        // Verify we can get all services
350        let user_output = container.user_output();
351        let file_repository_factory = container.file_repository_factory();
352        let repository = container.repository();
353        let clock = container.clock();
354
355        assert!(Arc::strong_count(&user_output) >= 1);
356        assert!(Arc::strong_count(&file_repository_factory) >= 1);
357        assert!(Arc::strong_count(&repository) >= 1);
358        assert!(Arc::strong_count(&clock) >= 1);
359    }
360
361    #[test]
362    fn it_should_return_cloned_arc_on_file_repository_factory_access() {
363        let temp_dir = TempDir::new().unwrap();
364        let container = Container::new(VerbosityLevel::Normal, temp_dir.path());
365        let factory1 = container.file_repository_factory();
366        let factory2 = container.file_repository_factory();
367
368        // Both should point to the same FileRepositoryFactory instance
369        assert!(Arc::ptr_eq(&factory1, &factory2));
370    }
371
372    #[test]
373    fn it_should_return_cloned_arc_on_repository_access() {
374        let temp_dir = TempDir::new().unwrap();
375        let container = Container::new(VerbosityLevel::Normal, temp_dir.path());
376        let repo1 = container.repository();
377        let repo2 = container.repository();
378
379        // Both should point to the same Repository instance
380        assert!(Arc::ptr_eq(&repo1, &repo2));
381    }
382
383    #[test]
384    fn it_should_return_cloned_arc_on_clock_access() {
385        let temp_dir = TempDir::new().unwrap();
386        let container = Container::new(VerbosityLevel::Normal, temp_dir.path());
387        let clock1 = container.clock();
388        let clock2 = container.clock();
389
390        // Both should point to the same Clock instance
391        assert!(Arc::ptr_eq(&clock1, &clock2));
392    }
393
394    #[test]
395    fn it_should_return_cloned_arc_on_user_output_access() {
396        let temp_dir = TempDir::new().unwrap();
397        let container = Container::new(VerbosityLevel::Normal, temp_dir.path());
398        let user_output1 = container.user_output();
399        let user_output2 = container.user_output();
400
401        // Both should point to the same UserOutput instance
402        assert!(Arc::ptr_eq(&user_output1, &user_output2));
403    }
404
405    #[test]
406    fn it_should_be_clonable() {
407        let temp_dir = TempDir::new().unwrap();
408        let container1 = Container::new(VerbosityLevel::Normal, temp_dir.path());
409        let container2 = container1.clone();
410
411        // Cloned containers should share all services
412        let user_output1 = container1.user_output();
413        let user_output2 = container2.user_output();
414        assert!(Arc::ptr_eq(&user_output1, &user_output2));
415
416        let factory1 = container1.file_repository_factory();
417        let factory2 = container2.file_repository_factory();
418        assert!(Arc::ptr_eq(&factory1, &factory2));
419
420        let repo1 = container1.repository();
421        let repo2 = container2.repository();
422        assert!(Arc::ptr_eq(&repo1, &repo2));
423
424        let clock1 = container1.clock();
425        let clock2 = container2.clock();
426        assert!(Arc::ptr_eq(&clock1, &clock2));
427    }
428
429    #[test]
430    fn it_should_create_container_with_silent_verbosity_for_tests() {
431        let temp_dir = TempDir::new().unwrap();
432        let container = Container::new(VerbosityLevel::Silent, temp_dir.path());
433
434        // All services should be available
435        let user_output = container.user_output();
436        let file_repository_factory = container.file_repository_factory();
437        let repository = container.repository();
438        let clock = container.clock();
439
440        assert!(Arc::strong_count(&user_output) >= 1);
441        assert!(Arc::strong_count(&file_repository_factory) >= 1);
442        assert!(Arc::strong_count(&repository) >= 1);
443        assert!(Arc::strong_count(&clock) >= 1);
444
445        // The container should work with any verbosity level, including Silent for tests
446        // Silent mode will suppress all output, making tests clean
447    }
448
449    #[test]
450    fn it_should_create_container_with_different_verbosity_levels() {
451        let temp_dir = TempDir::new().unwrap();
452
453        // Test all available verbosity levels
454        let levels = [
455            VerbosityLevel::Silent,
456            VerbosityLevel::Quiet,
457            VerbosityLevel::Normal,
458            VerbosityLevel::Verbose,
459            VerbosityLevel::VeryVerbose,
460            VerbosityLevel::Debug,
461        ];
462
463        for level in &levels {
464            let container = Container::new(*level, temp_dir.path());
465
466            // All services should be available regardless of verbosity level
467            let user_output = container.user_output();
468            let file_repository_factory = container.file_repository_factory();
469            let repository = container.repository();
470            let clock = container.clock();
471
472            assert!(Arc::strong_count(&user_output) >= 1);
473            assert!(Arc::strong_count(&file_repository_factory) >= 1);
474            assert!(Arc::strong_count(&repository) >= 1);
475            assert!(Arc::strong_count(&clock) >= 1);
476        }
477    }
478}