use std::fmt;
use std::io::{self, Write};
use serde::Serialize;
use shipshape_core::SCHEMA_VERSION;
use crate::error::CliError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputFormat {
#[default]
Text,
Json,
}
impl OutputFormat {
pub fn from_json_flag(json: bool) -> Self {
if json {
Self::Json
} else {
Self::Text
}
}
}
#[derive(Serialize)]
struct SuccessEnvelope<'a, T: Serialize> {
schema_version: u32,
data: &'a T,
warnings: &'a [String],
}
pub fn write_stdout(args: fmt::Arguments<'_>) -> Result<(), CliError> {
let stdout = io::stdout();
let mut lock = stdout.lock();
write_to(&mut lock, args)
}
fn write_to(writer: &mut dyn Write, args: fmt::Arguments<'_>) -> Result<(), CliError> {
match writer.write_fmt(args).and_then(|()| writer.flush()) {
Ok(()) => Ok(()),
Err(error) if error.kind() == io::ErrorKind::BrokenPipe => Ok(()),
Err(error) => Err(CliError::system(
"io_stdout",
format!("failed to write stdout: {error}"),
)),
}
}
macro_rules! stdout {
($($arg:tt)*) => {{
$crate::output::write_stdout(format_args!($($arg)*))
}};
}
macro_rules! stdoutln {
() => {{
$crate::output::write_stdout(format_args!("\n"))
}};
($($arg:tt)*) => {{
$crate::output::write_stdout(format_args!("{}\n", format_args!($($arg)*)))
}};
}
pub(crate) use stdout;
pub(crate) use stdoutln;
pub fn emit_json<T: Serialize>(body: &T, warnings: &[String]) -> Result<(), CliError> {
let envelope = SuccessEnvelope {
schema_version: SCHEMA_VERSION,
data: body,
warnings,
};
let mut s = serde_json::to_string_pretty(&envelope)
.map_err(|e| CliError::system("internal_serialize", e.to_string()))?;
s.push('\n');
write_stdout(format_args!("{s}"))
}
#[cfg(test)]
mod tests {
use super::*;
struct FailingWriter(io::ErrorKind);
impl Write for FailingWriter {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::from(self.0))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn broken_pipe_is_success() {
let mut writer = FailingWriter(io::ErrorKind::BrokenPipe);
assert!(write_to(&mut writer, format_args!("payload")).is_ok());
}
#[test]
fn other_stdout_failure_is_structured_system_error() {
let mut writer = FailingWriter(io::ErrorKind::PermissionDenied);
let error = write_to(&mut writer, format_args!("payload")).unwrap_err();
assert!(matches!(error.kind, crate::error::ExitKind::System));
assert_eq!(error.code, "io_stdout");
assert!(error.message.starts_with("failed to write stdout: "));
}
struct FlushFailingWriter(io::ErrorKind);
impl Write for FlushFailingWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Err(io::Error::from(self.0))
}
}
#[test]
fn deferred_broken_pipe_is_success() {
let mut writer = FlushFailingWriter(io::ErrorKind::BrokenPipe);
assert!(write_to(&mut writer, format_args!("payload")).is_ok());
}
#[test]
fn deferred_non_broken_pipe_is_structured_system_error() {
let mut writer = FlushFailingWriter(io::ErrorKind::PermissionDenied);
let error = write_to(&mut writer, format_args!("payload")).unwrap_err();
assert!(matches!(error.kind, crate::error::ExitKind::System));
assert_eq!(error.code, "io_stdout");
}
}