Skip to main content

datavalue_rs/
emit.rs

1//! Native JSON emitter for [`DataValue`] and [`OwnedDataValue`].
2//!
3//! Bypasses the `serde_json::to_string` path. The serde route pays trait
4//! dispatch per node and a per-byte string-escape loop; emitting directly
5//! into a buffer with `ryu` / `itoa` and a SWAR-driven escape scan lands
6//! closer to the bespoke emitters in `json-rust` / `simd_json`.
7//!
8//! The same writers feed three sinks — `Vec<u8>` for [`DataValue::write_json_into`],
9//! `fmt::Formatter` for the [`fmt::Display`] impls, and an indenting wrapper
10//! for [`DataValue::pretty`] — through the [`JsonSink`] trait below.
11//!
12//! The `Serialize` impl in [`crate::ser`] is still the right entry point
13//! when feeding non-JSON serde sinks (msgpack, flexbuffers, etc.).
14
15use core::fmt;
16
17use crate::number::NumberValue;
18use crate::owned::OwnedDataValue;
19use crate::value::DataValue;
20
21/// Sink abstraction over `Vec<u8>` and `fmt::Formatter`. Bytes pushed are
22/// always valid UTF-8 (numbers are ASCII; strings are passed through from
23/// `&str` sources; escapes are ASCII), so the str adapter is sound.
24///
25/// Contract: every `write_bytes` call passes a *complete* valid-UTF-8 chunk
26/// (string runs are sliced at escape hits, which are ASCII, so run boundaries
27/// are char boundaries; everything else written is ASCII). `FormatterSink`'s
28/// staging buffer relies on this — it never splits a chunk, so the buffer
29/// content is always a concatenation of whole chunks and stays valid UTF-8.
30pub(crate) trait JsonSink {
31    type Error;
32    fn write_bytes(&mut self, b: &[u8]) -> Result<(), Self::Error>;
33    fn write_byte(&mut self, b: u8) -> Result<(), Self::Error>;
34}
35
36impl JsonSink for Vec<u8> {
37    type Error = core::convert::Infallible;
38    #[inline]
39    fn write_bytes(&mut self, b: &[u8]) -> Result<(), Self::Error> {
40        self.extend_from_slice(b);
41        Ok(())
42    }
43    #[inline]
44    fn write_byte(&mut self, b: u8) -> Result<(), Self::Error> {
45        self.push(b);
46        Ok(())
47    }
48}
49
50impl JsonSink for bumpalo::collections::Vec<'_, u8> {
51    type Error = core::convert::Infallible;
52    #[inline]
53    fn write_bytes(&mut self, b: &[u8]) -> Result<(), Self::Error> {
54        // Not extend_from_slice: bumpalo's Vec is a pre-specialization std
55        // fork, so its extend_from_slice copies element-by-element through a
56        // cloned iterator (measured ~2x slower on string-heavy emit). Reserve
57        // once and memcpy.
58        self.reserve(b.len());
59        let len = self.len();
60        // SAFETY: `reserve` guarantees capacity for `len + b.len()`; the
61        // source and destination don't overlap (`b` borrows input strings or
62        // static tables, never this vec's buffer).
63        unsafe {
64            core::ptr::copy_nonoverlapping(b.as_ptr(), self.as_mut_ptr().add(len), b.len());
65            self.set_len(len + b.len());
66        }
67        Ok(())
68    }
69    #[inline]
70    fn write_byte(&mut self, b: u8) -> Result<(), Self::Error> {
71        self.push(b);
72        Ok(())
73    }
74}
75
76/// Finish an arena emit: reclaim unused BumpVec capacity, then reinterpret
77/// the bytes as `&str`.
78///
79/// SAFETY (of the contained `from_utf8_unchecked`): the emitters only write
80/// valid UTF-8 — ASCII structural bytes/escapes/numbers and `&str` payload
81/// runs (see the `JsonSink` contract).
82#[inline]
83fn into_arena_str<'b>(mut out: bumpalo::collections::Vec<'b, u8>) -> &'b str {
84    // Trim growth slack. The vec is the arena's most recent allocation
85    // (emitting allocates nothing else), so bumpalo reclaims in place;
86    // measured cost is noise-level because it only copies when more than
87    // half the capacity would be reclaimed.
88    out.shrink_to_fit();
89    unsafe { core::str::from_utf8_unchecked(out.into_bump_slice()) }
90}
91
92/// Staging capacity for `FormatterSink`. Without it, every structural byte
93/// (`[ ] { } , : "`) would be its own virtual `fmt::Write::write_str` call;
94/// with it, output reaches the formatter in ~128-byte runs.
95const FMT_STAGING: usize = 128;
96
97struct FormatterSink<'a, 'b> {
98    f: &'a mut fmt::Formatter<'b>,
99    buf: [u8; FMT_STAGING],
100    len: usize,
101}
102
103impl<'a, 'b> FormatterSink<'a, 'b> {
104    fn new(f: &'a mut fmt::Formatter<'b>) -> Self {
105        FormatterSink {
106            f,
107            buf: [0; FMT_STAGING],
108            len: 0,
109        }
110    }
111
112    fn flush(&mut self) -> fmt::Result {
113        if self.len > 0 {
114            // SAFETY: the buffer holds a concatenation of complete chunks,
115            // each valid UTF-8 (see the JsonSink contract); chunks are never
116            // split across flushes.
117            let s = unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) };
118            self.f.write_str(s)?;
119            self.len = 0;
120        }
121        Ok(())
122    }
123}
124
125impl<'a, 'b> JsonSink for FormatterSink<'a, 'b> {
126    type Error = fmt::Error;
127    #[inline]
128    fn write_bytes(&mut self, b: &[u8]) -> Result<(), Self::Error> {
129        if self.len + b.len() > FMT_STAGING {
130            self.flush()?;
131            if b.len() >= FMT_STAGING {
132                // SAFETY: chunks are complete valid UTF-8 (JsonSink contract).
133                let s = unsafe { core::str::from_utf8_unchecked(b) };
134                return self.f.write_str(s);
135            }
136        }
137        self.buf[self.len..self.len + b.len()].copy_from_slice(b);
138        self.len += b.len();
139        Ok(())
140    }
141    #[inline]
142    fn write_byte(&mut self, b: u8) -> Result<(), Self::Error> {
143        debug_assert!(b.is_ascii());
144        if self.len == FMT_STAGING {
145            self.flush()?;
146        }
147        self.buf[self.len] = b;
148        self.len += 1;
149        Ok(())
150    }
151}
152
153#[inline]
154fn write_escaped_str<S: JsonSink>(out: &mut S, s: &str) -> Result<(), S::Error> {
155    out.write_byte(b'"')?;
156    let bytes = s.as_bytes();
157    let mut run_start = 0;
158
159    // Scan-and-copy runs between escapes. The scan is the shared
160    // `crate::simd` helper: SWAR for slices under 32 bytes (the typical
161    // short JSON string — its criteria and mask are identical to the old
162    // inlined loop), the 16-byte SIMD stride for longer ones.
163    while let Some(off) = crate::simd::find_string_terminator(&bytes[run_start..]) {
164        let hit = run_start + off;
165        if hit > run_start {
166            out.write_bytes(&bytes[run_start..hit])?;
167        }
168        write_escape_byte(out, bytes[hit])?;
169        run_start = hit + 1;
170    }
171    if run_start < bytes.len() {
172        out.write_bytes(&bytes[run_start..])?;
173    }
174    out.write_byte(b'"')
175}
176
177/// DateTime arm: the 20-byte ISO buffer is pure ASCII with no JSON-special
178/// bytes, so it goes to the sink as quote + raw bytes + quote — no heap
179/// `String`, no escape scan. Years outside 0..=9999 fall back to chrono's
180/// RFC3339 formatter (heap) through the escaped path.
181#[cfg(feature = "datetime")]
182fn write_datetime<S: JsonSink>(
183    out: &mut S,
184    d: &crate::datetime::DataDateTime,
185) -> Result<(), S::Error> {
186    match d.iso_secs_buf() {
187        Some(buf) => {
188            out.write_byte(b'"')?;
189            out.write_bytes(&buf)?;
190            out.write_byte(b'"')
191        }
192        None => write_escaped_str(out, &d.to_iso_string()),
193    }
194}
195
196/// Duration arm: `Xd:Xh:Xm:Xs` is digits, `-`, letters, and `:` — never
197/// escaped, so it streams via itoa without the intermediate `String`.
198#[cfg(feature = "datetime")]
199fn write_duration<S: JsonSink>(
200    out: &mut S,
201    d: &crate::datetime::DataDuration,
202) -> Result<(), S::Error> {
203    let (days, hours, minutes, seconds) = d.dhms();
204    let mut b = itoa::Buffer::new();
205    out.write_byte(b'"')?;
206    out.write_bytes(b.format(days).as_bytes())?;
207    out.write_bytes(b"d:")?;
208    out.write_bytes(b.format(hours).as_bytes())?;
209    out.write_bytes(b"h:")?;
210    out.write_bytes(b.format(minutes).as_bytes())?;
211    out.write_bytes(b"m:")?;
212    out.write_bytes(b.format(seconds).as_bytes())?;
213    out.write_bytes(b"s\"")
214}
215
216/// Tensor arm: `{"tensor":{"dtype":"f32","shape":[2,3],"data":"<base64>"}}`.
217/// The dtype name and the base64 payload are pure ASCII with no
218/// JSON-special bytes, so both stream raw between quotes; the payload is
219/// chunked through the sink rather than materialised.
220#[cfg(feature = "tensor")]
221fn write_tensor<S: JsonSink>(
222    out: &mut S,
223    t: crate::tensor::DataTensor<'_>,
224) -> Result<(), S::Error> {
225    out.write_bytes(b"{\"")?;
226    out.write_bytes(crate::tensor::DataTensor::JSON_TAG.as_bytes())?;
227    out.write_bytes(b"\":{\"dtype\":\"")?;
228    out.write_bytes(t.dtype().name().as_bytes())?;
229    out.write_bytes(b"\",\"shape\":[")?;
230    let mut buf = itoa::Buffer::new();
231    for (i, d) in t.shape().iter().enumerate() {
232        if i > 0 {
233            out.write_byte(b',')?;
234        }
235        out.write_bytes(buf.format(*d).as_bytes())?;
236    }
237    out.write_bytes(b"],\"data\":\"")?;
238    crate::base64::encode_into(out, t.data())?;
239    out.write_bytes(b"\"}}")
240}
241
242/// Pretty tensor arm. Same shape `serde_json::to_string_pretty` gives the
243/// tagged object: one dimension per line, `[]` for a 0-d shape.
244#[cfg(feature = "tensor")]
245fn write_tensor_pretty<S: JsonSink>(
246    out: &mut S,
247    t: crate::tensor::DataTensor<'_>,
248    depth: usize,
249) -> Result<(), S::Error> {
250    out.write_bytes(b"{\n")?;
251    write_indent(out, depth + 1)?;
252    out.write_byte(b'"')?;
253    out.write_bytes(crate::tensor::DataTensor::JSON_TAG.as_bytes())?;
254    out.write_bytes(b"\": {\n")?;
255    write_indent(out, depth + 2)?;
256    out.write_bytes(b"\"dtype\": \"")?;
257    out.write_bytes(t.dtype().name().as_bytes())?;
258    out.write_bytes(b"\",\n")?;
259    write_indent(out, depth + 2)?;
260    out.write_bytes(b"\"shape\": [")?;
261    if !t.shape().is_empty() {
262        let mut buf = itoa::Buffer::new();
263        for (i, d) in t.shape().iter().enumerate() {
264            if i > 0 {
265                out.write_byte(b',')?;
266            }
267            out.write_byte(b'\n')?;
268            write_indent(out, depth + 3)?;
269            out.write_bytes(buf.format(*d).as_bytes())?;
270        }
271        out.write_byte(b'\n')?;
272        write_indent(out, depth + 2)?;
273    }
274    out.write_bytes(b"],\n")?;
275    write_indent(out, depth + 2)?;
276    out.write_bytes(b"\"data\": \"")?;
277    crate::base64::encode_into(out, t.data())?;
278    out.write_bytes(b"\"\n")?;
279    write_indent(out, depth + 1)?;
280    out.write_bytes(b"}\n")?;
281    write_indent(out, depth)?;
282    out.write_byte(b'}')
283}
284
285#[inline]
286fn write_escape_byte<S: JsonSink>(out: &mut S, b: u8) -> Result<(), S::Error> {
287    match b {
288        b'"' => out.write_bytes(b"\\\""),
289        b'\\' => out.write_bytes(b"\\\\"),
290        b'\n' => out.write_bytes(b"\\n"),
291        b'\r' => out.write_bytes(b"\\r"),
292        b'\t' => out.write_bytes(b"\\t"),
293        0x08 => out.write_bytes(b"\\b"),
294        0x0C => out.write_bytes(b"\\f"),
295        c => {
296            // Other control bytes (< 0x20 not named above) use \u00XX. The
297            // high byte is always 0 here.
298            const HEX: &[u8; 16] = b"0123456789abcdef";
299            out.write_bytes(b"\\u00")?;
300            out.write_byte(HEX[((c >> 4) & 0x0F) as usize])?;
301            out.write_byte(HEX[(c & 0x0F) as usize])
302        }
303    }
304}
305
306#[inline]
307fn write_number<S: JsonSink>(out: &mut S, n: NumberValue) -> Result<(), S::Error> {
308    match n {
309        NumberValue::Integer(i) => {
310            let mut buf = itoa::Buffer::new();
311            out.write_bytes(buf.format(i).as_bytes())
312        }
313        NumberValue::Float(f) => {
314            if !f.is_finite() {
315                // serde_json emits non-finite floats as `null` to keep
316                // output valid JSON. Match that.
317                return out.write_bytes(b"null");
318            }
319            let mut buf = ryu::Buffer::new();
320            out.write_bytes(buf.format_finite(f).as_bytes())
321        }
322    }
323}
324
325// ---- Compact emit (no whitespace) ------------------------------------------------
326
327fn write_data_value<S: JsonSink>(out: &mut S, v: &DataValue<'_>) -> Result<(), S::Error> {
328    match *v {
329        DataValue::Null => out.write_bytes(b"null"),
330        DataValue::Bool(true) => out.write_bytes(b"true"),
331        DataValue::Bool(false) => out.write_bytes(b"false"),
332        DataValue::Number(n) => write_number(out, n),
333        DataValue::String(s) => write_escaped_str(out, s),
334        DataValue::Array(items) => {
335            out.write_byte(b'[')?;
336            let mut first = true;
337            for item in items {
338                if !first {
339                    out.write_byte(b',')?;
340                }
341                first = false;
342                write_data_value(out, item)?;
343            }
344            out.write_byte(b']')
345        }
346        DataValue::Object(pairs) => {
347            out.write_byte(b'{')?;
348            let mut first = true;
349            for (k, v) in pairs {
350                if !first {
351                    out.write_byte(b',')?;
352                }
353                first = false;
354                write_escaped_str(out, k)?;
355                out.write_byte(b':')?;
356                write_data_value(out, v)?;
357            }
358            out.write_byte(b'}')
359        }
360        #[cfg(feature = "datetime")]
361        DataValue::DateTime(d) => write_datetime(out, &d),
362        #[cfg(feature = "datetime")]
363        DataValue::Duration(d) => write_duration(out, &d),
364        #[cfg(feature = "tensor")]
365        DataValue::Tensor(t) => write_tensor(out, *t),
366    }
367}
368
369fn write_owned_value<S: JsonSink>(out: &mut S, v: &OwnedDataValue) -> Result<(), S::Error> {
370    match v {
371        OwnedDataValue::Null => out.write_bytes(b"null"),
372        OwnedDataValue::Bool(true) => out.write_bytes(b"true"),
373        OwnedDataValue::Bool(false) => out.write_bytes(b"false"),
374        OwnedDataValue::Number(n) => write_number(out, *n),
375        OwnedDataValue::String(s) => write_escaped_str(out, s),
376        OwnedDataValue::Array(items) => {
377            out.write_byte(b'[')?;
378            let mut first = true;
379            for item in items {
380                if !first {
381                    out.write_byte(b',')?;
382                }
383                first = false;
384                write_owned_value(out, item)?;
385            }
386            out.write_byte(b']')
387        }
388        OwnedDataValue::Object(pairs) => {
389            out.write_byte(b'{')?;
390            let mut first = true;
391            for (k, v) in pairs {
392                if !first {
393                    out.write_byte(b',')?;
394                }
395                first = false;
396                write_escaped_str(out, k)?;
397                out.write_byte(b':')?;
398                write_owned_value(out, v)?;
399            }
400            out.write_byte(b'}')
401        }
402        #[cfg(feature = "datetime")]
403        OwnedDataValue::DateTime(d) => write_datetime(out, d),
404        #[cfg(feature = "datetime")]
405        OwnedDataValue::Duration(d) => write_duration(out, d),
406        #[cfg(feature = "tensor")]
407        OwnedDataValue::Tensor(t) => write_tensor(out, t.view()),
408    }
409}
410
411// ---- Pretty emit (two-space indent, matches serde_json::to_string_pretty) -------
412
413#[inline]
414fn write_indent<S: JsonSink>(out: &mut S, depth: usize) -> Result<(), S::Error> {
415    // Two spaces per level. Keep a reasonably long literal so most depths
416    // need a single write.
417    const SPACES: &[u8; 64] = b"                                                                ";
418    let mut remaining = depth * 2;
419    while remaining > 0 {
420        let chunk = remaining.min(SPACES.len());
421        out.write_bytes(&SPACES[..chunk])?;
422        remaining -= chunk;
423    }
424    Ok(())
425}
426
427fn write_data_value_pretty<S: JsonSink>(
428    out: &mut S,
429    v: &DataValue<'_>,
430    depth: usize,
431) -> Result<(), S::Error> {
432    match *v {
433        DataValue::Null => out.write_bytes(b"null"),
434        DataValue::Bool(true) => out.write_bytes(b"true"),
435        DataValue::Bool(false) => out.write_bytes(b"false"),
436        DataValue::Number(n) => write_number(out, n),
437        DataValue::String(s) => write_escaped_str(out, s),
438        DataValue::Array(items) => {
439            if items.is_empty() {
440                return out.write_bytes(b"[]");
441            }
442            out.write_byte(b'[')?;
443            for (i, item) in items.iter().enumerate() {
444                if i > 0 {
445                    out.write_byte(b',')?;
446                }
447                out.write_byte(b'\n')?;
448                write_indent(out, depth + 1)?;
449                write_data_value_pretty(out, item, depth + 1)?;
450            }
451            out.write_byte(b'\n')?;
452            write_indent(out, depth)?;
453            out.write_byte(b']')
454        }
455        DataValue::Object(pairs) => {
456            if pairs.is_empty() {
457                return out.write_bytes(b"{}");
458            }
459            out.write_byte(b'{')?;
460            for (i, (k, v)) in pairs.iter().enumerate() {
461                if i > 0 {
462                    out.write_byte(b',')?;
463                }
464                out.write_byte(b'\n')?;
465                write_indent(out, depth + 1)?;
466                write_escaped_str(out, k)?;
467                out.write_bytes(b": ")?;
468                write_data_value_pretty(out, v, depth + 1)?;
469            }
470            out.write_byte(b'\n')?;
471            write_indent(out, depth)?;
472            out.write_byte(b'}')
473        }
474        #[cfg(feature = "datetime")]
475        DataValue::DateTime(d) => write_datetime(out, &d),
476        #[cfg(feature = "datetime")]
477        DataValue::Duration(d) => write_duration(out, &d),
478        #[cfg(feature = "tensor")]
479        DataValue::Tensor(t) => write_tensor_pretty(out, *t, depth),
480    }
481}
482
483fn write_owned_value_pretty<S: JsonSink>(
484    out: &mut S,
485    v: &OwnedDataValue,
486    depth: usize,
487) -> Result<(), S::Error> {
488    match v {
489        OwnedDataValue::Null => out.write_bytes(b"null"),
490        OwnedDataValue::Bool(true) => out.write_bytes(b"true"),
491        OwnedDataValue::Bool(false) => out.write_bytes(b"false"),
492        OwnedDataValue::Number(n) => write_number(out, *n),
493        OwnedDataValue::String(s) => write_escaped_str(out, s),
494        OwnedDataValue::Array(items) => {
495            if items.is_empty() {
496                return out.write_bytes(b"[]");
497            }
498            out.write_byte(b'[')?;
499            for (i, item) in items.iter().enumerate() {
500                if i > 0 {
501                    out.write_byte(b',')?;
502                }
503                out.write_byte(b'\n')?;
504                write_indent(out, depth + 1)?;
505                write_owned_value_pretty(out, item, depth + 1)?;
506            }
507            out.write_byte(b'\n')?;
508            write_indent(out, depth)?;
509            out.write_byte(b']')
510        }
511        OwnedDataValue::Object(pairs) => {
512            if pairs.is_empty() {
513                return out.write_bytes(b"{}");
514            }
515            out.write_byte(b'{')?;
516            for (i, (k, v)) in pairs.iter().enumerate() {
517                if i > 0 {
518                    out.write_byte(b',')?;
519                }
520                out.write_byte(b'\n')?;
521                write_indent(out, depth + 1)?;
522                write_escaped_str(out, k)?;
523                out.write_bytes(b": ")?;
524                write_owned_value_pretty(out, v, depth + 1)?;
525            }
526            out.write_byte(b'\n')?;
527            write_indent(out, depth)?;
528            out.write_byte(b'}')
529        }
530        #[cfg(feature = "datetime")]
531        OwnedDataValue::DateTime(d) => write_datetime(out, d),
532        #[cfg(feature = "datetime")]
533        OwnedDataValue::Duration(d) => write_duration(out, d),
534        #[cfg(feature = "tensor")]
535        OwnedDataValue::Tensor(t) => write_tensor_pretty(out, t.view(), depth),
536    }
537}
538
539// ---- Public API on DataValue ---------------------------------------------------
540
541impl DataValue<'_> {
542    /// Append the compact JSON encoding of this value to `out`. Useful when
543    /// you want to amortize allocation across many values into a shared buffer.
544    /// For one-shot string conversion, use the [`fmt::Display`] impl
545    /// (`v.to_string()` / `format!("{v}")` / `println!("{v}")`).
546    pub fn write_json_into(&self, out: &mut Vec<u8>) {
547        let _ = write_data_value(out, self);
548    }
549
550    /// Pretty-print wrapper. `format!("{}", v.pretty())` produces the same
551    /// two-space-indented layout as `serde_json::to_string_pretty`.
552    ///
553    /// ```
554    /// use bumpalo::Bump;
555    /// use datavalue_rs::DataValue;
556    ///
557    /// let arena = Bump::new();
558    /// let v = DataValue::from_str(r#"{"a":1}"#, &arena).unwrap();
559    /// assert_eq!(v.pretty().to_string(), "{\n  \"a\": 1\n}");
560    /// ```
561    pub fn pretty(&self) -> Pretty<'_, DataValue<'_>> {
562        Pretty(self)
563    }
564
565    /// Append the pretty JSON encoding of this value to `out`.
566    pub fn write_json_pretty_into(&self, out: &mut Vec<u8>) {
567        let _ = write_data_value_pretty(out, self, 0);
568    }
569
570    /// Emit the compact JSON encoding directly into `arena` and return the
571    /// arena-resident string — no heap allocation. The single-copy path for
572    /// consumers that render values to text living alongside the values
573    /// (e.g. a `&str` result slot in the same evaluation arena).
574    ///
575    /// ```
576    /// use bumpalo::Bump;
577    /// use datavalue_rs::DataValue;
578    ///
579    /// let arena = Bump::new();
580    /// let v = DataValue::from_str(r#"{"a":[1,2.5,"hi"]}"#, &arena).unwrap();
581    /// let s: &str = v.to_json_str_in(&arena);
582    /// assert_eq!(s, r#"{"a":[1,2.5,"hi"]}"#);
583    /// ```
584    pub fn to_json_str_in<'b>(&self, arena: &'b bumpalo::Bump) -> &'b str {
585        let mut out = bumpalo::collections::Vec::new_in(arena);
586        let _ = write_data_value(&mut out, self);
587        into_arena_str(out)
588    }
589
590    /// Pretty sibling of [`DataValue::to_json_str_in`].
591    pub fn to_json_pretty_str_in<'b>(&self, arena: &'b bumpalo::Bump) -> &'b str {
592        let mut out = bumpalo::collections::Vec::new_in(arena);
593        let _ = write_data_value_pretty(&mut out, self, 0);
594        into_arena_str(out)
595    }
596}
597
598impl OwnedDataValue {
599    /// Append the compact JSON encoding of this value to `out`. See
600    /// [`DataValue::write_json_into`]; this is the owned-side mirror.
601    pub fn write_json_into(&self, out: &mut Vec<u8>) {
602        let _ = write_owned_value(out, self);
603    }
604
605    /// Pretty-print wrapper; see [`DataValue::pretty`].
606    ///
607    /// ```
608    /// use datavalue_rs::OwnedDataValue;
609    ///
610    /// let v: OwnedDataValue = r#"{"a":1}"#.parse().unwrap();
611    /// assert_eq!(v.pretty().to_string(), "{\n  \"a\": 1\n}");
612    /// ```
613    pub fn pretty(&self) -> Pretty<'_, OwnedDataValue> {
614        Pretty(self)
615    }
616
617    /// Append the pretty JSON encoding of this value to `out`.
618    pub fn write_json_pretty_into(&self, out: &mut Vec<u8>) {
619        let _ = write_owned_value_pretty(out, self, 0);
620    }
621
622    /// Emit the compact JSON encoding directly into `arena`; see
623    /// [`DataValue::to_json_str_in`]. This is the owned-side mirror.
624    pub fn to_json_str_in<'b>(&self, arena: &'b bumpalo::Bump) -> &'b str {
625        let mut out = bumpalo::collections::Vec::new_in(arena);
626        let _ = write_owned_value(&mut out, self);
627        into_arena_str(out)
628    }
629
630    /// Pretty sibling of [`OwnedDataValue::to_json_str_in`].
631    pub fn to_json_pretty_str_in<'b>(&self, arena: &'b bumpalo::Bump) -> &'b str {
632        let mut out = bumpalo::collections::Vec::new_in(arena);
633        let _ = write_owned_value_pretty(&mut out, self, 0);
634        into_arena_str(out)
635    }
636}
637
638// ---- Display + Pretty wrapper ---------------------------------------------------
639
640/// Wrapper produced by [`DataValue::pretty`] / [`OwnedDataValue::pretty`] that
641/// renders the value as indented JSON via `Display`.
642pub struct Pretty<'b, T: ?Sized>(&'b T);
643
644impl fmt::Display for DataValue<'_> {
645    /// Compact JSON. Same shape as `serde_json::to_string`.
646    ///
647    /// ```
648    /// use bumpalo::Bump;
649    /// use datavalue_rs::DataValue;
650    ///
651    /// let arena = Bump::new();
652    /// let v = DataValue::from_str(r#"{"a":[1,2.5,"hi"]}"#, &arena).unwrap();
653    /// assert_eq!(v.to_string(), r#"{"a":[1,2.5,"hi"]}"#);
654    /// ```
655    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656        let mut sink = FormatterSink::new(f);
657        write_data_value(&mut sink, self)?;
658        sink.flush()
659    }
660}
661
662impl fmt::Display for OwnedDataValue {
663    /// Compact JSON. Same shape as `serde_json::to_string`.
664    ///
665    /// ```
666    /// use datavalue_rs::OwnedDataValue;
667    ///
668    /// let v: OwnedDataValue = r#"{"a":[1,2.5,"hi"]}"#.parse().unwrap();
669    /// assert_eq!(v.to_string(), r#"{"a":[1,2.5,"hi"]}"#);
670    /// ```
671    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
672        let mut sink = FormatterSink::new(f);
673        write_owned_value(&mut sink, self)?;
674        sink.flush()
675    }
676}
677
678impl fmt::Display for Pretty<'_, DataValue<'_>> {
679    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
680        let mut sink = FormatterSink::new(f);
681        write_data_value_pretty(&mut sink, self.0, 0)?;
682        sink.flush()
683    }
684}
685
686impl fmt::Display for Pretty<'_, OwnedDataValue> {
687    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
688        let mut sink = FormatterSink::new(f);
689        write_owned_value_pretty(&mut sink, self.0, 0)?;
690        sink.flush()
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697    use bumpalo::Bump;
698
699    fn round_trip(s: &str) -> String {
700        let arena = Bump::new();
701        let v = DataValue::from_str(s, &arena).unwrap();
702        v.to_string()
703    }
704
705    #[test]
706    fn primitives() {
707        assert_eq!(round_trip("null"), "null");
708        assert_eq!(round_trip("true"), "true");
709        assert_eq!(round_trip("false"), "false");
710        assert_eq!(round_trip("42"), "42");
711        assert_eq!(round_trip("-7"), "-7");
712        assert_eq!(round_trip("3.5"), "3.5");
713    }
714
715    #[test]
716    fn strings_with_escapes() {
717        assert_eq!(round_trip(r#""hello""#), r#""hello""#);
718        assert_eq!(round_trip(r#""a\nb""#), r#""a\nb""#);
719        assert_eq!(round_trip(r#""a\\b""#), r#""a\\b""#);
720        assert_eq!(round_trip(r#""a\"b""#), r#""a\"b""#);
721        // Unicode passes through verbatim (we don't re-escape non-ASCII).
722        assert_eq!(round_trip(r#""café""#), r#""café""#);
723    }
724
725    #[test]
726    fn control_bytes_render_as_unicode_escapes() {
727        let arena = Bump::new();
728        let v = DataValue::from_str("\"\\u0001\"", &arena).unwrap();
729        assert_eq!(v.to_string(), "\"\\u0001\"");
730    }
731
732    #[test]
733    fn nested_round_trip_matches_serde_json() {
734        let input = r#"{"a":[1,2,{"b":"hi\n","c":null,"d":true}],"e":-3.5,"f":[],"g":{}}"#;
735        let arena = Bump::new();
736        let v = DataValue::from_str(input, &arena).unwrap();
737        let ours = v.to_string();
738        let serde: serde_json::Value = serde_json::from_str(input).unwrap();
739        let theirs = serde_json::to_string(&serde).unwrap();
740        assert_eq!(ours, theirs);
741    }
742
743    #[test]
744    fn long_string_swar_path() {
745        let arena = Bump::new();
746        let s = format!("\"{}\"", "x".repeat(200));
747        let v = DataValue::from_str(&s, &arena).unwrap();
748        assert_eq!(v.to_string(), s);
749    }
750
751    // Display goes through FormatterSink's staging buffer; write_json_into
752    // goes straight to the Vec. The two must be byte-identical for every
753    // buffering edge case: chunks that straddle the FMT_STAGING boundary,
754    // chunks larger than the buffer (direct-write path), multi-byte UTF-8
755    // near flush points, and escape-heavy strings (many tiny chunks).
756    fn assert_display_matches_vec(input: &str) {
757        let arena = Bump::new();
758        let v = DataValue::from_str(input, &arena).unwrap();
759        let mut buf = Vec::new();
760        v.write_json_into(&mut buf);
761        assert_eq!(v.to_string().into_bytes(), buf, "compact mismatch");
762
763        let mut pretty_buf = Vec::new();
764        v.write_json_pretty_into(&mut pretty_buf);
765        assert_eq!(
766            v.pretty().to_string().into_bytes(),
767            pretty_buf,
768            "pretty mismatch"
769        );
770    }
771
772    #[cfg(feature = "datetime")]
773    #[test]
774    fn datetime_emit_matches_display_wire_format() {
775        use crate::datetime::DataDateTime;
776
777        let dt = DataDateTime::parse("2024-01-15T12:30:45Z").unwrap();
778        let later = DataDateTime::parse("2024-01-18T16:35:51Z").unwrap();
779        let du = later.diff(&dt);
780
781        for v in [DataValue::DateTime(dt), DataValue::Duration(du)] {
782            let display = v.to_string();
783            let mut buf = Vec::new();
784            v.write_json_into(&mut buf);
785            assert_eq!(display.into_bytes(), buf);
786
787            let owned = v.to_owned();
788            assert_eq!(owned.to_string(), v.to_string());
789        }
790        assert_eq!(
791            DataValue::DateTime(dt).to_string(),
792            "\"2024-01-15T12:30:45Z\""
793        );
794        assert_eq!(DataValue::Duration(du).to_string(), "\"3d:4h:5m:6s\"");
795    }
796
797    #[test]
798    fn to_json_str_in_matches_to_string() {
799        let inputs = [
800            "null",
801            "[]",
802            r#"{"a":[1,2.5,"hi\n",null,true],"b":{"c":"é€"},"e":-0.125}"#,
803        ];
804        let arena = Bump::new();
805        // Emit into a *different* arena than the values live in.
806        let out_arena = Bump::new();
807        for input in inputs {
808            let v = DataValue::from_str(input, &arena).unwrap();
809            assert_eq!(v.to_json_str_in(&out_arena), v.to_string());
810            assert_eq!(v.to_json_pretty_str_in(&out_arena), v.pretty().to_string());
811
812            let owned = v.to_owned();
813            assert_eq!(owned.to_json_str_in(&out_arena), owned.to_string());
814            assert_eq!(
815                owned.to_json_pretty_str_in(&out_arena),
816                owned.pretty().to_string()
817            );
818        }
819        // Long string: forces BumpVec growth inside the emit.
820        let long = format!("\"{}\"", "x".repeat(5000));
821        let v = DataValue::from_str(&long, &arena).unwrap();
822        assert_eq!(v.to_json_str_in(&out_arena), long);
823    }
824
825    #[test]
826    fn display_matches_vec_across_staging_boundaries() {
827        // ASCII strings sized to land runs on every offset around the
828        // 128-byte staging capacity.
829        for n in [1, 7, 126, 127, 128, 129, 200, 255, 256, 257, 1000] {
830            assert_display_matches_vec(&format!("\"{}\"", "x".repeat(n)));
831        }
832        // Multi-byte UTF-8 (2- and 3-byte chars) filling past the boundary —
833        // a split inside a char would corrupt output or trip UTF-8 checks.
834        for n in [60, 63, 64, 65, 100] {
835            assert_display_matches_vec(&format!("\"{}\"", "é".repeat(n)));
836            assert_display_matches_vec(&format!("\"{}\"", "€".repeat(n)));
837        }
838        // Escape-heavy: alternating escapes chop the string into 1-byte runs.
839        assert_display_matches_vec(&format!("\"{}\"", r#"a\n"#.repeat(100)));
840        // Composite document with many small structural writes.
841        assert_display_matches_vec(
842            r#"{"a":[1,2.5,"hi\n",null,true],"b":{"c":"é€","d":[[],{}]},"e":-0.125}"#,
843        );
844    }
845
846    #[test]
847    fn non_finite_floats_render_as_null() {
848        let v = DataValue::from_f64(f64::NAN);
849        assert_eq!(v.to_string(), "null");
850        let v = DataValue::from_f64(f64::INFINITY);
851        assert_eq!(v.to_string(), "null");
852    }
853
854    #[test]
855    fn owned_round_trip() {
856        let v: OwnedDataValue = r#"{"name":"alice","age":30}"#.parse().unwrap();
857        let serde: serde_json::Value = serde_json::from_str(&v.to_string()).unwrap();
858        assert_eq!(serde["name"], "alice");
859        assert_eq!(serde["age"], 30);
860    }
861
862    #[test]
863    fn write_json_into_buffer() {
864        let arena = Bump::new();
865        let v = DataValue::from_str(r#"[1,2,3]"#, &arena).unwrap();
866        let mut buf = Vec::new();
867        v.write_json_into(&mut buf);
868        assert_eq!(buf, b"[1,2,3]");
869    }
870
871    #[test]
872    fn pretty_matches_serde_json_pretty() {
873        let input = r#"{"a":[1,2,{"b":"hi","c":null}],"e":-3.5,"f":[],"g":{}}"#;
874        let arena = Bump::new();
875        let v = DataValue::from_str(input, &arena).unwrap();
876        let ours = v.pretty().to_string();
877        let serde: serde_json::Value = serde_json::from_str(input).unwrap();
878        let theirs = serde_json::to_string_pretty(&serde).unwrap();
879        assert_eq!(ours, theirs);
880    }
881
882    #[test]
883    fn pretty_owned_matches_serde_json_pretty() {
884        let input = r#"{"a":[1,2,{"b":"hi","c":null}],"e":-3.5,"f":[],"g":{}}"#;
885        let v: OwnedDataValue = input.parse().unwrap();
886        let serde: serde_json::Value = serde_json::from_str(input).unwrap();
887        assert_eq!(
888            v.pretty().to_string(),
889            serde_json::to_string_pretty(&serde).unwrap()
890        );
891    }
892
893    #[test]
894    fn pretty_empty_collections_inline() {
895        let arena = Bump::new();
896        let v = DataValue::from_str(r#"{"a":[],"b":{}}"#, &arena).unwrap();
897        assert_eq!(v.pretty().to_string(), "{\n  \"a\": [],\n  \"b\": {}\n}");
898    }
899
900    #[test]
901    fn pretty_deep_indent_beyond_64_spaces() {
902        // 35 levels deep -> 70 spaces of indent on the leaf line. Exercises
903        // the chunked SPACES write loop.
904        let arena = Bump::new();
905        let mut s = String::new();
906        for _ in 0..35 {
907            s.push('[');
908        }
909        s.push('1');
910        for _ in 0..35 {
911            s.push(']');
912        }
913        let v = DataValue::from_str(&s, &arena).unwrap();
914        let ours = v.pretty().to_string();
915        let serde: serde_json::Value = serde_json::from_str(&s).unwrap();
916        assert_eq!(ours, serde_json::to_string_pretty(&serde).unwrap());
917    }
918
919    #[cfg(feature = "tensor")]
920    #[test]
921    fn tensor_compact_wire_format() {
922        use crate::tensor::{DType, DataTensor, OwnedDataTensor};
923        let arena = Bump::new();
924        let t =
925            DataTensor::from_slice_in(&[2, 3], &[1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], &arena).unwrap();
926        let v = DataValue::tensor_in(t, &arena);
927        let expected =
928            r#"{"tensor":{"dtype":"f32","shape":[2,3],"data":"AACAPwAAAEAAAEBAAACAQAAAoEAAAMBA"}}"#;
929        assert_eq!(v.to_string(), expected);
930        assert_eq!(v.to_json_str_in(&arena), expected);
931        let mut buf = Vec::new();
932        v.write_json_into(&mut buf);
933        assert_eq!(buf, expected.as_bytes());
934        assert_eq!(v.to_owned().to_string(), expected);
935
936        // Nested inside a document, and the edge shapes.
937        let doc = arena.alloc_slice_copy(&[("t", v), ("n", DataValue::from_i64(1))]);
938        assert_eq!(
939            DataValue::Object(doc).to_string(),
940            format!(r#"{{"t":{expected},"n":1}}"#)
941        );
942        let scalar = OwnedDataTensor::from_slice([], &[7u8]).unwrap();
943        assert_eq!(
944            OwnedDataValue::tensor(scalar).to_string(),
945            r#"{"tensor":{"dtype":"u8","shape":[],"data":"Bw=="}}"#
946        );
947        let empty = OwnedDataTensor::from_bytes(DType::I16, [0, 3], &[]).unwrap();
948        assert_eq!(
949            OwnedDataValue::tensor(empty).to_string(),
950            r#"{"tensor":{"dtype":"i16","shape":[0,3],"data":""}}"#
951        );
952    }
953
954    #[cfg(feature = "tensor")]
955    #[test]
956    fn tensor_pretty_wire_format() {
957        use crate::tensor::{DType, DataTensor, OwnedDataTensor};
958        let arena = Bump::new();
959        let t =
960            DataTensor::from_slice_in(&[2, 3], &[1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], &arena).unwrap();
961        let v = DataValue::tensor_in(t, &arena);
962        let expected = "{\n  \"tensor\": {\n    \"dtype\": \"f32\",\n    \"shape\": [\n      2,\n      3\n    ],\n    \"data\": \"AACAPwAAAEAAAEBAAACAQAAAoEAAAMBA\"\n  }\n}";
963        assert_eq!(v.pretty().to_string(), expected);
964        assert_eq!(v.to_json_pretty_str_in(&arena), expected);
965        assert_eq!(v.to_owned().pretty().to_string(), expected);
966
967        let doc = arena.alloc_slice_copy(&[("t", v)]);
968        let nested = DataValue::Object(doc).pretty().to_string();
969        assert!(nested.starts_with("{\n  \"t\": {\n    \"tensor\": {\n      \"dtype\": \"f32\","));
970        assert!(nested.ends_with("    }\n  }\n}"));
971
972        let scalar = OwnedDataTensor::from_bytes(DType::U8, [], &[7]).unwrap();
973        assert_eq!(
974            OwnedDataValue::tensor(scalar).pretty().to_string(),
975            "{\n  \"tensor\": {\n    \"dtype\": \"u8\",\n    \"shape\": [],\n    \"data\": \"Bw==\"\n  }\n}"
976        );
977    }
978
979    #[cfg(all(feature = "tensor", feature = "serde_json"))]
980    #[test]
981    fn tensor_pretty_matches_serde_json_pretty() {
982        use crate::tensor::DataTensor;
983        let arena = Bump::new();
984        for shape in [&[2usize, 3][..], &[6], &[], &[0, 6], &[1, 6, 1]] {
985            let elems: Vec<i32> = (0..shape.iter().product::<usize>() as i32).collect();
986            let t = DataTensor::from_slice_in(shape, &elems, &arena).unwrap();
987            let doc = arena.alloc_slice_copy(&[("t", DataValue::tensor_in(t, &arena))]);
988            let v = DataValue::Object(doc);
989            assert_eq!(
990                v.pretty().to_string(),
991                serde_json::to_string_pretty(&v).unwrap()
992            );
993            assert_eq!(v.to_string(), serde_json::to_string(&v).unwrap());
994        }
995    }
996
997    #[cfg(feature = "tensor")]
998    #[test]
999    fn tensor_display_matches_vec_across_chunk_boundaries() {
1000        use crate::tensor::DataTensor;
1001        let arena = Bump::new();
1002        // 5000 bytes: crosses the 3072-byte encoder chunk and the 128-byte
1003        // Display staging buffer many times.
1004        let bytes: Vec<u8> = (0..5000).map(|i| (i % 251) as u8).collect();
1005        let t =
1006            DataTensor::from_bytes_in(crate::tensor::DType::U8, &[5000], &bytes, &arena).unwrap();
1007        let v = DataValue::tensor_in(t, &arena);
1008        let mut buf = Vec::new();
1009        v.write_json_into(&mut buf);
1010        assert_eq!(v.to_string().into_bytes(), buf);
1011        let mut pretty_buf = Vec::new();
1012        v.write_json_pretty_into(&mut pretty_buf);
1013        assert_eq!(v.pretty().to_string().into_bytes(), pretty_buf);
1014    }
1015}