Skip to main content

docspec_json/
struson_backend.rs

1//! [`JsonBackend`] adapter for `struson::JsonStreamWriter`.
2
3use std::io::Write;
4
5use struson::writer::{JsonStreamWriter, JsonWriter as _};
6
7use crate::JsonBackend;
8use docspec_core::{Error, Result};
9
10/// Adapter wrapping `struson::JsonStreamWriter<W>`.
11///
12/// Forwards each [`JsonBackend`] method to the corresponding struson API call.
13/// Errors from struson are converted to [`docspec_core::Error::Io`].
14pub struct StrusonBackend<W: Write> {
15    writer: JsonStreamWriter<W>,
16}
17
18impl<W: Write> StrusonBackend<W> {
19    /// Create a new `StrusonBackend` wrapping the given writer.
20    #[inline]
21    pub fn new(writer: W) -> Self {
22        Self {
23            writer: JsonStreamWriter::new(writer),
24        }
25    }
26}
27
28impl<W: Write> JsonBackend for StrusonBackend<W> {
29    type Output = W;
30
31    #[inline]
32    fn begin_array(&mut self) -> Result<()> {
33        self.writer.begin_array().map_err(Error::from)
34    }
35
36    #[inline]
37    fn begin_object(&mut self) -> Result<()> {
38        self.writer.begin_object().map_err(Error::from)
39    }
40
41    #[inline]
42    fn end_array(&mut self) -> Result<()> {
43        self.writer.end_array().map_err(Error::from)
44    }
45
46    #[inline]
47    fn end_object(&mut self) -> Result<()> {
48        self.writer.end_object().map_err(Error::from)
49    }
50
51    #[inline]
52    fn finish(self) -> Result<W> {
53        self.writer.finish_document().map_err(Error::from)
54    }
55
56    #[inline]
57    fn write_bool(&mut self, b: bool) -> Result<()> {
58        self.writer.bool_value(b).map_err(Error::from)
59    }
60
61    #[inline]
62    fn write_name(&mut self, name: &str) -> Result<()> {
63        self.writer.name(name).map_err(Error::from)
64    }
65
66    #[inline]
67    fn write_null(&mut self) -> Result<()> {
68        self.writer.null_value().map_err(Error::from)
69    }
70
71    #[inline]
72    fn write_number(&mut self, n: u32) -> Result<()> {
73        self.writer.number_value(n).map_err(Error::from)
74    }
75
76    #[inline]
77    fn write_string(&mut self, s: &str) -> Result<()> {
78        self.writer.string_value(s).map_err(Error::from)
79    }
80
81    #[inline]
82    fn write_string_streaming<F>(&mut self, f: F) -> Result<()>
83    where
84        F: FnOnce(&mut dyn Write) -> std::io::Result<()>,
85    {
86        use struson::writer::StringValueWriter as _;
87
88        let mut svw = self.writer.string_value_writer().map_err(Error::from)?;
89        let inner_result = f(&mut svw);
90        let finish_result = svw.finish_value().map_err(Error::from);
91        inner_result.map_err(Error::from)?;
92        finish_result
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use core::cell::RefCell;
100    use std::rc::Rc;
101
102    struct ErrorWriter;
103
104    impl Write for ErrorWriter {
105        fn flush(&mut self) -> std::io::Result<()> {
106            Ok(())
107        }
108
109        fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
110            Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "test"))
111        }
112    }
113
114    #[test]
115    fn struson_backend_error_propagates_as_io_err() {
116        let mut b = StrusonBackend::new(ErrorWriter);
117        let result = b.begin_object();
118        assert!(matches!(result, Err(Error::Io { .. })));
119    }
120
121    #[test]
122    fn struson_backend_error_writer_flush_succeeds() {
123        let mut w = ErrorWriter;
124        assert!(w.flush().is_ok());
125    }
126
127    #[test]
128    fn struson_backend_finish_returns_underlying_writer() {
129        let mut b = StrusonBackend::new(Vec::new());
130        assert!(b.begin_array().is_ok());
131        assert!(b.end_array().is_ok());
132        let result = b.finish();
133        assert!(result.is_ok());
134        let bytes = result.unwrap_or_default();
135        assert!(bytes == b"[]");
136    }
137
138    #[test]
139    fn struson_backend_writes_array_of_values() {
140        let mut b = StrusonBackend::new(Vec::new());
141        assert!(b.begin_array().is_ok());
142        assert!(b.write_number(1).is_ok());
143        assert!(b.write_bool(true).is_ok());
144        assert!(b.write_null().is_ok());
145        assert!(b.write_string("x").is_ok());
146        assert!(b.end_array().is_ok());
147        let result = b.finish();
148        assert!(result.is_ok());
149        let bytes = result.unwrap_or_default();
150        assert!(bytes == br#"[1,true,null,"x"]"#);
151    }
152
153    #[test]
154    fn struson_backend_writes_empty_object() {
155        let mut b = StrusonBackend::new(Vec::new());
156        assert!(b.begin_object().is_ok());
157        assert!(b.end_object().is_ok());
158        let result = b.finish();
159        assert!(result.is_ok());
160        let bytes = result.unwrap_or_default();
161        assert!(bytes == b"{}");
162    }
163
164    #[test]
165    fn struson_backend_writes_nested_structure() {
166        let mut b = StrusonBackend::new(Vec::new());
167        assert!(b.begin_object().is_ok());
168        assert!(b.write_name("a").is_ok());
169        assert!(b.begin_array().is_ok());
170        assert!(b.write_number(1).is_ok());
171        assert!(b.begin_object().is_ok());
172        assert!(b.write_name("b").is_ok());
173        assert!(b.write_bool(true).is_ok());
174        assert!(b.end_object().is_ok());
175        assert!(b.end_array().is_ok());
176        assert!(b.end_object().is_ok());
177        let result = b.finish();
178        assert!(result.is_ok());
179        let bytes = result.unwrap_or_default();
180        assert!(bytes == br#"{"a":[1,{"b":true}]}"#);
181    }
182
183    #[test]
184    fn struson_backend_writes_simple_key_value() {
185        let mut b = StrusonBackend::new(Vec::new());
186        assert!(b.begin_object().is_ok());
187        assert!(b.write_name("k").is_ok());
188        assert!(b.write_string("v").is_ok());
189        assert!(b.end_object().is_ok());
190        let result = b.finish();
191        assert!(result.is_ok());
192        let bytes = result.unwrap_or_default();
193        assert!(bytes == br#"{"k":"v"}"#);
194    }
195
196    #[test]
197    fn streaming_writes_simple_string() {
198        let mut b = StrusonBackend::new(Vec::new());
199        assert!(b.write_string_streaming(|w| w.write_all(b"hello")).is_ok());
200        let result = b.finish();
201        assert!(result.is_ok());
202        let bytes = result.unwrap_or_default();
203        assert!(bytes == br#""hello""#);
204    }
205
206    #[test]
207    fn streaming_writes_base64_safe_chars() {
208        let mut b = StrusonBackend::new(Vec::new());
209        assert!(b
210            .write_string_streaming(|w| w.write_all(b"data:image/png;base64,iVBORw=="))
211            .is_ok());
212        let result = b.finish();
213        assert!(result.is_ok());
214        let bytes = result.unwrap_or_default();
215        assert!(bytes == br#""data:image/png;base64,iVBORw==""#);
216    }
217
218    #[test]
219    fn streaming_writes_json_special_chars_are_escaped() {
220        let mut b = StrusonBackend::new(Vec::new());
221        assert!(b
222            .write_string_streaming(|w| w.write_all(br#""quoted""#))
223            .is_ok());
224        let result = b.finish();
225        assert!(result.is_ok());
226        let bytes = result.unwrap_or_default();
227        assert!(bytes == br#""\"quoted\"""#);
228    }
229
230    #[test]
231    fn streaming_writes_multi_chunk() {
232        let mut b = StrusonBackend::new(Vec::new());
233        assert!(b
234            .write_string_streaming(|w| {
235                w.write_all(b"one")?;
236                w.write_all(b"-")?;
237                w.write_all(b"two")?;
238                w.write_all(b"-")?;
239                w.write_all(b"three")
240            })
241            .is_ok());
242        let result = b.finish();
243        assert!(result.is_ok());
244        let bytes = result.unwrap_or_default();
245        assert!(bytes == br#""one-two-three""#);
246    }
247
248    #[test]
249    fn streaming_writes_error_in_closure_still_finishes_value() {
250        let mut b = StrusonBackend::new(Vec::new());
251        assert!(b.begin_object().is_ok());
252        assert!(b.write_name("failed").is_ok());
253
254        let result = b.write_string_streaming(|_| Err(std::io::Error::other("closure failed")));
255        assert!(result.is_err());
256
257        let after_key =
258            std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| b.write_name("after")));
259        #[allow(clippy::expect_used)]
260        let write_result = after_key.expect("asserted catch_unwind returned Ok");
261        assert!(write_result.is_ok());
262
263        assert!(b.write_string("ok").is_ok());
264        assert!(b.end_object().is_ok());
265        let finish_result = b.finish();
266        assert!(finish_result.is_ok());
267        let bytes = finish_result.unwrap_or_default();
268        assert!(bytes == br#"{"failed":"","after":"ok"}"#);
269    }
270
271    /// Tracks each write call to the inner writer.
272    struct CallCountingWriter {
273        write_calls: Rc<RefCell<Vec<usize>>>,
274        buffer: Vec<u8>,
275    }
276
277    impl CallCountingWriter {
278        fn new() -> Self {
279            Self {
280                write_calls: Rc::new(RefCell::new(Vec::new())),
281                buffer: Vec::new(),
282            }
283        }
284
285        fn write_calls(&self) -> Vec<usize> {
286            self.write_calls.borrow().clone()
287        }
288
289        fn total_bytes(&self) -> usize {
290            self.write_calls.borrow().iter().sum()
291        }
292    }
293
294    impl Write for CallCountingWriter {
295        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
296            self.write_calls.borrow_mut().push(buf.len());
297            self.buffer.extend_from_slice(buf);
298            Ok(buf.len())
299        }
300
301        fn flush(&mut self) -> std::io::Result<()> {
302            Ok(())
303        }
304    }
305
306    #[test]
307    fn override_chunks_through_inner_writer() {
308        // This test proves that write_string_streaming override is actually used,
309        // not the default buffering impl. If the override is wired correctly,
310        // the inner writer should see multiple separate write calls (streaming).
311        // If buffering was used, we'd see one large write.
312
313        let mut counter = CallCountingWriter::new();
314        let mut b = StrusonBackend::new(&mut counter);
315
316        // Write 1024 bytes in 4 separate chunks of 256 bytes each.
317        // This is large enough that if buffering was used, we'd see ONE write of 1024 bytes.
318        // If streaming is used (override is wired), we should see multiple smaller writes.
319        let chunk = vec![b'x'; 256];
320        assert!(b
321            .write_string_streaming(|w| {
322                w.write_all(&chunk)?;
323                w.write_all(&chunk)?;
324                w.write_all(&chunk)?;
325                w.write_all(&chunk)
326            })
327            .is_ok());
328
329        let write_calls = counter.write_calls();
330        let total_bytes = counter.total_bytes();
331
332        // Verify total bytes written is at least 1024 (the content we wrote).
333        // The actual total may be slightly more due to JSON escaping/framing.
334        assert!(
335            total_bytes >= 1024,
336            "Expected at least 1024 bytes written, got {total_bytes}"
337        );
338
339        // Verify that the largest single write is less than the total.
340        // This proves streaming: if buffering was used, we'd see ONE write of ~1024 bytes.
341        // If streaming is used, we see multiple smaller writes.
342        let max_write = write_calls.iter().max().copied().unwrap_or(0);
343        assert!(
344             max_write < total_bytes,
345             "Expected multiple writes (streaming), but got one large write: max={max_write}, total={total_bytes}"
346         );
347
348        // Verify we got multiple write calls (not just one).
349        assert!(
350            write_calls.len() > 1,
351            "Expected multiple write calls (streaming), got {}",
352            write_calls.len()
353        );
354    }
355}