Skip to main content

standout_dispatch/
stream.rs

1//! The entry stream a handler writes to under the `ndjson` output mode.
2//!
3//! [`EntryStream::emit`] serializes one value as compact JSON and writes it
4//! as one line, followed by a flush, so a consumer reading the pipe sees the
5//! entry when the handler produced it. The framework builds the stream at the
6//! dispatch edge: live, over a [`StreamSink`], when the resolved mode is
7//! `ndjson`; discarding otherwise, in which case `emit` neither serializes
8//! nor writes. Nothing more than line-per-value: no buffering, no
9//! backpressure, no async.
10//!
11//! The sink is the one destination of everything the stream carries: the
12//! handler's entries, then the result or the diagnostic, then the warning
13//! entries. The process edge writes through it to stdout; a capture entry
14//! point hands it a [`StreamCapture`] and reads the bytes back; an output
15//! file override retargets it with [`StreamSink::redirect`] before the
16//! handler runs, so the file receives the whole stream and stdout nothing.
17
18use serde::Serialize;
19use std::cell::RefCell;
20use std::fmt;
21use std::io::Write;
22use std::rc::Rc;
23
24#[derive(Clone)]
25pub struct StreamSink(Rc<RefCell<Box<dyn Write>>>);
26
27impl StreamSink {
28    pub fn new(writer: impl Write + 'static) -> Self {
29        Self(Rc::new(RefCell::new(Box::new(writer))))
30    }
31
32    pub fn process_stdout() -> Self {
33        Self::new(std::io::stdout())
34    }
35
36    /// Replace the destination; every clone of this sink follows.
37    pub fn redirect(&self, writer: impl Write + 'static) {
38        *self.0.borrow_mut() = Box::new(writer);
39    }
40
41    /// For the bytes that follow the handler's entries on the same stream.
42    pub fn with_writer<R>(&self, write: impl FnOnce(&mut dyn Write) -> R) -> R {
43        write(&mut **self.0.borrow_mut())
44    }
45
46    fn write_line(&self, line: &[u8]) -> std::io::Result<()> {
47        self.with_writer(|writer| {
48            writer.write_all(line)?;
49            writer.write_all(b"\n")?;
50            writer.flush()
51        })
52    }
53}
54
55#[derive(Clone, Debug, Default)]
56pub struct StreamCapture(Rc<RefCell<Vec<u8>>>);
57
58impl StreamCapture {
59    pub fn take(&self) -> Vec<u8> {
60        std::mem::take(&mut *self.0.borrow_mut())
61    }
62}
63
64impl Write for StreamCapture {
65    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
66        self.0.borrow_mut().extend_from_slice(buf);
67        Ok(buf.len())
68    }
69
70    fn flush(&mut self) -> std::io::Result<()> {
71        Ok(())
72    }
73}
74
75impl fmt::Debug for StreamSink {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        f.write_str("StreamSink")
78    }
79}
80
81#[derive(Clone, Debug, Default)]
82pub struct EntryStream {
83    sink: Option<StreamSink>,
84}
85
86impl EntryStream {
87    pub fn discarding() -> Self {
88        Self { sink: None }
89    }
90
91    pub fn writing_to(sink: StreamSink) -> Self {
92        Self { sink: Some(sink) }
93    }
94
95    /// True only under `ndjson`.
96    pub fn is_live(&self) -> bool {
97        self.sink.is_some()
98    }
99
100    /// A no-op on a discarding stream; fails when the value does not serialize or the write fails.
101    pub fn emit<T: Serialize + ?Sized>(&self, entry: &T) -> Result<(), StreamError> {
102        let Some(sink) = &self.sink else {
103            return Ok(());
104        };
105        let line = serde_json::to_vec(entry)?;
106        sink.write_line(&line)?;
107        Ok(())
108    }
109}
110
111#[derive(Debug, thiserror::Error)]
112pub enum StreamError {
113    #[error("stream entry does not serialize: {0}")]
114    Serialize(#[from] serde_json::Error),
115    #[error("stream entry could not be written: {0}")]
116    Write(#[from] std::io::Error),
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[derive(Serialize)]
124    struct Entry<'a> {
125        #[serde(rename = "type")]
126        entry_type: &'a str,
127        resource: &'a str,
128    }
129
130    #[test]
131    fn a_live_stream_writes_one_compact_line_per_entry() {
132        let captured = StreamCapture::default();
133        let stream = EntryStream::writing_to(StreamSink::new(captured.clone()));
134        assert!(stream.is_live());
135        stream
136            .emit(&Entry {
137                entry_type: "apply_start",
138                resource: "web",
139            })
140            .unwrap();
141        stream
142            .emit(&Entry {
143                entry_type: "note",
144                resource: "line\nbreak",
145            })
146            .unwrap();
147        assert_eq!(
148            String::from_utf8(captured.take()).unwrap(),
149            "{\"type\":\"apply_start\",\"resource\":\"web\"}\n{\"type\":\"note\",\"resource\":\"line\\nbreak\"}\n"
150        );
151    }
152
153    #[test]
154    fn a_redirected_sink_moves_every_clone_to_the_new_destination() {
155        let first = StreamCapture::default();
156        let second = StreamCapture::default();
157        let sink = StreamSink::new(first.clone());
158        let stream = EntryStream::writing_to(sink.clone());
159        stream.emit(&serde_json::json!({"n": 1})).unwrap();
160        sink.redirect(second.clone());
161        stream.emit(&serde_json::json!({"n": 2})).unwrap();
162        sink.with_writer(|w| w.write_all(b"tail\n")).unwrap();
163        assert_eq!(first.take(), b"{\"n\":1}\n");
164        assert_eq!(second.take(), b"{\"n\":2}\ntail\n");
165    }
166
167    #[test]
168    fn a_discarding_stream_neither_serializes_nor_writes() {
169        struct Unserializable;
170        impl Serialize for Unserializable {
171            fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
172                Err(serde::ser::Error::custom("never asked"))
173            }
174        }
175        let stream = EntryStream::discarding();
176        assert!(!stream.is_live());
177        stream.emit(&Unserializable).unwrap();
178        assert!(EntryStream::default().emit(&Unserializable).is_ok());
179    }
180
181    #[test]
182    fn serialization_and_write_failures_are_distinct_errors() {
183        struct Closed;
184        impl Write for Closed {
185            fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
186                Err(std::io::Error::new(
187                    std::io::ErrorKind::BrokenPipe,
188                    "closed",
189                ))
190            }
191            fn flush(&mut self) -> std::io::Result<()> {
192                Ok(())
193            }
194        }
195        let stream = EntryStream::writing_to(StreamSink::new(Closed));
196        let write = stream.emit(&serde_json::json!({})).unwrap_err();
197        assert!(matches!(write, StreamError::Write(_)), "{write}");
198
199        let mut map = std::collections::HashMap::new();
200        map.insert((1u8, 2u8), 3u8);
201        let stream = EntryStream::writing_to(StreamSink::new(Vec::new()));
202        let serialize = stream.emit(&map).unwrap_err();
203        assert!(
204            matches!(serialize, StreamError::Serialize(_)),
205            "{serialize}"
206        );
207    }
208}