Skip to main content

torrust_tracker_deployer_lib/presentation/cli/views/messages/
steps.rs

1//! Steps message type for sequential instructions
2
3use super::super::{Channel, OutputMessage, Theme, VerbosityLevel};
4
5/// Steps message for sequential instructions
6///
7/// Steps messages display numbered lists of sequential items.
8/// Useful for showing action items or instructions.
9///
10/// # Examples
11///
12/// Simple constructor for cases where you have all items upfront:
13///
14/// ```rust
15/// use torrust_tracker_deployer_lib::presentation::cli::views::StepsMessage;
16///
17/// let message = StepsMessage::new("Next steps:", vec![
18///     "Edit the configuration file".to_string(),
19///     "Review the settings".to_string(),
20/// ]);
21/// ```
22///
23/// Builder pattern for dynamic construction or better readability:
24///
25/// ```rust
26/// use torrust_tracker_deployer_lib::presentation::cli::views::StepsMessage;
27///
28/// let message = StepsMessage::builder("Next steps:")
29///     .add("Edit the configuration file")
30///     .add("Review the settings")
31///     .build();
32/// ```
33pub struct StepsMessage {
34    /// The title for the steps list
35    pub title: String,
36    /// The list of step items
37    pub items: Vec<String>,
38}
39
40impl StepsMessage {
41    /// Create a new steps message with the given title and items
42    ///
43    /// This is a convenience constructor for simple cases where you have
44    /// all items upfront. For dynamic construction or better readability,
45    /// consider using `StepsMessage::builder()` instead.
46    ///
47    /// # Examples
48    ///
49    /// ```rust
50    /// use torrust_tracker_deployer_lib::presentation::cli::views::StepsMessage;
51    ///
52    /// let msg = StepsMessage::new("Next steps:", vec![
53    ///     "Edit config".to_string(),
54    ///     "Run tests".to_string(),
55    /// ]);
56    /// ```
57    #[must_use]
58    pub fn new(title: impl Into<String>, items: Vec<String>) -> Self {
59        Self {
60            title: title.into(),
61            items,
62        }
63    }
64
65    /// Create a builder for constructing steps messages with a fluent API
66    ///
67    /// The builder pattern is useful when:
68    /// - Adding items dynamically
69    /// - You want self-documenting, readable code
70    /// - Building the message in multiple steps
71    ///
72    /// # Examples
73    ///
74    /// ```rust
75    /// use torrust_tracker_deployer_lib::presentation::cli::views::StepsMessage;
76    ///
77    /// let msg = StepsMessage::builder("Next steps:")
78    ///     .add("Edit configuration")
79    ///     .add("Review settings")
80    ///     .build();
81    /// ```
82    #[must_use]
83    pub fn builder(title: impl Into<String>) -> StepsMessageBuilder {
84        StepsMessageBuilder::new(title)
85    }
86}
87
88impl OutputMessage for StepsMessage {
89    fn format(&self, _theme: &Theme) -> String {
90        use std::fmt::Write;
91
92        let mut output = format!("{}\n", self.title);
93        for (idx, step) in self.items.iter().enumerate() {
94            writeln!(&mut output, "{}. {}", idx + 1, step).ok();
95        }
96        output
97    }
98
99    fn required_verbosity(&self) -> VerbosityLevel {
100        VerbosityLevel::Normal
101    }
102
103    fn channel(&self) -> Channel {
104        Channel::Stderr
105    }
106
107    fn type_name(&self) -> &'static str {
108        "StepsMessage"
109    }
110}
111
112/// Builder for constructing `StepsMessage` with a fluent API
113///
114/// Provides a consuming builder pattern for constructing step messages
115/// with optional customization. Use this for complex cases where items
116/// are added dynamically or for improved readability. Simple cases can
117/// use `StepsMessage::new()` directly.
118///
119/// # Examples
120///
121/// ```rust
122/// use torrust_tracker_deployer_lib::presentation::cli::views::StepsMessage;
123///
124/// let message = StepsMessage::builder("Next steps:")
125///     .add("Edit configuration")
126///     .add("Review settings")
127///     .add("Deploy changes")
128///     .build();
129/// ```
130///
131/// Empty builders are valid:
132///
133/// ```rust
134/// use torrust_tracker_deployer_lib::presentation::cli::views::StepsMessage;
135///
136/// let message = StepsMessage::builder("Title").build();
137/// ```
138pub struct StepsMessageBuilder {
139    title: String,
140    items: Vec<String>,
141}
142
143impl StepsMessageBuilder {
144    /// Create a new builder with the given title
145    ///
146    /// # Examples
147    ///
148    /// ```rust
149    /// use torrust_tracker_deployer_lib::presentation::cli::views::StepsMessageBuilder;
150    ///
151    /// let builder = StepsMessageBuilder::new("My steps:");
152    /// ```
153    #[must_use]
154    pub fn new(title: impl Into<String>) -> Self {
155        Self {
156            title: title.into(),
157            items: Vec::new(),
158        }
159    }
160
161    /// Add a step to the list (consuming self for method chaining)
162    ///
163    /// This method consumes the builder and returns it, enabling
164    /// method chaining in a fluent API style.
165    ///
166    /// # Examples
167    ///
168    /// ```rust
169    /// use torrust_tracker_deployer_lib::presentation::cli::views::StepsMessage;
170    ///
171    /// let message = StepsMessage::builder("Steps:")
172    ///     .add("First step")
173    ///     .add("Second step")
174    ///     .build();
175    /// ```
176    #[must_use]
177    #[allow(clippy::should_implement_trait)]
178    pub fn add(mut self, step: impl Into<String>) -> Self {
179        self.items.push(step.into());
180        self
181    }
182
183    /// Build the final `StepsMessage`
184    ///
185    /// Consumes the builder and produces the final message.
186    ///
187    /// # Examples
188    ///
189    /// ```rust
190    /// use torrust_tracker_deployer_lib::presentation::cli::views::StepsMessage;
191    ///
192    /// let message = StepsMessage::builder("Steps:")
193    ///     .add("Step 1")
194    ///     .build();
195    /// ```
196    #[must_use]
197    pub fn build(self) -> StepsMessage {
198        StepsMessage {
199            title: self.title,
200            items: self.items,
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn it_should_format_numbered_list_when_displaying_steps() {
211        let theme = Theme::emoji();
212        let message = StepsMessage {
213            title: "Next steps:".to_string(),
214            items: vec!["First step".to_string(), "Second step".to_string()],
215        };
216
217        let formatted = message.format(&theme);
218
219        assert_eq!(formatted, "Next steps:\n1. First step\n2. Second step\n");
220    }
221
222    #[test]
223    fn it_should_require_normal_verbosity_when_displaying_steps() {
224        let message = StepsMessage {
225            title: "Next steps:".to_string(),
226            items: vec!["First step".to_string()],
227        };
228
229        assert_eq!(message.required_verbosity(), VerbosityLevel::Normal);
230    }
231
232    #[test]
233    fn it_should_use_stderr_channel_when_displaying_steps() {
234        let message = StepsMessage {
235            title: "Next steps:".to_string(),
236            items: vec!["First step".to_string()],
237        };
238
239        assert_eq!(message.channel(), Channel::Stderr);
240    }
241
242    #[test]
243    fn it_should_build_steps_with_fluent_api() {
244        let message = StepsMessage::builder("Title")
245            .add("Step 1")
246            .add("Step 2")
247            .add("Step 3")
248            .build();
249
250        assert_eq!(message.title, "Title");
251        assert_eq!(message.items, vec!["Step 1", "Step 2", "Step 3"]);
252    }
253
254    #[test]
255    fn it_should_create_simple_steps_directly() {
256        let message = StepsMessage::new("Title", vec!["Step 1".to_string(), "Step 2".to_string()]);
257
258        assert_eq!(message.title, "Title");
259        assert_eq!(message.items, vec!["Step 1", "Step 2"]);
260    }
261
262    #[test]
263    fn it_should_build_empty_steps() {
264        let message = StepsMessage::builder("Title").build();
265
266        assert_eq!(message.title, "Title");
267        assert!(message.items.is_empty());
268    }
269
270    #[test]
271    fn it_should_build_single_step() {
272        let message = StepsMessage::builder("Title").add("Single step").build();
273
274        assert_eq!(message.title, "Title");
275        assert_eq!(message.items, vec!["Single step"]);
276    }
277
278    #[test]
279    fn it_should_accept_string_types_in_builder() {
280        let message = StepsMessage::builder("Title")
281            .add("String literal")
282            .add(String::from("Owned string"))
283            .add("Another literal".to_string())
284            .build();
285
286        assert_eq!(message.items.len(), 3);
287    }
288
289    #[test]
290    fn it_should_accept_string_types_in_constructor() {
291        let message =
292            StepsMessage::new("Title", vec!["Step 1".to_string(), String::from("Step 2")]);
293
294        assert_eq!(message.items.len(), 2);
295    }
296
297    #[test]
298    fn it_should_format_builder_messages_correctly() {
299        let theme = Theme::emoji();
300        let message = StepsMessage::builder("Next steps:")
301            .add("Configure")
302            .add("Deploy")
303            .build();
304
305        let formatted = message.format(&theme);
306        assert!(formatted.contains("Next steps:"));
307        assert!(formatted.contains("1. Configure"));
308        assert!(formatted.contains("2. Deploy"));
309    }
310
311    #[test]
312    fn it_should_maintain_backward_compatibility_for_steps() {
313        // Old way: direct construction
314        let old_message = StepsMessage {
315            title: "Steps".to_string(),
316            items: vec!["Step 1".to_string()],
317        };
318
319        // New way: constructor
320        let new_message = StepsMessage::new("Steps", vec!["Step 1".to_string()]);
321
322        // Should produce identical results
323        assert_eq!(old_message.title, new_message.title);
324        assert_eq!(old_message.items, new_message.items);
325    }
326
327    #[test]
328    fn it_should_handle_many_items_in_builder() {
329        let mut builder = StepsMessage::builder("Many steps");
330        for i in 1..=100 {
331            builder = builder.add(format!("Step {i}"));
332        }
333        let message = builder.build();
334
335        assert_eq!(message.items.len(), 100);
336        assert_eq!(message.items[0], "Step 1");
337        assert_eq!(message.items[99], "Step 100");
338    }
339}