Skip to main content

ready_set_sdk/
output.rs

1//! Output formatting helpers shared by every plugin.
2
3use std::io::{self, Write};
4
5use serde::Serialize;
6
7use crate::context::{ColorMode, Context, LogLevel};
8use crate::error::{Error, Result};
9
10/// Requested output mode. Mirrors `READY_SET_OUTPUT`.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum OutputMode {
13    /// Human-readable output (default).
14    #[default]
15    Human,
16    /// Machine-readable JSON output.
17    Json,
18}
19
20impl OutputMode {
21    pub(crate) fn parse(raw: Option<&str>) -> Self {
22        match raw.map(str::trim) {
23            Some("json") => Self::Json,
24            // "human" or anything unrecognized falls back to Human per the env contract.
25            _ => Self::Human,
26        }
27    }
28}
29
30/// Sink for plugin output. Plugins write here instead of `println!` so output
31/// stays consistent with the requested mode and verbosity.
32pub struct Output {
33    mode: OutputMode,
34    log_level: LogLevel,
35    color: ColorMode,
36    writer: Box<dyn Write + Send>,
37}
38
39impl Output {
40    /// Build an `Output` from a [`Context`] and a writer (typically stdout).
41    pub fn for_context(ctx: &Context, writer: impl Write + Send + 'static) -> Self {
42        Self {
43            mode: ctx.output_mode(),
44            log_level: ctx.log_level(),
45            color: ctx.color(),
46            writer: Box::new(writer),
47        }
48    }
49
50    /// Replace the inner writer. Useful in tests.
51    #[must_use]
52    pub fn with_writer(mut self, writer: impl Write + Send + 'static) -> Self {
53        self.writer = Box::new(writer);
54        self
55    }
56
57    /// Reported output mode.
58    #[must_use]
59    pub const fn mode(&self) -> OutputMode {
60        self.mode
61    }
62
63    /// Reported color preference.
64    #[must_use]
65    pub const fn color(&self) -> ColorMode {
66        self.color
67    }
68
69    /// Write a human-readable message terminated by a newline.
70    ///
71    /// Suppressed when the log level is [`LogLevel::Quiet`] or the output mode
72    /// is [`OutputMode::Json`].
73    pub fn human(&mut self, msg: &str) {
74        if self.mode == OutputMode::Json || self.log_level == LogLevel::Quiet {
75            return;
76        }
77        // Best-effort write; output failures are not actionable for callers.
78        drop(writeln!(self.writer, "{msg}"));
79    }
80
81    /// Serialize a value as a single line of JSON to the output.
82    ///
83    /// Always emits regardless of log level (errors are surfaced via
84    /// [`Self::error`]).
85    ///
86    /// # Errors
87    ///
88    /// Returns an [`Error::JsonParse`] if serialization fails or an
89    /// [`Error::Io`] if writing to the underlying writer fails.
90    pub fn json<T: Serialize>(&mut self, value: &T) -> Result<()> {
91        let line = serde_json::to_string(value)?;
92        writeln!(self.writer, "{line}").map_err(Error::Io)?;
93        Ok(())
94    }
95
96    /// Print an error using the writer's diagnostic conventions.
97    pub fn error(&mut self, err: &dyn std::error::Error) {
98        match self.mode {
99            OutputMode::Json => {
100                drop(self.json(&serde_json::json!({
101                    "error": err.to_string(),
102                })));
103            },
104            OutputMode::Human => {
105                drop(writeln!(self.writer, "error: {err}"));
106            },
107        }
108    }
109
110    /// Flush the underlying writer.
111    ///
112    /// # Errors
113    ///
114    /// Forwards I/O failures from the underlying writer.
115    pub fn flush(&mut self) -> io::Result<()> {
116        self.writer.flush()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    struct Sink(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
125
126    impl Write for Sink {
127        fn write(&mut self, b: &[u8]) -> io::Result<usize> {
128            self.0.lock().unwrap().extend_from_slice(b);
129            Ok(b.len())
130        }
131        fn flush(&mut self) -> io::Result<()> {
132            Ok(())
133        }
134    }
135
136    fn captured(ctx: &Context) -> (Output, std::sync::Arc<std::sync::Mutex<Vec<u8>>>) {
137        let buf: std::sync::Arc<std::sync::Mutex<Vec<u8>>> =
138            std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
139        (Output::for_context(ctx, Sink(buf.clone())), buf)
140    }
141
142    #[test]
143    fn human_writes_one_line() {
144        let ctx = Context::default_for_tests();
145        let (mut out, buf) = captured(&ctx);
146        out.human("hello");
147        let s = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
148        assert_eq!(s, "hello\n");
149    }
150
151    #[test]
152    fn json_serializes_value() {
153        let ctx = Context::default_for_tests();
154        let (mut out, buf) = captured(&ctx);
155        out.json(&serde_json::json!({"k": 1})).unwrap();
156        let s = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
157        assert_eq!(s, "{\"k\":1}\n");
158    }
159}