Skip to main content

mago_reporting/
output.rs

1use std::io;
2use std::io::Write;
3use std::sync::Arc;
4use std::sync::Mutex;
5use std::sync::PoisonError;
6
7/// Target for report output.
8#[derive(strum::Display, strum::EnumString, strum::VariantNames)]
9#[strum(serialize_all = "kebab-case")]
10#[derive(Default)]
11pub enum ReportingTarget {
12    /// Write to standard output.
13    #[strum(serialize = "stdout", serialize = "out")]
14    #[default]
15    Stdout,
16    /// Write to standard error.
17    #[strum(serialize = "stderr", serialize = "err")]
18    Stderr,
19    /// Write to a custom writer.
20    #[strum(disabled)]
21    Writer(Arc<Mutex<Box<dyn Write + Send>>>),
22}
23
24impl Clone for ReportingTarget {
25    fn clone(&self) -> Self {
26        match self {
27            ReportingTarget::Stdout => ReportingTarget::Stdout,
28            ReportingTarget::Stderr => ReportingTarget::Stderr,
29            ReportingTarget::Writer(writer) => ReportingTarget::Writer(Arc::clone(writer)),
30        }
31    }
32}
33
34impl std::fmt::Debug for ReportingTarget {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            ReportingTarget::Stdout => write!(f, "ReportingTarget::Stdout"),
38            ReportingTarget::Stderr => write!(f, "ReportingTarget::Stderr"),
39            ReportingTarget::Writer(_) => write!(f, "ReportingTarget::Writer(..)"),
40        }
41    }
42}
43
44impl ReportingTarget {
45    /// Create a target that writes to a shared buffer.
46    ///
47    /// This is useful for testing where you want to capture the output.
48    ///
49    /// # Returns
50    ///
51    /// A tuple of (target, buffer) where the buffer can be read after reporting.
52    #[must_use]
53    pub fn buffer() -> (Self, Arc<Mutex<Vec<u8>>>) {
54        let buffer = Arc::new(Mutex::new(Vec::new()));
55        let writer_buffer = Arc::clone(&buffer);
56        let target = ReportingTarget::Writer(Arc::new(Mutex::new(Box::new(BufferWriter { buffer: writer_buffer }))));
57        (target, buffer)
58    }
59
60    /// Resolve this target to an actual writer.
61    ///
62    /// # Returns
63    ///
64    /// A boxed writer that can be written to.
65    pub(crate) fn resolve(&self) -> Box<dyn Write + '_> {
66        match self {
67            ReportingTarget::Stdout => Box::new(io::stdout()),
68            ReportingTarget::Stderr => Box::new(io::stderr()),
69            ReportingTarget::Writer(writer) => Box::new(LockedWriter { writer: Arc::clone(writer) }),
70        }
71    }
72}
73
74/// A wrapper that provides Write implementation for Arc<Mutex<Box<dyn Write>>>.
75struct LockedWriter {
76    writer: Arc<Mutex<Box<dyn Write + Send>>>,
77}
78
79impl Write for LockedWriter {
80    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
81        self.writer.lock().unwrap_or_else(PoisonError::into_inner).write(buf)
82    }
83
84    fn flush(&mut self) -> io::Result<()> {
85        self.writer.lock().unwrap_or_else(PoisonError::into_inner).flush()
86    }
87}
88
89/// A writer that writes to a shared buffer.
90struct BufferWriter {
91    buffer: Arc<Mutex<Vec<u8>>>,
92}
93
94impl Write for BufferWriter {
95    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
96        self.buffer.lock().unwrap_or_else(PoisonError::into_inner).write(buf)
97    }
98
99    fn flush(&mut self) -> io::Result<()> {
100        self.buffer.lock().unwrap_or_else(PoisonError::into_inner).flush()
101    }
102}