use std::io::{self, Write};
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Format {
#[default]
Human,
Json,
Yaml,
}
impl Format {
pub fn from_str(s: &str) -> Self {
match s {
"json" => Self::Json,
"yaml" => Self::Yaml,
_ => Self::Human,
}
}
}
#[inline]
fn stdout_is_tty() -> bool {
false
}
pub struct Out {
format: Format,
#[allow(dead_code)]
color: bool,
quiet: bool,
verbose: bool,
stdout: Box<dyn Write + Send>,
stderr: Box<dyn Write + Send>,
}
impl Out {
pub fn new(format: Format) -> Self {
Self {
format,
color: stdout_is_tty(),
quiet: false,
verbose: false,
stdout: Box::new(io::stdout()),
stderr: Box::new(io::stderr()),
}
}
pub fn with_format(format: Format) -> Self {
Self::new(format)
}
pub fn with_sinks(
format: Format,
stdout: Box<dyn Write + Send>,
stderr: Box<dyn Write + Send>,
) -> Self {
Self {
format,
color: false,
quiet: false,
verbose: false,
stdout,
stderr,
}
}
pub fn quiet(mut self) -> Self {
self.quiet = true;
self
}
pub fn verbose(mut self) -> Self {
self.verbose = true;
self
}
pub fn no_color(mut self) -> Self {
self.color = false;
self
}
pub fn data<T: Serialize + std::fmt::Display>(&mut self, value: &T) -> io::Result<()> {
match self.format {
Format::Json => {
let json = serde_json::to_string_pretty(value)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
writeln!(self.stdout, "{}", json)
}
Format::Human | Format::Yaml => writeln!(self.stdout, "{}", value),
}
}
pub fn json(&mut self, value: &serde_json::Value) -> io::Result<()> {
let s = serde_json::to_string_pretty(value)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
writeln!(self.stdout, "{}", s)
}
pub fn diag(&mut self, diag: &crate::diag::Diag) -> io::Result<()> {
if !self.quiet {
writeln!(self.stderr, "[E{:04}] {}", diag.code, diag.message)?;
if let Some(hint) = &diag.hint {
writeln!(self.stderr, " hint: {}", hint)?;
}
}
Ok(())
}
pub fn warn(&mut self, msg: &str) -> io::Result<()> {
if !self.quiet {
writeln!(self.stderr, "warn: {}", msg)?;
}
Ok(())
}
pub fn info(&mut self, msg: &str) -> io::Result<()> {
if self.verbose && !self.quiet {
writeln!(self.stderr, "info: {}", msg)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use serde_json::json;
use super::*;
use crate::diag::{Diag, ErrorCode};
struct SharedVec(Arc<Mutex<Vec<u8>>>);
impl Write for SharedVec {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn build_out(
format: Format,
) -> (Out, Arc<Mutex<Vec<u8>>>, Arc<Mutex<Vec<u8>>>) {
let stdout_arc: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
let stderr_arc: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
let out = Out::with_sinks(
format,
Box::new(SharedVec(Arc::clone(&stdout_arc))),
Box::new(SharedVec(Arc::clone(&stderr_arc))),
);
(out, stdout_arc, stderr_arc)
}
#[test]
fn format_from_str_parses_known_values() {
assert_eq!(Format::from_str("json"), Format::Json);
assert_eq!(Format::from_str("yaml"), Format::Yaml);
assert_eq!(Format::from_str("human"), Format::Human);
}
#[test]
fn format_from_str_falls_back_to_human() {
assert_eq!(Format::from_str(""), Format::Human);
assert_eq!(Format::from_str("unknown"), Format::Human);
assert_eq!(Format::from_str("JSON"), Format::Human); }
#[test]
fn format_default_is_human() {
assert_eq!(Format::default(), Format::Human);
}
#[test]
fn data_human_uses_display() {
struct Msg;
impl std::fmt::Display for Msg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("hello from display")
}
}
impl Serialize for Msg {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str("hello from display")
}
}
let (mut out, so, _se) = build_out(Format::Human);
out.data(&Msg).expect("data");
let bytes = so.lock().unwrap().clone();
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("hello from display"));
}
#[test]
fn data_json_emits_pretty_json() {
#[derive(Serialize)]
struct Item {
x: u32,
}
impl std::fmt::Display for Item {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Item({})", self.x)
}
}
let (mut out, so, _se) = build_out(Format::Json);
out.data(&Item { x: 42 }).expect("data");
let bytes = so.lock().unwrap().clone();
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("\"x\""));
assert!(s.contains("42"));
}
#[test]
fn json_writes_to_stdout() {
let (mut out, so, _se) = build_out(Format::Human);
out.json(&json!({"key": "value"})).expect("json");
let bytes = so.lock().unwrap().clone();
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("\"key\""));
assert!(s.contains("\"value\""));
}
#[test]
fn diag_writes_code_and_message_to_stderr() {
let (mut out, _so, se) = build_out(Format::Human);
let d = Diag::new(ErrorCode::SeqGap, "gap at seq 5");
out.diag(&d).expect("diag");
let bytes = se.lock().unwrap().clone();
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("[E1003]"), "expected E1003 got: {s}");
assert!(s.contains("gap at seq 5"));
}
#[test]
fn diag_writes_hint_when_present() {
let (mut out, _so, se) = build_out(Format::Human);
let d = Diag::new(ErrorCode::SeqGap, "gap").with_hint("fix the gap");
out.diag(&d).expect("diag");
let bytes = se.lock().unwrap().clone();
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("hint: fix the gap"));
}
#[test]
fn diag_suppressed_when_quiet() {
let (out, _so, se) = build_out(Format::Human);
let mut out = out.quiet();
let d = Diag::new(ErrorCode::SeqGap, "gap");
out.diag(&d).expect("diag");
let bytes = se.lock().unwrap().clone();
assert!(bytes.is_empty(), "quiet mode should suppress diag output");
}
#[test]
fn warn_writes_to_stderr() {
let (mut out, _so, se) = build_out(Format::Human);
out.warn("something smells wrong").expect("warn");
let bytes = se.lock().unwrap().clone();
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("warn: something smells wrong"));
}
#[test]
fn warn_suppressed_when_quiet() {
let (out, _so, se) = build_out(Format::Human);
let mut out = out.quiet();
out.warn("ignored").expect("warn");
let bytes = se.lock().unwrap().clone();
assert!(bytes.is_empty());
}
#[test]
fn info_only_emits_when_verbose() {
{
let (mut out, _so, se) = build_out(Format::Human);
out.info("detail").expect("info");
let bytes = se.lock().unwrap().clone();
assert!(bytes.is_empty(), "info should be silent without verbose");
}
{
let (out, _so, se) = build_out(Format::Human);
let mut out = out.verbose();
out.info("detail").expect("info");
let bytes = se.lock().unwrap().clone();
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("info: detail"));
}
}
#[test]
fn info_suppressed_when_quiet_and_verbose() {
let (out, _so, se) = build_out(Format::Human);
let mut out = out.verbose().quiet();
out.info("detail").expect("info");
let bytes = se.lock().unwrap().clone();
assert!(bytes.is_empty(), "quiet takes precedence over verbose");
}
#[test]
fn diag_writes_nothing_to_stdout() {
let (mut out, so, _se) = build_out(Format::Human);
let d = Diag::new(ErrorCode::ReceiptNotFound, "missing");
out.diag(&d).expect("diag");
let bytes = so.lock().unwrap().clone();
assert!(bytes.is_empty(), "diag must not write to stdout");
}
#[test]
fn no_color_sets_color_false() {
let out = Out::with_format(Format::Human).no_color();
drop(out);
}
}