Skip to main content

ralph_workflow/json_parser/printer/
traits.rs

1// Printer trait and standard implementations.
2//
3// Contains the Printable trait and StdoutPrinter/StderrPrinter.
4
5/// Trait for output destinations in parsers.
6///
7/// This trait allows parsers to write to different output destinations
8/// (stdout, stderr, or test collectors) without hardcoding the specific
9/// destination. This makes parsers testable by allowing output capture.
10pub trait Printable: std::io::Write {
11    /// Check if this printer is connected to a terminal.
12    ///
13    /// This is used to determine whether to use terminal-specific features
14    /// like colors and carriage return-based updates.
15    fn is_terminal(&self) -> bool;
16}
17
18/// Printer that writes to stdout.
19#[derive(Debug, Clone)]
20pub struct StdoutPrinter {
21    buffer: String,
22    is_terminal: bool,
23}
24
25impl StdoutPrinter {
26    /// Create a new stdout printer.
27    #[must_use]
28    pub fn new() -> Self {
29        Self {
30            buffer: String::new(),
31            is_terminal: std::io::stdout().is_terminal(),
32        }
33    }
34
35    /// Write text to the buffer.
36    #[must_use]
37    pub fn write_text(self, text: &str) -> Self {
38        Self {
39            buffer: format!("{}{}", self.buffer, text),
40            is_terminal: self.is_terminal,
41        }
42    }
43
44    /// Write a line to the buffer.
45    #[must_use]
46    pub fn write_line(self, line: &str) -> Self {
47        self.write_text(&format!("{}\n", line))
48    }
49
50    /// Emit the buffered content and reset the buffer.
51    /// Returns (printer with empty buffer, emitted content).
52    #[must_use]
53    pub fn emit(self) -> (Self, String) {
54        let output = self.buffer.clone();
55        (
56            Self {
57                buffer: String::new(),
58                is_terminal: self.is_terminal,
59            },
60            output,
61        )
62    }
63
64    /// Get the current buffer content.
65    #[must_use]
66    pub fn get_buffer(&self) -> &str {
67        &self.buffer
68    }
69
70    /// Flush the printer (no-op for stdout, kept for trait compatibility).
71    pub fn flush(self) -> Self {
72        self
73    }
74}
75
76impl Default for StdoutPrinter {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl Printable for StdoutPrinter {
83    fn is_terminal(&self) -> bool {
84        self.is_terminal
85    }
86}
87
88impl std::io::Write for StdoutPrinter {
89    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
90        let s =
91            std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
92        self.buffer.push_str(s);
93        std::io::stdout().write(buf)
94    }
95
96    fn flush(&mut self) -> io::Result<()> {
97        std::io::stdout().flush()
98    }
99}
100
101/// Printer that writes to stderr.
102#[derive(Debug, Clone)]
103#[cfg(any(test, feature = "test-utils"))]
104pub struct StderrPrinter {
105    buffer: String,
106    is_terminal: bool,
107}
108
109#[cfg(any(test, feature = "test-utils"))]
110impl StderrPrinter {
111    /// Create a new stderr printer.
112    #[must_use]
113    pub fn new() -> Self {
114        Self {
115            buffer: String::new(),
116            is_terminal: std::io::stderr().is_terminal(),
117        }
118    }
119
120    /// Write text to the buffer.
121    #[must_use]
122    pub fn write_text(self, text: &str) -> Self {
123        Self {
124            buffer: format!("{}{}", self.buffer, text),
125            is_terminal: self.is_terminal,
126        }
127    }
128
129    /// Write a line to the buffer.
130    #[must_use]
131    pub fn write_line(self, line: &str) -> Self {
132        self.write_text(&format!("{}\n", line))
133    }
134
135    /// Emit the buffered content and reset the buffer.
136    /// Returns (printer with empty buffer, emitted content).
137    #[must_use]
138    pub fn emit(self) -> (Self, String) {
139        let output = self.buffer.clone();
140        (
141            Self {
142                buffer: String::new(),
143                is_terminal: self.is_terminal,
144            },
145            output,
146        )
147    }
148
149    /// Get the current buffer content.
150    #[must_use]
151    pub fn get_buffer(&self) -> &str {
152        &self.buffer
153    }
154
155    /// Flush the printer (no-op for stderr, kept for trait compatibility).
156    pub fn flush(self) -> Self {
157        self
158    }
159}
160
161#[cfg(any(test, feature = "test-utils"))]
162impl Default for StderrPrinter {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168#[cfg(any(test, feature = "test-utils"))]
169impl std::io::Write for StderrPrinter {
170    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
171        let s =
172            std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
173        self.buffer.push_str(s);
174        std::io::stderr().write(buf)
175    }
176
177    fn flush(&mut self) -> io::Result<()> {
178        std::io::stderr().flush()
179    }
180}
181
182#[cfg(any(test, feature = "test-utils"))]
183impl Printable for StderrPrinter {
184    fn is_terminal(&self) -> bool {
185        self.is_terminal
186    }
187}
188
189/// Shared printer reference for use in parsers.
190///
191/// This type alias represents a shared, mutable reference to a printer
192/// that can be used across parser methods.
193pub type SharedPrinter = Rc<RefCell<dyn Printable>>;
194
195/// Create a shared stdout printer.
196#[must_use]
197pub fn shared_stdout() -> SharedPrinter {
198    Rc::new(RefCell::new(StdoutPrinter::new()))
199}