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}