ralph_workflow/json_parser/
printer.rs

1//! Printer abstraction for testable output.
2//!
3//! This module provides a trait-based abstraction for output destinations,
4//! allowing parsers to write to stdout, stderr, or test collectors without
5//! changing their core logic.
6
7use std::cell::RefCell;
8use std::io::{self, IsTerminal, Stdout};
9use std::rc::Rc;
10
11#[cfg(test)]
12use std::io::Stderr;
13
14/// Trait for output destinations in parsers.
15///
16/// This trait allows parsers to write to different output destinations
17/// (stdout, stderr, or test collectors) without hardcoding the specific
18/// destination. This makes parsers testable by allowing output capture.
19pub trait Printable: std::io::Write {
20    /// Check if this printer is connected to a terminal.
21    ///
22    /// This is used to determine whether to use terminal-specific features
23    /// like colors and carriage return-based updates.
24    fn is_terminal(&self) -> bool;
25}
26
27/// Printer that writes to stdout.
28#[derive(Debug)]
29pub struct StdoutPrinter {
30    stdout: Stdout,
31    is_terminal: bool,
32}
33
34impl StdoutPrinter {
35    /// Create a new stdout printer.
36    pub fn new() -> Self {
37        let is_terminal = std::io::stdout().is_terminal();
38        Self {
39            stdout: std::io::stdout(),
40            is_terminal,
41        }
42    }
43}
44
45impl Default for StdoutPrinter {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl std::io::Write for StdoutPrinter {
52    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
53        self.stdout.write(buf)
54    }
55
56    fn flush(&mut self) -> io::Result<()> {
57        self.stdout.flush()
58    }
59}
60
61impl Printable for StdoutPrinter {
62    fn is_terminal(&self) -> bool {
63        self.is_terminal
64    }
65}
66
67/// Printer that writes to stderr.
68#[derive(Debug)]
69#[cfg(test)]
70pub struct StderrPrinter {
71    stderr: Stderr,
72    is_terminal: bool,
73}
74
75#[cfg(test)]
76impl StderrPrinter {
77    /// Create a new stderr printer.
78    pub fn new() -> Self {
79        let is_terminal = std::io::stderr().is_terminal();
80        Self {
81            stderr: std::io::stderr(),
82            is_terminal,
83        }
84    }
85}
86
87#[cfg(test)]
88impl Default for StderrPrinter {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94#[cfg(test)]
95impl std::io::Write for StderrPrinter {
96    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
97        self.stderr.write(buf)
98    }
99
100    fn flush(&mut self) -> io::Result<()> {
101        self.stderr.flush()
102    }
103}
104
105#[cfg(test)]
106impl Printable for StderrPrinter {
107    fn is_terminal(&self) -> bool {
108        self.is_terminal
109    }
110}
111
112/// Test printer that captures output for assertion.
113///
114/// This printer stores all output in memory for testing purposes.
115/// It provides methods to retrieve and inspect the captured output.
116#[cfg(test)]
117#[derive(Debug, Default)]
118pub struct TestPrinter {
119    /// Captured output lines.
120    output: RefCell<Vec<String>>,
121    /// Buffer for incomplete lines.
122    buffer: RefCell<String>,
123}
124
125#[cfg(test)]
126impl TestPrinter {
127    /// Create a new test printer.
128    pub fn new() -> Self {
129        Self::default()
130    }
131
132    /// Get all captured output as a single string.
133    pub fn get_output(&self) -> String {
134        let mut result = self.buffer.borrow().clone();
135        for line in self.output.borrow().iter() {
136            result.push_str(line);
137        }
138        result
139    }
140
141    /// Get captured output lines.
142    pub fn get_lines(&self) -> Vec<String> {
143        let mut result: Vec<String> = self.output.borrow().clone();
144        let buffer = self.buffer.borrow();
145        if !buffer.is_empty() {
146            result.push(buffer.clone());
147        }
148        result
149    }
150
151    /// Clear all captured output.
152    pub fn clear(&self) {
153        self.output.borrow_mut().clear();
154        self.buffer.borrow_mut().clear();
155    }
156
157    /// Check if a specific line exists in the output.
158    pub fn has_line(&self, line: &str) -> bool {
159        self.get_lines().iter().any(|l| l.contains(line))
160    }
161
162    /// Get the number of times a specific pattern appears in output.
163    pub fn count_pattern(&self, pattern: &str) -> usize {
164        self.get_lines()
165            .iter()
166            .filter(|l| l.contains(pattern))
167            .count()
168    }
169
170    /// Check if there are duplicate consecutive lines in output.
171    pub fn has_duplicate_consecutive_lines(&self) -> bool {
172        let lines = self.get_lines();
173        for i in 1..lines.len() {
174            if lines[i] == lines[i - 1] && !lines[i].is_empty() {
175                return true;
176            }
177        }
178        false
179    }
180
181    /// Find and return all duplicate consecutive lines.
182    pub fn find_duplicate_consecutive_lines(&self) -> Vec<(usize, String)> {
183        let mut duplicates = Vec::new();
184        let lines = self.get_lines();
185        for i in 1..lines.len() {
186            if lines[i] == lines[i - 1] && !lines[i].is_empty() {
187                duplicates.push((i - 1, lines[i - 1].clone()));
188            }
189        }
190        duplicates
191    }
192
193    /// Get statistics about the output.
194    ///
195    /// Returns a tuple of (`line_count`, `char_count`).
196    pub fn get_stats(&self) -> (usize, usize) {
197        let lines = self.get_lines();
198        let char_count: usize = lines.iter().map(String::len).sum();
199        (lines.len(), char_count)
200    }
201}
202
203#[cfg(test)]
204impl std::io::Write for TestPrinter {
205    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
206        let s =
207            std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
208        let mut buffer = self.buffer.borrow_mut();
209        buffer.push_str(s);
210
211        // Process complete lines
212        while let Some(newline_pos) = buffer.find('\n') {
213            let line = buffer.drain(..=newline_pos).collect::<String>();
214            self.output.borrow_mut().push(line);
215        }
216
217        Ok(buf.len())
218    }
219
220    fn flush(&mut self) -> io::Result<()> {
221        // Flush any remaining buffer content
222        let mut buffer = self.buffer.borrow_mut();
223        if !buffer.is_empty() {
224            self.output.borrow_mut().push(buffer.clone());
225            buffer.clear();
226        }
227        Ok(())
228    }
229}
230
231#[cfg(test)]
232impl Printable for TestPrinter {
233    fn is_terminal(&self) -> bool {
234        // Test printer is never a terminal
235        false
236    }
237}
238
239/// Shared printer reference for use in parsers.
240///
241/// This type alias represents a shared, mutable reference to a printer
242/// that can be used across parser methods.
243pub type SharedPrinter = Rc<RefCell<dyn Printable>>;
244
245/// Create a shared stdout printer.
246pub fn shared_stdout() -> SharedPrinter {
247    Rc::new(RefCell::new(StdoutPrinter::new()))
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use std::io::Write;
254
255    #[test]
256    fn test_stdout_printer() {
257        let mut printer = StdoutPrinter::new();
258        // Just ensure it compiles and works
259        let result = printer.write_all(b"test\n");
260        assert!(result.is_ok());
261        assert!(printer.flush().is_ok());
262
263        // Verify is_terminal() method is accessible
264        let _is_term = printer.is_terminal();
265    }
266
267    #[cfg(test)]
268    #[test]
269    fn test_printable_trait_is_terminal() {
270        let printer = StdoutPrinter::new();
271        // Test that the Printable trait's is_terminal method works
272        let _should_use_colors = printer.is_terminal();
273    }
274
275    #[test]
276    #[cfg(test)]
277    fn test_stderr_printer() {
278        let mut printer = StderrPrinter::new();
279        // Just ensure it compiles and works
280        let result = printer.write_all(b"test\n");
281        assert!(result.is_ok());
282        assert!(printer.flush().is_ok());
283    }
284
285    #[test]
286    #[cfg(test)]
287    fn test_printer_captures_output() {
288        let mut printer = TestPrinter::new();
289
290        printer
291            .write_all(b"Hello World\n")
292            .expect("Failed to write");
293        printer.flush().expect("Failed to flush");
294
295        let output = printer.get_output();
296        assert!(output.contains("Hello World"));
297    }
298
299    #[test]
300    #[cfg(test)]
301    fn test_printer_get_lines() {
302        let mut printer = TestPrinter::new();
303
304        printer.write_all(b"Line 1\nLine 2\n").unwrap();
305        printer.flush().unwrap();
306
307        let lines = printer.get_lines();
308        assert_eq!(lines.len(), 2);
309        assert!(lines[0].contains("Line 1"));
310        assert!(lines[1].contains("Line 2"));
311    }
312
313    #[test]
314    #[cfg(test)]
315    fn test_printer_clear() {
316        let mut printer = TestPrinter::new();
317
318        printer.write_all(b"Before\n").unwrap();
319        printer.flush().unwrap();
320
321        assert!(!printer.get_output().is_empty());
322
323        printer.clear();
324        assert!(printer.get_output().is_empty());
325    }
326
327    #[cfg(test)]
328    #[test]
329    fn test_printer_has_line() {
330        let mut printer = TestPrinter::new();
331
332        printer.write_all(b"Hello World\n").unwrap();
333        printer.flush().unwrap();
334
335        assert!(printer.has_line("Hello"));
336        assert!(printer.has_line("World"));
337        assert!(!printer.has_line("Goodbye"));
338    }
339
340    #[cfg(test)]
341    #[test]
342    fn test_printer_count_pattern() {
343        let mut printer = TestPrinter::new();
344
345        printer.write_all(b"test\nmore test\ntest again\n").unwrap();
346        printer.flush().unwrap();
347
348        assert_eq!(printer.count_pattern("test"), 3);
349    }
350
351    #[cfg(test)]
352    #[test]
353    fn test_printer_detects_duplicates() {
354        let mut printer = TestPrinter::new();
355
356        printer.write_all(b"Line 1\nLine 1\nLine 2\n").unwrap();
357        printer.flush().unwrap();
358
359        assert!(printer.has_duplicate_consecutive_lines());
360    }
361
362    #[cfg(test)]
363    #[test]
364    fn test_printer_finds_duplicates() {
365        let mut printer = TestPrinter::new();
366
367        printer
368            .write_all(b"Line 1\nLine 1\nLine 2\nLine 3\nLine 3\n")
369            .unwrap();
370        printer.flush().unwrap();
371
372        let duplicates = printer.find_duplicate_consecutive_lines();
373        assert_eq!(duplicates.len(), 2);
374        assert_eq!(duplicates[0].0, 0); // First duplicate at line 0-1
375        assert_eq!(duplicates[0].1, "Line 1\n");
376        assert_eq!(duplicates[1].0, 3); // Second duplicate at line 3-4
377        assert_eq!(duplicates[1].1, "Line 3\n");
378    }
379
380    #[cfg(test)]
381    #[test]
382    fn test_printer_no_false_positives() {
383        let mut printer = TestPrinter::new();
384
385        printer.write_all(b"Line 1\nLine 2\nLine 3\n").unwrap();
386        printer.flush().unwrap();
387
388        assert!(!printer.has_duplicate_consecutive_lines());
389    }
390
391    #[cfg(test)]
392    #[test]
393    fn test_printer_buffer_handling() {
394        let mut printer = TestPrinter::new();
395
396        // Write without newline - buffer should hold it
397        printer.write_all(b"Partial").unwrap();
398
399        // Without flush, content is in buffer but accessible via get_output/get_lines
400        // The TestPrinter stores partial content in buffer which is included in get_output
401        assert!(printer.get_output().contains("Partial"));
402
403        // Add newline to complete the line
404        printer.write_all(b" content\n").unwrap();
405        printer.flush().unwrap();
406
407        // Now should have the complete content
408        assert!(printer.has_line("Partial content"));
409
410        // Verify the complete output
411        let output = printer.get_output();
412        assert!(output.contains("Partial content\n"));
413    }
414
415    #[cfg(test)]
416    #[test]
417    fn test_printer_get_stats() {
418        let mut printer = TestPrinter::new();
419
420        printer.write_all(b"Line 1\nLine 2\n").unwrap();
421        printer.flush().unwrap();
422
423        let (line_count, char_count) = printer.get_stats();
424        assert_eq!(line_count, 2);
425        assert!(char_count > 0);
426    }
427
428    #[test]
429    fn test_shared_stdout() {
430        let printer = shared_stdout();
431        // Verify the function creates a valid SharedPrinter
432        let _borrowed = printer.borrow();
433    }
434}