torrust_tracker_deployer_lib/application/traits/progress.rs
1//! Progress reporting interface for command workflows
2//!
3//! This module defines the `CommandProgressListener` trait that enables
4//! application-layer command handlers to report progress to the user interface
5//! without depending on presentation-layer types.
6//!
7//! # DDD Layer Placement
8//!
9//! - **Defined in**: Application layer (`src/application/traits/`)
10//! - **Implemented in**: Presentation layer (`src/presentation/`)
11//! - **Dependency direction**: Presentation → Application (correct)
12//!
13//! The trait lives here because it's a use-case concern: progress reporting
14//! is about orchestrating steps in a command workflow, which is the application
15//! layer's responsibility. The presentation layer implements the trait to
16//! translate progress events into user-facing output.
17//!
18//! # Verbosity Mapping
19//!
20//! The listener methods map to verbosity levels (but the listener itself
21//! does not know about verbosity — that's the implementation's concern):
22//!
23//! - `on_step_started` / `on_step_completed` → Verbose (`-v`)
24//! - `on_detail` → `VeryVerbose` (`-vv`)
25//! - `on_debug` → Debug (`-vvv`)
26//!
27//! The application layer reports everything; the presentation layer filters
28//! based on the user's chosen verbosity level.
29//!
30//! # Example
31//!
32//! ```rust,ignore
33//! use torrust_tracker_deployer_lib::application::traits::CommandProgressListener;
34//!
35//! async fn execute(
36//! &self,
37//! env_name: &EnvironmentName,
38//! listener: Option<&dyn CommandProgressListener>,
39//! ) -> Result<(), Error> {
40//! if let Some(l) = listener {
41//! l.on_step_started(1, 3, "Rendering templates");
42//! }
43//! // ... perform step ...
44//! if let Some(l) = listener {
45//! l.on_step_completed(1, "Rendering templates");
46//! }
47//! Ok(())
48//! }
49//! ```
50
51/// A listener for reporting command progress to the user interface.
52///
53/// This trait is defined in the application layer and implemented in the
54/// presentation layer, following the Dependency Inversion Principle.
55/// The application layer depends on this abstraction, not on concrete
56/// UI implementations.
57///
58/// # Design Rationale
59///
60/// - **Generic**: One trait serves all commands (provision, configure, etc.)
61/// - **String-based**: Receives human-readable descriptions, not command-specific enums
62/// - **Optional**: Handlers accept `Option<&dyn CommandProgressListener>` for backward compatibility
63/// - **Filtering-agnostic**: Reports everything; the implementation decides what to display
64pub trait CommandProgressListener: Send + Sync {
65 /// Called when a step begins execution.
66 ///
67 /// # Arguments
68 ///
69 /// * `step_number` - 1-based step index within the current workflow
70 /// * `total_steps` - Total number of steps in the workflow
71 /// * `description` - Human-readable step description
72 fn on_step_started(&self, step_number: usize, total_steps: usize, description: &str);
73
74 /// Called when a step completes successfully.
75 ///
76 /// # Arguments
77 ///
78 /// * `step_number` - 1-based step index within the current workflow
79 /// * `description` - Human-readable step description
80 fn on_step_completed(&self, step_number: usize, description: &str);
81
82 /// Reports a contextual detail about the current operation.
83 ///
84 /// Intended for intermediate results, file paths, counts, retry attempts, etc.
85 /// Maps to `VeryVerbose` (`-vv`) level in the presentation implementation.
86 ///
87 /// # Arguments
88 ///
89 /// * `message` - Human-readable detail message
90 fn on_detail(&self, message: &str);
91
92 /// Reports a technical/debug detail about the current operation.
93 ///
94 /// Intended for commands executed, exit codes, raw output, etc.
95 /// Maps to Debug (`-vvv`) level in the presentation implementation.
96 ///
97 /// # Arguments
98 ///
99 /// * `message` - Technical detail message
100 fn on_debug(&self, message: &str);
101}
102
103/// A no-op listener that discards all progress events.
104///
105/// Used when progress reporting is not needed, such as in tests
106/// or when the caller does not provide a listener.
107///
108/// # Examples
109///
110/// ```rust
111/// use torrust_tracker_deployer_lib::application::traits::NullProgressListener;
112/// use torrust_tracker_deployer_lib::application::traits::CommandProgressListener;
113///
114/// let listener = NullProgressListener;
115/// listener.on_step_started(1, 3, "Step one");
116/// listener.on_step_completed(1, "Step one");
117/// listener.on_detail("some detail");
118/// listener.on_debug("some debug info");
119/// // All calls are no-ops
120/// ```
121pub struct NullProgressListener;
122
123impl CommandProgressListener for NullProgressListener {
124 fn on_step_started(&self, _step_number: usize, _total_steps: usize, _description: &str) {}
125 fn on_step_completed(&self, _step_number: usize, _description: &str) {}
126 fn on_detail(&self, _message: &str) {}
127 fn on_debug(&self, _message: &str) {}
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn it_should_accept_null_listener_without_panicking() {
136 let listener = NullProgressListener;
137 listener.on_step_started(1, 9, "Rendering templates");
138 listener.on_step_completed(1, "Rendering templates");
139 listener.on_detail("Template directory: build/test/tofu");
140 listener.on_debug("Command: tofu init");
141 }
142
143 #[test]
144 fn it_should_work_as_trait_object() {
145 let listener: &dyn CommandProgressListener = &NullProgressListener;
146 listener.on_step_started(1, 3, "First step");
147 listener.on_step_completed(1, "First step");
148 listener.on_detail("detail");
149 listener.on_debug("debug");
150 }
151
152 #[test]
153 fn it_should_work_as_optional_trait_object() {
154 let listener: Option<&dyn CommandProgressListener> = Some(&NullProgressListener);
155 if let Some(l) = listener {
156 l.on_step_started(1, 3, "First step");
157 }
158
159 let no_listener: Option<&dyn CommandProgressListener> = None;
160 if let Some(l) = no_listener {
161 l.on_step_started(1, 3, "Should not reach here");
162 }
163 }
164}