Skip to main content

wx_uploader/
output.rs

1//! Output formatting utilities
2//!
3//! This module provides centralized formatting for console output to ensure
4//! consistent styling and reduce code duplication across the application.
5
6use colored::*;
7use std::path::Path;
8
9/// Trait for formatting console output with consistent styling
10pub trait OutputFormatter {
11    /// Formats a success message with green checkmark
12    fn success(&self, message: &str) -> String;
13
14    /// Formats an error message with red X
15    fn error(&self, message: &str) -> String;
16
17    /// Formats a warning message with yellow warning symbol
18    fn warning(&self, message: &str) -> String;
19
20    /// Formats an info message with blue info symbol
21    fn info(&self, message: &str) -> String;
22
23    /// Formats a progress message with blue arrow
24    fn progress(&self, message: &str) -> String;
25
26    /// Formats a skip message with yellow skip symbol
27    fn skip(&self, message: &str) -> String;
28
29    /// Formats a generation message with cyan art symbol
30    fn generation(&self, message: &str) -> String;
31
32    /// Prints a success message
33    fn print_success(&self, message: &str) {
34        println!("{}", self.success(message));
35    }
36
37    /// Prints an error message
38    fn print_error(&self, message: &str) {
39        eprintln!("{}", self.error(message));
40    }
41
42    /// Prints a warning message
43    fn print_warning(&self, message: &str) {
44        println!("{}", self.warning(message));
45    }
46
47    /// Prints an info message
48    fn print_info(&self, message: &str) {
49        println!("{}", self.info(message));
50    }
51
52    /// Prints a progress message
53    fn print_progress(&self, message: &str) {
54        println!("{}", self.progress(message));
55    }
56
57    /// Prints a skip message
58    fn print_skip(&self, message: &str) {
59        println!("{}", self.skip(message));
60    }
61
62    /// Prints a generation message
63    fn print_generation(&self, message: &str) {
64        println!("{}", self.generation(message));
65    }
66}
67
68/// Standard console output formatter with colored output
69#[derive(Debug, Clone, Copy)]
70pub struct ConsoleFormatter;
71
72impl OutputFormatter for ConsoleFormatter {
73    fn success(&self, message: &str) -> String {
74        format!("{} {}", "✓".bright_green(), message.green())
75    }
76
77    fn error(&self, message: &str) -> String {
78        format!("{} {}", "✗".bright_red(), message.red())
79    }
80
81    fn warning(&self, message: &str) -> String {
82        format!("{} {}", "⚠".bright_yellow(), message.yellow())
83    }
84
85    fn info(&self, message: &str) -> String {
86        format!("{} {}", "ℹ".bright_blue(), message.dimmed())
87    }
88
89    fn progress(&self, message: &str) -> String {
90        format!("{} {}", "🔄".bright_blue(), message.bright_white())
91    }
92
93    fn skip(&self, message: &str) -> String {
94        format!("{} {}", "⏭".bright_yellow(), message.dimmed())
95    }
96
97    fn generation(&self, message: &str) -> String {
98        format!("{} {}", "🎨".bright_cyan(), message.bright_white())
99    }
100}
101
102/// Extensions for file path formatting
103pub trait FilePathFormatter {
104    /// Formats a file operation message with consistent path display
105    fn format_file_operation(&self, operation: &str, path: &Path) -> String;
106
107    /// Formats an upload success message
108    fn format_upload_success(&self, path: &Path) -> String;
109
110    /// Formats an upload failure message
111    fn format_upload_failure(&self, path: &Path) -> String;
112
113    /// Formats a skip message for already published files
114    fn format_skip_published(&self, path: &Path) -> String;
115
116    /// Formats a cover generation message
117    fn format_cover_generation(&self, path: &Path) -> String;
118
119    /// Formats a cover generation success message
120    fn format_cover_success(&self, filename: &str) -> String;
121
122    /// Formats a cover generation failure message
123    fn format_cover_failure(&self) -> String;
124
125    /// Formats an image prompt message
126    fn format_image_prompt(&self, prompt: &str) -> String;
127
128    /// Formats a target path message
129    fn format_target_path(&self, path: &Path) -> String;
130
131    /// Formats an image save success message
132    fn format_image_saved(&self, path: &Path) -> String;
133}
134
135impl<T: OutputFormatter> FilePathFormatter for T {
136    fn format_file_operation(&self, operation: &str, path: &Path) -> String {
137        format!("{}: {}", operation, path.display())
138    }
139
140    fn format_upload_success(&self, path: &Path) -> String {
141        self.success(&format!("uploaded: {}", path.display()))
142    }
143
144    fn format_upload_failure(&self, path: &Path) -> String {
145        self.error(&format!("failed: {}", path.display()))
146    }
147
148    fn format_skip_published(&self, path: &Path) -> String {
149        self.skip(&format!("skipped: {}", path.display()))
150    }
151
152    fn format_cover_generation(&self, path: &Path) -> String {
153        self.generation(&format!("generating cover: {}", path.display()))
154    }
155
156    fn format_cover_success(&self, filename: &str) -> String {
157        format!(
158            "{} {} {}",
159            "✨".bright_green(),
160            "cover generated:".green(),
161            filename
162        )
163    }
164
165    fn format_cover_failure(&self) -> String {
166        self.warning("cover generation failed, continuing...")
167    }
168
169    fn format_image_prompt(&self, prompt: &str) -> String {
170        format!(
171            "  {} {}",
172            "→".bright_blue(),
173            format!("Image prompt: {}", prompt).bright_white()
174        )
175    }
176
177    fn format_target_path(&self, path: &Path) -> String {
178        format!("  {} Target path: {}", "📍".bright_cyan(), path.display())
179    }
180
181    fn format_image_saved(&self, path: &Path) -> String {
182        format!(
183            "  {} Image saved to: {}",
184            "💾".bright_green(),
185            path.display()
186        )
187    }
188}
189
190/// API error formatter for consistent error reporting
191pub trait ApiErrorFormatter {
192    /// Formats an OpenAI API error message
193    fn format_openai_error(&self, status: u16, response: &str, endpoint: &str) -> String;
194
195    /// Formats a general API error
196    fn format_api_error(&self, service: &str, error: &str) -> String;
197
198    /// Formats image generation failure
199    fn format_image_generation_failure(&self, error: &str) -> String;
200
201    /// Formats image download failure
202    fn format_image_download_failure(&self, error: &str) -> String;
203}
204
205impl<T: OutputFormatter> ApiErrorFormatter for T {
206    fn format_openai_error(&self, status: u16, response: &str, endpoint: &str) -> String {
207        format!(
208            "  {} OpenAI API Error:\n    Status: {}\n    Response: {}\n    Endpoint: {}",
209            "⚠".bright_yellow(),
210            status,
211            response,
212            endpoint
213        )
214    }
215
216    fn format_api_error(&self, service: &str, error: &str) -> String {
217        format!("  {} {} API Error: {}", "❌".bright_red(), service, error)
218    }
219
220    fn format_image_generation_failure(&self, error: &str) -> String {
221        format!(
222            "  {} Failed to generate image: {}",
223            "❌".bright_red(),
224            error
225        )
226    }
227
228    fn format_image_download_failure(&self, error: &str) -> String {
229        format!(
230            "  {} Failed to download/save image: {}",
231            "❌".bright_red(),
232            error
233        )
234    }
235}
236
237/// Global formatter instance for consistent usage across the application
238pub const FORMATTER: ConsoleFormatter = ConsoleFormatter;
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use std::path::PathBuf;
244
245    #[test]
246    fn test_console_formatter_success() {
247        let formatter = ConsoleFormatter;
248        let message = formatter.success("Operation completed");
249        assert!(message.contains("✓"));
250        assert!(message.contains("Operation completed"));
251    }
252
253    #[test]
254    fn test_console_formatter_error() {
255        let formatter = ConsoleFormatter;
256        let message = formatter.error("Operation failed");
257        assert!(message.contains("✗"));
258        assert!(message.contains("Operation failed"));
259    }
260
261    #[test]
262    fn test_console_formatter_warning() {
263        let formatter = ConsoleFormatter;
264        let message = formatter.warning("Warning message");
265        assert!(message.contains("⚠"));
266        assert!(message.contains("Warning message"));
267    }
268
269    #[test]
270    fn test_file_path_formatter() {
271        let formatter = ConsoleFormatter;
272        let path = PathBuf::from("/path/to/file.md");
273
274        let upload_success = formatter.format_upload_success(&path);
275        assert!(upload_success.contains("uploaded"));
276        assert!(upload_success.contains("file.md"));
277
278        let upload_failure = formatter.format_upload_failure(&path);
279        assert!(upload_failure.contains("failed"));
280        assert!(upload_failure.contains("file.md"));
281    }
282
283    #[test]
284    fn test_api_error_formatter() {
285        let formatter = ConsoleFormatter;
286
287        let openai_error = formatter.format_openai_error(429, "Rate limit", "/endpoint");
288        assert!(openai_error.contains("OpenAI API Error"));
289        assert!(openai_error.contains("429"));
290        assert!(openai_error.contains("Rate limit"));
291        assert!(openai_error.contains("/endpoint"));
292
293        let api_error = formatter.format_api_error("WeChat", "Authentication failed");
294        assert!(api_error.contains("WeChat API Error"));
295        assert!(api_error.contains("Authentication failed"));
296    }
297
298    #[test]
299    fn test_cover_generation_messages() {
300        let formatter = ConsoleFormatter;
301        let path = PathBuf::from("/path/to/article.md");
302
303        let generation_msg = formatter.format_cover_generation(&path);
304        assert!(generation_msg.contains("generating cover"));
305        assert!(generation_msg.contains("article.md"));
306
307        let success_msg = formatter.format_cover_success("cover_image.png");
308        assert!(success_msg.contains("cover generated"));
309        assert!(success_msg.contains("cover_image.png"));
310
311        let failure_msg = formatter.format_cover_failure();
312        assert!(failure_msg.contains("cover generation failed"));
313    }
314}