browser_automation_cli/
output.rs1use std::io::{self, Write};
13
14use crate::error::{CliError, ErrorKind};
15
16pub fn map_io_error(err: &io::Error, stream: &str) -> CliError {
21 if err.kind() == io::ErrorKind::BrokenPipe {
22 CliError::new(
23 ErrorKind::BrokenPipe,
24 format!("{stream}: broken pipe"),
25 )
26 } else {
27 CliError::new(ErrorKind::Io, format!("{stream}: {err}"))
28 }
29}
30
31pub fn write_stdout(bytes: &[u8]) -> Result<(), CliError> {
33 let mut out = io::stdout().lock();
34 out.write_all(bytes)
35 .map_err(|e| map_io_error(&e, "stdout"))?;
36 Ok(())
37}
38
39pub fn writeln_stdout(line: impl AsRef<str>) -> Result<(), CliError> {
43 let mut out = io::stdout().lock();
44 out.write_all(line.as_ref().as_bytes())
45 .map_err(|e| map_io_error(&e, "stdout"))?;
46 out.write_all(b"\n")
47 .map_err(|e| map_io_error(&e, "stdout"))?;
48 out.flush().map_err(|e| map_io_error(&e, "stdout"))?;
49 Ok(())
50}
51
52pub fn writeln_stderr(line: impl AsRef<str>) -> Result<(), CliError> {
54 let mut err = io::stderr().lock();
55 err.write_all(line.as_ref().as_bytes())
56 .map_err(|e| map_io_error(&e, "stderr"))?;
57 err.write_all(b"\n")
58 .map_err(|e| map_io_error(&e, "stderr"))?;
59 err.flush().map_err(|e| map_io_error(&e, "stderr"))?;
60 Ok(())
61}
62
63pub fn flush_stdout() -> Result<(), CliError> {
65 io::stdout()
66 .flush()
67 .map_err(|e| map_io_error(&e, "stdout"))
68}
69
70pub fn flush_stderr() -> Result<(), CliError> {
72 io::stderr()
73 .flush()
74 .map_err(|e| map_io_error(&e, "stderr"))
75}
76
77pub fn write_json_line(value: &serde_json::Value) -> Result<(), CliError> {
82 write_json_line_ser(value)
83}
84
85pub fn write_json_line_ser<T: serde::Serialize>(value: &T) -> Result<(), CliError> {
87 let s = crate::json_util::to_compact_string(value)?;
88 if s.as_bytes().contains(&b'\n') {
90 return Err(CliError::new(
91 ErrorKind::Software,
92 "json encode produced embedded newline (breaks NDJSON/envelope contract)",
93 ));
94 }
95 writeln_stdout(s)
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn broken_pipe_maps_to_141() {
104 let io_err = io::Error::new(io::ErrorKind::BrokenPipe, "pipe");
105 let err = map_io_error(&io_err, "stdout");
106 assert_eq!(err.kind(), ErrorKind::BrokenPipe);
107 assert_eq!(err.exit_code(), 141);
108 }
109
110 #[test]
111 fn other_io_maps_to_74() {
112 let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "nope");
113 let err = map_io_error(&io_err, "stdout");
114 assert_eq!(err.kind(), ErrorKind::Io);
115 assert_eq!(err.exit_code(), 74);
116 }
117}