ggen_utils/
alert.rs

1//! Alert Helpers
2//!
3//! Provides macros and functions for emitting visual alerts (problem indicators)
4//! similar to chicago-tdd-tools alert macros. Alerts make problems immediately visible
5//! and provide actionable guidance for resolution.
6//!
7//! ## Usage
8//!
9//! ```rust
10//! use ggen_utils::alert_critical;
11//! use ggen_utils::alert_warning;
12//! use ggen_utils::alert_info;
13//!
14//! // Critical alert - must stop immediately
15//! alert_critical!("Docker daemon is not running", "Start Docker Desktop");
16//!
17//! // Warning alert - should stop
18//! alert_warning!("Container operation failed", "Check container state");
19//!
20//! // Info alert - informational
21//! alert_info!("Container started successfully");
22//! ```
23//!
24//! ## Alert Levels
25//!
26//! - **Critical** (🚨): Must stop immediately - cannot proceed
27//! - **Warning** (âš ī¸): Should stop - investigate before proceeding
28//! - **Info** (â„šī¸): Informational - no action required
29//! - **Success** (✅): Success indicator - operation completed
30//! - **Debug** (🔍): Debug information - detailed diagnostics
31//!
32//! ## Integration with Slog
33//!
34//! When slog logger is initialized, alerts use structured logging.
35//! When slog is not available, alerts fall back to eprintln! with emoji formatting.
36
37use std::io::{self, Write};
38
39/// Emit a critical alert (🚨)
40///
41/// Critical alerts indicate problems that must stop work immediately.
42/// Use for errors that prevent further execution.
43///
44/// # Arguments
45///
46/// * `message` - The error message
47/// * `fix` - Suggested fix (optional)
48///
49/// # Example
50///
51/// ```rust
52/// use ggen_utils::alert_critical;
53///
54/// alert_critical!("Docker daemon is not running", "Start Docker Desktop");
55/// ```
56#[macro_export]
57macro_rules! alert_critical {
58    ($message:expr, $fix:expr, $($action:expr),+) => {
59        {
60            let actions: Vec<String> = vec![$($action.to_string()),+];
61            let action_str = actions.join("\n   📋 ");
62            $crate::alert::_alert_critical_impl($message, Some(&format!("{}\n   📋 {}", $fix, action_str)));
63        }
64    };
65    ($message:expr, $fix:expr) => {
66        $crate::alert::_alert_critical_impl($message, Some($fix));
67    };
68    ($format_str:expr, $($arg:expr),+ $(,)?) => {
69        {
70            let msg = format!($format_str, $($arg),+);
71            $crate::alert::_alert_critical_impl(&msg, None::<&str>);
72        }
73    };
74    ($message:expr) => {
75        $crate::alert::_alert_critical_impl($message, None::<&str>);
76    };
77}
78
79/// Emit a warning alert (âš ī¸)
80///
81/// Warning alerts indicate problems that should stop work.
82/// Use for errors that may cause issues but don't prevent execution.
83///
84/// # Arguments
85///
86/// * `message` - The warning message
87/// * `fix` - Suggested fix (optional)
88///
89/// # Example
90///
91/// ```rust
92/// use ggen_utils::alert_warning;
93///
94/// alert_warning!("Container operation failed", "Check container state");
95/// ```
96#[macro_export]
97macro_rules! alert_warning {
98    ($message:expr, $fix:expr) => {
99        $crate::alert::_alert_warning_impl($message, Some($fix));
100    };
101    ($format_str:expr, $($arg:expr),+ $(,)?) => {
102        {
103            let msg = format!($format_str, $($arg),+);
104            $crate::alert::_alert_warning_impl(&msg, None::<&str>);
105        }
106    };
107    ($message:expr) => {
108        $crate::alert::_alert_warning_impl($message, None::<&str>);
109    };
110}
111
112/// Emit an info alert (â„šī¸)
113///
114/// Info alerts provide informational messages.
115/// Use for status updates and non-critical information.
116///
117/// # Arguments
118///
119/// * `message` - The info message
120///
121/// # Example
122///
123/// ```rust
124/// use ggen_utils::alert_info;
125///
126/// alert_info!("Container started successfully");
127/// ```
128#[macro_export]
129macro_rules! alert_info {
130    ($message:expr) => {
131        $crate::alert::_alert_info_impl($message);
132    };
133    ($($arg:tt)*) => {
134        {
135            let msg = format!($($arg)*);
136            $crate::alert::_alert_info_impl(&msg);
137        }
138    };
139}
140
141/// Emit a success alert (✅)
142///
143/// Success alerts indicate successful operations.
144/// Use to confirm operations completed successfully.
145///
146/// # Arguments
147///
148/// * `message` - The success message
149///
150/// # Example
151///
152/// ```rust
153/// use ggen_utils::alert_success;
154///
155/// alert_success!("Container started successfully");
156/// ```
157#[macro_export]
158macro_rules! alert_success {
159    ($message:expr) => {
160        $crate::alert::_alert_success_impl($message);
161    };
162    ($($arg:tt)*) => {
163        {
164            let msg = format!($($arg)*);
165            $crate::alert::_alert_success_impl(&msg);
166        }
167    };
168}
169
170/// Emit a debug alert (🔍)
171///
172/// Debug alerts provide detailed debugging information.
173/// Use for detailed diagnostic information during development.
174///
175/// # Arguments
176///
177/// * `message` - The debug message
178///
179/// # Example
180///
181/// ```rust
182/// use ggen_utils::alert_debug;
183///
184/// alert_debug!("Container state: {:?}", container_state);
185/// ```
186#[macro_export]
187macro_rules! alert_debug {
188    ($message:expr) => {
189        $crate::alert::_alert_debug_impl($message);
190    };
191    ($($arg:tt)*) => {
192        {
193            let msg = format!($($arg)*);
194            $crate::alert::_alert_debug_impl(&msg);
195        }
196    };
197}
198
199/// Emit an alert with custom severity
200///
201/// Allows emitting custom alerts with user-defined severity levels.
202///
203/// # Arguments
204///
205/// * `severity` - Severity emoji (🚨, âš ī¸, â„šī¸, ✅, 🔍)
206/// * `message` - The message
207/// * `stop` - Stop message (optional)
208/// * `fix` - Fix suggestion (optional)
209///
210/// # Example
211///
212/// ```rust
213/// use ggen_utils::alert;
214///
215/// alert!("🚨", "Custom critical error", "STOP: Cannot proceed", "FIX: Resolve issue");
216/// ```
217#[macro_export]
218macro_rules! alert {
219    ($severity:expr, $message:expr) => {
220        $crate::alert::_alert_custom_impl($severity, $message, None::<&str>, None::<&str>);
221    };
222    ($severity:expr, $message:expr, $stop:expr, $fix:expr) => {
223        $crate::alert::_alert_custom_impl($severity, $message, Some($stop), Some($fix));
224    };
225    ($severity:expr, $message:expr, $stop:expr, $fix:expr, $($action:expr),+) => {
226        {
227            let actions: Vec<String> = vec![$($action.to_string()),+];
228            let action_str = actions.join("\n   📋 ");
229            $crate::alert::_alert_custom_impl($severity, $message, Some($stop), Some(&format!("{}\n   📋 {}", $fix, action_str)));
230        }
231    };
232}
233
234// Implementation functions that check for slog availability
235
236/// Internal implementation for critical alerts
237#[allow(clippy::module_name_repetitions)]
238pub fn _alert_critical_impl(message: &str, fix: Option<&str>) {
239    if let Some(fix_msg) = fix {
240        _try_slog_error(&format!(
241            "{}\n   âš ī¸  STOP: Cannot proceed\n   💡 FIX: {}",
242            message, fix_msg
243        ));
244        eprintln!(
245            "🚨 {}\n   âš ī¸  STOP: Cannot proceed\n   💡 FIX: {}",
246            message, fix_msg
247        );
248    } else {
249        _try_slog_error(&format!(
250            "{}\n   âš ī¸  STOP: Cannot proceed\n   💡 FIX: Investigate and resolve",
251            message
252        ));
253        eprintln!(
254            "🚨 {}\n   âš ī¸  STOP: Cannot proceed\n   💡 FIX: Investigate and resolve",
255            message
256        );
257    }
258}
259
260/// Internal implementation for warning alerts
261#[allow(clippy::module_name_repetitions)]
262pub fn _alert_warning_impl(message: &str, fix: Option<&str>) {
263    if let Some(fix_msg) = fix {
264        _try_slog_warn(&format!(
265            "{}\n   âš ī¸  WARNING: Investigate before proceeding\n   💡 FIX: {}",
266            message, fix_msg
267        ));
268        eprintln!(
269            "âš ī¸  {}\n   âš ī¸  WARNING: Investigate before proceeding\n   💡 FIX: {}",
270            message, fix_msg
271        );
272    } else {
273        _try_slog_warn(&format!(
274            "{}\n   âš ī¸  WARNING: Investigate before proceeding\n   💡 FIX: Check and resolve",
275            message
276        ));
277        eprintln!(
278            "âš ī¸  {}\n   âš ī¸  WARNING: Investigate before proceeding\n   💡 FIX: Check and resolve",
279            message
280        );
281    }
282}
283
284/// Internal implementation for info alerts
285#[allow(clippy::module_name_repetitions)]
286pub fn _alert_info_impl(message: &str) {
287    _try_slog_info(message);
288    eprintln!("â„šī¸  {}", message);
289}
290
291/// Internal implementation for success alerts
292#[allow(clippy::module_name_repetitions)]
293pub fn _alert_success_impl(message: &str) {
294    _try_slog_info(&format!("✅ {}", message));
295    eprintln!("✅ {}", message);
296}
297
298/// Internal implementation for debug alerts
299#[allow(clippy::module_name_repetitions)]
300pub fn _alert_debug_impl(message: &str) {
301    _try_slog_debug(message);
302    eprintln!("🔍 {}", message);
303}
304
305/// Internal implementation for custom alerts
306#[allow(clippy::module_name_repetitions)]
307pub fn _alert_custom_impl(severity: &str, message: &str, stop: Option<&str>, fix: Option<&str>) {
308    if let (Some(stop_msg), Some(fix_msg)) = (stop, fix) {
309        _try_slog_warn(&format!(
310            "{} {}\n   {} {}\n   💡 FIX: {}",
311            severity, message, severity, stop_msg, fix_msg
312        ));
313        eprintln!(
314            "{} {}\n   {} {}\n   💡 FIX: {}",
315            severity, message, severity, stop_msg, fix_msg
316        );
317    } else {
318        _try_slog_info(&format!("{} {}", severity, message));
319        eprintln!("{} {}", severity, message);
320    }
321}
322
323// Try to use slog if available, otherwise fall back to eprintln!
324
325// Try to use slog if available via slog_scope
326// Note: slog_scope may not be initialized, so we always fall back to eprintln!
327// This ensures alerts are always visible even if slog isn't initialized
328fn _try_slog_error(msg: &str) {
329    // Try to use slog if available, but always output to stderr as well
330    // slog_scope::logger() may panic if not initialized, so we catch that
331    let _ = std::panic::catch_unwind(|| {
332        let logger = slog_scope::logger();
333        slog::error!(logger, "{}", msg);
334    });
335}
336
337fn _try_slog_warn(msg: &str) {
338    let _ = std::panic::catch_unwind(|| {
339        let logger = slog_scope::logger();
340        slog::warn!(logger, "{}", msg);
341    });
342}
343
344fn _try_slog_info(msg: &str) {
345    let _ = std::panic::catch_unwind(|| {
346        let logger = slog_scope::logger();
347        slog::info!(logger, "{}", msg);
348    });
349}
350
351fn _try_slog_debug(msg: &str) {
352    let _ = std::panic::catch_unwind(|| {
353        let logger = slog_scope::logger();
354        slog::debug!(logger, "{}", msg);
355    });
356}
357
358/// Write alert to a writer
359///
360/// Allows writing alerts to custom writers (e.g., files, buffers).
361///
362/// # Arguments
363///
364/// * `writer` - Writer to write to
365/// * `severity` - Severity emoji
366/// * `message` - The message
367/// * `stop` - Stop message (optional)
368/// * `fix` - Fix suggestion (optional)
369///
370/// # Example
371///
372/// ```rust
373/// use ggen_utils::alert::write_alert;
374/// use std::io::BufWriter;
375/// use std::fs::File;
376///
377/// let file = File::create("alert.log").unwrap();
378/// let mut writer = BufWriter::new(file);
379/// write_alert(&mut writer, "🚨", "Critical error", "STOP: Cannot proceed", "FIX: Resolve issue").unwrap();
380/// ```
381///
382/// # Errors
383///
384/// Returns an error if writing to the writer fails.
385pub fn write_alert<W: Write>(
386    writer: &mut W, severity: &str, message: &str, stop: Option<&str>, fix: Option<&str>,
387) -> io::Result<()> {
388    if let (Some(stop_msg), Some(fix_msg)) = (stop, fix) {
389        writeln!(
390            writer,
391            "{severity} {message}\n   {severity} {stop_msg}\n   💡 FIX: {fix_msg}"
392        )?;
393    } else if let Some(stop_msg) = stop {
394        writeln!(writer, "{severity} {message}\n   {severity} {stop_msg}")?;
395    } else {
396        writeln!(writer, "{severity} {message}")?;
397    }
398    Ok(())
399}
400
401#[cfg(test)]
402#[allow(clippy::panic)] // Test code - panic is appropriate for test failures
403mod tests {
404    use super::*;
405
406    #[test]
407    fn test_alert_critical() {
408        // Test critical alert without fix
409        alert_critical!("Test critical error");
410
411        // Test critical alert with fix
412        alert_critical!("Test critical error", "Test fix");
413
414        // Test critical alert with fix and actions
415        alert_critical!("Test critical error", "Test fix", "Action 1", "Action 2");
416    }
417
418    #[test]
419    fn test_alert_warning() {
420        // Test warning alert without fix
421        alert_warning!("Test warning");
422
423        // Test warning alert with fix
424        alert_warning!("Test warning", "Test fix");
425
426        // Test warning alert with fix and actions
427        alert_warning!(
428            "Test warning: {} - Actions: {}, {}",
429            "Test fix",
430            "Action 1",
431            "Action 2"
432        );
433    }
434
435    #[test]
436    fn test_alert_info() {
437        // Test info alert
438        alert_info!("Test info");
439
440        // Test info alert with details
441        alert_info!("Test info: {}, {}", "Detail 1", "Detail 2");
442    }
443
444    #[test]
445    fn test_alert_success() {
446        // Test success alert
447        alert_success!("Test success");
448
449        // Test success alert with details
450        alert_success!("Test success: {}, {}", "Detail 1", "Detail 2");
451    }
452
453    #[test]
454    fn test_alert_debug() {
455        // Test debug alert
456        alert_debug!("Test debug");
457
458        // Test debug alert with format
459        alert_debug!("Test debug: {}", "value");
460    }
461
462    #[test]
463    fn test_alert_custom() {
464        // Test custom alert
465        alert!("🚨", "Custom critical");
466
467        // Test custom alert with stop and fix
468        alert!(
469            "🚨",
470            "Custom critical",
471            "STOP: Cannot proceed",
472            "FIX: Resolve issue"
473        );
474
475        // Test custom alert with stop, fix, and actions
476        alert!(
477            "🚨",
478            "Custom critical",
479            "STOP: Cannot proceed",
480            "FIX: Resolve issue",
481            "Action 1",
482            "Action 2"
483        );
484    }
485
486    #[test]
487    fn test_write_alert() {
488        let mut buffer = Vec::new();
489
490        // Test write alert without stop/fix
491        write_alert(&mut buffer, "🚨", "Test error", None, None).unwrap();
492        let output = String::from_utf8_lossy(&buffer);
493        assert!(output.contains("🚨 Test error"));
494
495        // Test write alert with stop
496        buffer.clear();
497        write_alert(
498            &mut buffer,
499            "🚨",
500            "Test error",
501            Some("STOP: Cannot proceed"),
502            None,
503        )
504        .unwrap();
505        let output = String::from_utf8_lossy(&buffer);
506        assert!(output.contains("🚨 Test error"));
507        assert!(output.contains("STOP: Cannot proceed"));
508
509        // Test write alert with stop and fix
510        buffer.clear();
511        write_alert(
512            &mut buffer,
513            "🚨",
514            "Test error",
515            Some("STOP: Cannot proceed"),
516            Some("FIX: Resolve issue"),
517        )
518        .unwrap();
519        let output = String::from_utf8_lossy(&buffer);
520        assert!(output.contains("🚨 Test error"));
521        assert!(output.contains("STOP: Cannot proceed"));
522        assert!(output.contains("FIX: Resolve issue"));
523    }
524}