Skip to main content

datavalue_rs/
value.rs

1//! [`DataValue`] — bump-allocated JSON value type.
2//!
3//! Lifetime `'a` ties the value tree to a [`bumpalo::Bump`]. Composite
4//! variants (`String`, `Array`, `Object`) hold arena-allocated slices, so
5//! constructing a `DataValue` tree costs one or two arena bumps per node
6//! instead of a heap allocation per `Vec` / `BTreeMap` / `String`.
7
8use core::ops::Index;
9
10use bumpalo::Bump;
11
12#[cfg(feature = "datetime")]
13use crate::datetime::{DataDateTime, DataDuration};
14use crate::number::NumberValue;
15#[cfg(feature = "tensor")]
16use crate::tensor::DataTensor;
17
18/// Arena-allocated JSON value tree. Mirrors `serde_json::Value` in shape
19/// and access surface, but every composite payload lives in a `Bump`.
20#[derive(Debug, Clone, Copy)]
21pub enum DataValue<'a> {
22    Null,
23    Bool(bool),
24    Number(NumberValue),
25    String(&'a str),
26    Array(&'a [DataValue<'a>]),
27    Object(&'a [(&'a str, DataValue<'a>)]),
28    /// UTC instant + original tz offset. JSON has no native datetime, so
29    /// the JSON parser never produces this — consumers upgrade from
30    /// `String` at the operator boundary.
31    #[cfg(feature = "datetime")]
32    DateTime(DataDateTime),
33    /// Signed duration. Same boundary rules as `DateTime`.
34    #[cfg(feature = "datetime")]
35    Duration(DataDuration),
36    /// Opaque n-dimensional typed buffer. Held behind a reference so the
37    /// enum stays 24 bytes. The parser never produces this — consumers
38    /// build a [`DataTensor`] at the operator boundary.
39    #[cfg(feature = "tensor")]
40    Tensor(&'a DataTensor<'a>),
41}
42
43/// Returned by `Index` impls when a key/index is missing — matches
44/// `serde_json::Value`'s "indexing returns Null on miss" behaviour.
45pub(crate) static NULL: DataValue<'static> = DataValue::Null;
46
47// Feature-gated variants must not grow the enum: every payload is at most
48// 16 bytes plus tag, or lives behind a reference. Checked on 64-bit targets.
49#[cfg(target_pointer_width = "64")]
50const _: () = assert!(core::mem::size_of::<DataValue<'static>>() == 24);
51
52impl<'a> DataValue<'a> {
53    // ---- Constructors ----
54
55    #[inline]
56    pub fn null() -> Self {
57        DataValue::Null
58    }
59
60    #[inline]
61    pub fn bool(b: bool) -> Self {
62        DataValue::Bool(b)
63    }
64
65    #[inline]
66    pub fn from_i64(i: i64) -> Self {
67        DataValue::Number(NumberValue::from_i64(i))
68    }
69
70    #[inline]
71    pub fn from_f64(f: f64) -> Self {
72        DataValue::Number(NumberValue::from_f64(f))
73    }
74
75    #[inline]
76    pub fn from_str_in(s: &str, arena: &'a Bump) -> Self {
77        DataValue::String(arena.alloc_str(s))
78    }
79
80    /// Wrap a string slice that already lives in the arena (or has the
81    /// required lifetime). No allocation.
82    #[inline]
83    pub fn from_borrowed_str(s: &'a str) -> Self {
84        DataValue::String(s)
85    }
86
87    // ---- Type predicates ----
88
89    #[inline]
90    pub fn is_null(&self) -> bool {
91        matches!(self, DataValue::Null)
92    }
93    #[inline]
94    pub fn is_bool(&self) -> bool {
95        matches!(self, DataValue::Bool(_))
96    }
97    #[inline]
98    pub fn is_number(&self) -> bool {
99        matches!(self, DataValue::Number(_))
100    }
101    #[inline]
102    pub fn is_i64(&self) -> bool {
103        matches!(self, DataValue::Number(NumberValue::Integer(_)))
104    }
105    #[inline]
106    pub fn is_f64(&self) -> bool {
107        matches!(self, DataValue::Number(NumberValue::Float(_)))
108    }
109    #[inline]
110    pub fn is_string(&self) -> bool {
111        matches!(self, DataValue::String(_))
112    }
113    #[inline]
114    pub fn is_array(&self) -> bool {
115        matches!(self, DataValue::Array(_))
116    }
117    #[inline]
118    pub fn is_object(&self) -> bool {
119        matches!(self, DataValue::Object(_))
120    }
121
122    #[cfg(feature = "datetime")]
123    #[inline]
124    pub fn is_datetime(&self) -> bool {
125        matches!(self, DataValue::DateTime(_))
126    }
127    #[cfg(feature = "datetime")]
128    #[inline]
129    pub fn is_duration(&self) -> bool {
130        matches!(self, DataValue::Duration(_))
131    }
132    #[cfg(feature = "tensor")]
133    #[inline]
134    pub fn is_tensor(&self) -> bool {
135        matches!(self, DataValue::Tensor(_))
136    }
137
138    // ---- Accessors ----
139
140    #[inline]
141    pub fn as_bool(&self) -> Option<bool> {
142        match self {
143            DataValue::Bool(b) => Some(*b),
144            _ => None,
145        }
146    }
147
148    #[inline]
149    pub fn as_i64(&self) -> Option<i64> {
150        match self {
151            DataValue::Number(n) => n.as_i64(),
152            _ => None,
153        }
154    }
155
156    #[inline]
157    pub fn as_f64(&self) -> Option<f64> {
158        match self {
159            DataValue::Number(n) => Some(n.as_f64()),
160            _ => None,
161        }
162    }
163
164    #[inline]
165    pub fn as_number(&self) -> Option<&NumberValue> {
166        match self {
167            DataValue::Number(n) => Some(n),
168            _ => None,
169        }
170    }
171
172    #[inline]
173    pub fn as_str(&self) -> Option<&'a str> {
174        match *self {
175            DataValue::String(s) => Some(s),
176            _ => None,
177        }
178    }
179
180    #[inline]
181    pub fn as_array(&self) -> Option<&'a [DataValue<'a>]> {
182        match *self {
183            DataValue::Array(a) => Some(a),
184            _ => None,
185        }
186    }
187
188    #[inline]
189    pub fn as_object(&self) -> Option<&'a [(&'a str, DataValue<'a>)]> {
190        match *self {
191            DataValue::Object(o) => Some(o),
192            _ => None,
193        }
194    }
195
196    #[cfg(feature = "datetime")]
197    #[inline]
198    pub fn as_datetime(&self) -> Option<&DataDateTime> {
199        match self {
200            DataValue::DateTime(d) => Some(d),
201            _ => None,
202        }
203    }
204
205    #[cfg(feature = "datetime")]
206    #[inline]
207    pub fn as_duration(&self) -> Option<&DataDuration> {
208        match self {
209            DataValue::Duration(d) => Some(d),
210            _ => None,
211        }
212    }
213
214    #[cfg(feature = "datetime")]
215    #[inline]
216    pub fn datetime(dt: DataDateTime) -> Self {
217        DataValue::DateTime(dt)
218    }
219
220    #[cfg(feature = "datetime")]
221    #[inline]
222    pub fn duration(d: DataDuration) -> Self {
223        DataValue::Duration(d)
224    }
225
226    #[cfg(feature = "tensor")]
227    #[inline]
228    pub fn as_tensor(&self) -> Option<&'a DataTensor<'a>> {
229        match *self {
230            DataValue::Tensor(t) => Some(t),
231            _ => None,
232        }
233    }
234
235    /// Wrap a tensor header that already lives in the arena (or has the
236    /// required lifetime). No allocation.
237    #[cfg(feature = "tensor")]
238    #[inline]
239    pub fn tensor(t: &'a DataTensor<'a>) -> Self {
240        DataValue::Tensor(t)
241    }
242
243    /// Move a tensor header into `arena` and wrap it. One 40-byte bump.
244    #[cfg(feature = "tensor")]
245    #[inline]
246    pub fn tensor_in(t: DataTensor<'a>, arena: &'a Bump) -> Self {
247        DataValue::Tensor(arena.alloc(t))
248    }
249
250    /// `serde_json::Value::get`-style lookup. Accepts `&str` for object
251    /// keys or `usize` for array indices.
252    #[inline]
253    pub fn get<I: ValueIndex>(&self, index: I) -> Option<&DataValue<'a>> {
254        I::index_into(&index, self)
255    }
256
257    /// Number of elements in an array / object. `None` for non-collections.
258    #[inline]
259    pub fn len(&self) -> Option<usize> {
260        match self {
261            DataValue::Array(a) => Some(a.len()),
262            DataValue::Object(o) => Some(o.len()),
263            _ => None,
264        }
265    }
266
267    #[inline]
268    pub fn is_empty(&self) -> Option<bool> {
269        self.len().map(|n| n == 0)
270    }
271
272    /// Iterate array items. Returns an empty iterator if `self` is not an
273    /// array — same convenience pattern as `json-rust`'s `members`.
274    #[inline]
275    pub fn members(&self) -> core::slice::Iter<'_, DataValue<'a>> {
276        match *self {
277            DataValue::Array(items) => items.iter(),
278            _ => [].iter(),
279        }
280    }
281
282    /// Iterate object entries as `(key, value)` pairs in insertion order.
283    /// Returns an empty iterator if `self` is not an object.
284    #[inline]
285    pub fn entries(&self) -> EntriesIter<'_, 'a> {
286        match *self {
287            DataValue::Object(pairs) => EntriesIter {
288                inner: pairs.iter(),
289            },
290            _ => EntriesIter { inner: [].iter() },
291        }
292    }
293
294    /// Serialise to a compact JSON string. Equivalent to `format!("{self}")` /
295    /// `self.to_string()` — provided as the conventional name people reach
296    /// for, and so callers don't have to import `std::fmt::Write`.
297    #[inline]
298    pub fn to_json_string(&self) -> String {
299        self.to_string()
300    }
301}
302
303/// Iterator over `(key, value)` pairs in a [`DataValue::Object`]. Created
304/// via [`DataValue::entries`].
305pub struct EntriesIter<'v, 'a> {
306    inner: core::slice::Iter<'v, (&'a str, DataValue<'a>)>,
307}
308
309impl<'v, 'a> Iterator for EntriesIter<'v, 'a> {
310    type Item = (&'a str, &'v DataValue<'a>);
311    #[inline]
312    fn next(&mut self) -> Option<Self::Item> {
313        self.inner.next().map(|(k, v)| (*k, v))
314    }
315    #[inline]
316    fn size_hint(&self) -> (usize, Option<usize>) {
317        self.inner.size_hint()
318    }
319}
320
321impl ExactSizeIterator for EntriesIter<'_, '_> {}
322
323impl Default for DataValue<'_> {
324    #[inline]
325    fn default() -> Self {
326        DataValue::Null
327    }
328}
329
330impl<'a> PartialEq for DataValue<'a> {
331    #[inline]
332    fn eq(&self, other: &Self) -> bool {
333        match (self, other) {
334            (DataValue::Null, DataValue::Null) => true,
335            (DataValue::Bool(a), DataValue::Bool(b)) => a == b,
336            (DataValue::Number(a), DataValue::Number(b)) => a == b,
337            (DataValue::String(a), DataValue::String(b)) => a == b,
338            (DataValue::Array(a), DataValue::Array(b)) => a == b,
339            (DataValue::Object(a), DataValue::Object(b)) => {
340                if a.len() != b.len() {
341                    return false;
342                }
343                // Object equality is by key set, not key order — match serde_json.
344                a.iter().all(|(k, v)| {
345                    b.iter()
346                        .find(|(bk, _)| bk == k)
347                        .is_some_and(|(_, bv)| v == bv)
348                })
349            }
350            #[cfg(feature = "datetime")]
351            (DataValue::DateTime(a), DataValue::DateTime(b)) => a == b,
352            #[cfg(feature = "datetime")]
353            (DataValue::Duration(a), DataValue::Duration(b)) => a == b,
354            #[cfg(feature = "tensor")]
355            (DataValue::Tensor(a), DataValue::Tensor(b)) => a == b,
356            _ => false,
357        }
358    }
359}
360
361// ---- Index trait dispatch ----
362
363/// Sealed-style helper for `DataValue::get`. Implemented for `&str`,
364/// `String`, and `usize`.
365pub trait ValueIndex: private::Sealed {
366    fn index_into<'v, 'a>(&self, value: &'v DataValue<'a>) -> Option<&'v DataValue<'a>>;
367    fn index_into_or_null<'v, 'a>(&self, value: &'v DataValue<'a>) -> &'v DataValue<'a>;
368}
369
370mod private {
371    pub trait Sealed {}
372    impl Sealed for str {}
373    impl Sealed for String {}
374    impl Sealed for usize {}
375    impl<T: Sealed + ?Sized> Sealed for &T {}
376}
377
378impl ValueIndex for str {
379    #[inline]
380    fn index_into<'v, 'a>(&self, value: &'v DataValue<'a>) -> Option<&'v DataValue<'a>> {
381        match value {
382            DataValue::Object(pairs) => pairs.iter().find(|(k, _)| *k == self).map(|(_, v)| v),
383            _ => None,
384        }
385    }
386    #[inline]
387    fn index_into_or_null<'v, 'a>(&self, value: &'v DataValue<'a>) -> &'v DataValue<'a> {
388        // &NULL is &'static DataValue<'static>; covariance in 'a coerces it
389        // to &'v DataValue<'a> since 'static: 'a.
390        self.index_into(value).unwrap_or(&NULL)
391    }
392}
393
394impl ValueIndex for String {
395    #[inline]
396    fn index_into<'v, 'a>(&self, value: &'v DataValue<'a>) -> Option<&'v DataValue<'a>> {
397        self.as_str().index_into(value)
398    }
399    #[inline]
400    fn index_into_or_null<'v, 'a>(&self, value: &'v DataValue<'a>) -> &'v DataValue<'a> {
401        self.as_str().index_into_or_null(value)
402    }
403}
404
405impl ValueIndex for usize {
406    #[inline]
407    fn index_into<'v, 'a>(&self, value: &'v DataValue<'a>) -> Option<&'v DataValue<'a>> {
408        match value {
409            DataValue::Array(items) => items.get(*self),
410            _ => None,
411        }
412    }
413    #[inline]
414    fn index_into_or_null<'v, 'a>(&self, value: &'v DataValue<'a>) -> &'v DataValue<'a> {
415        self.index_into(value).unwrap_or(&NULL)
416    }
417}
418
419impl<T: ValueIndex + ?Sized> ValueIndex for &T {
420    #[inline]
421    fn index_into<'v, 'a>(&self, value: &'v DataValue<'a>) -> Option<&'v DataValue<'a>> {
422        (**self).index_into(value)
423    }
424    #[inline]
425    fn index_into_or_null<'v, 'a>(&self, value: &'v DataValue<'a>) -> &'v DataValue<'a> {
426        (**self).index_into_or_null(value)
427    }
428}
429
430impl<'a, I: ValueIndex> Index<I> for DataValue<'a> {
431    type Output = DataValue<'a>;
432    #[inline]
433    fn index(&self, index: I) -> &DataValue<'a> {
434        index.index_into_or_null(self)
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    fn sample<'a>(arena: &'a Bump) -> DataValue<'a> {
443        let nested = arena.alloc_slice_copy(&[
444            DataValue::from_i64(1),
445            DataValue::from_i64(2),
446            DataValue::from_i64(3),
447        ]);
448        let inner_obj = arena.alloc_slice_copy(&[("k", DataValue::Bool(true))]);
449        let pairs = arena.alloc_slice_copy(&[
450            ("name", DataValue::from_borrowed_str("alice")),
451            ("nums", DataValue::Array(nested)),
452            ("inner", DataValue::Object(inner_obj)),
453        ]);
454        DataValue::Object(pairs)
455    }
456
457    #[test]
458    fn get_object_key() {
459        let arena = Bump::new();
460        let v = sample(&arena);
461        assert_eq!(v.get("name").and_then(|x| x.as_str()), Some("alice"));
462        assert!(v.get("missing").is_none());
463    }
464
465    #[test]
466    fn get_array_index() {
467        let arena = Bump::new();
468        let v = sample(&arena);
469        let nums = v.get("nums").unwrap();
470        assert_eq!(nums.get(0).and_then(|x| x.as_i64()), Some(1));
471        assert_eq!(nums.get(2).and_then(|x| x.as_i64()), Some(3));
472        assert!(nums.get(99).is_none());
473    }
474
475    #[test]
476    fn index_returns_null_for_missing() {
477        let arena = Bump::new();
478        let v = sample(&arena);
479        assert!(v["missing"].is_null());
480        assert!(v["nums"][99].is_null());
481    }
482
483    #[test]
484    fn chained_index() {
485        let arena = Bump::new();
486        let v = sample(&arena);
487        assert_eq!(v["inner"]["k"].as_bool(), Some(true));
488    }
489
490    #[test]
491    fn predicates_and_len() {
492        let arena = Bump::new();
493        let v = sample(&arena);
494        assert!(v.is_object());
495        assert_eq!(v.len(), Some(3));
496        assert_eq!(v["nums"].len(), Some(3));
497        assert_eq!(v["name"].len(), None);
498    }
499
500    #[cfg(feature = "datetime")]
501    #[test]
502    fn datetime_variant_round_trips_through_value() {
503        use crate::datetime::{DataDateTime, DataDuration};
504        let dt = DataDateTime::parse("2024-01-15T12:30:45Z").unwrap();
505        let v = DataValue::datetime(dt);
506        assert!(v.is_datetime());
507        assert_eq!(
508            v.as_datetime().map(|d| d.to_iso_string()).as_deref(),
509            Some("2024-01-15T12:30:45Z")
510        );
511
512        let dur = DataDuration::parse("1d:2h").unwrap();
513        let v2 = DataValue::duration(dur);
514        assert!(v2.is_duration());
515        assert_eq!(v2.as_duration().unwrap().to_string(), "1d:2h:0m:0s");
516
517        // Equality on the parent enum dispatches to the variant.
518        assert_eq!(v, DataValue::datetime(dt));
519        assert_ne!(v, v2);
520    }
521
522    #[test]
523    fn equality_object_order_insensitive() {
524        let arena = Bump::new();
525        let a =
526            arena.alloc_slice_copy(&[("x", DataValue::from_i64(1)), ("y", DataValue::from_i64(2))]);
527        let b =
528            arena.alloc_slice_copy(&[("y", DataValue::from_i64(2)), ("x", DataValue::from_i64(1))]);
529        assert_eq!(DataValue::Object(a), DataValue::Object(b));
530    }
531
532    #[cfg(feature = "tensor")]
533    #[test]
534    fn tensor_variant_is_opaque_and_copy() {
535        use crate::tensor::DataTensor;
536        let arena = Bump::new();
537        let t = DataTensor::from_slice_in(&[2, 2], &[1.0f32, 2.0, 3.0, 4.0], &arena).unwrap();
538        let v = DataValue::tensor_in(t, &arena);
539        let copy = v; // Copy
540        assert!(v.is_tensor());
541        assert_eq!(v.as_tensor().unwrap().shape(), &[2, 2]);
542        assert!(v.as_array().is_none());
543        assert!(v.get(0).is_none());
544        assert!(v.get("x").is_none());
545        assert!(v[0].is_null());
546        assert_eq!(v.len(), None);
547        assert_eq!(v.members().count(), 0);
548        assert_eq!(v.entries().count(), 0);
549        assert_eq!(v, copy);
550        let other = DataValue::tensor_in(
551            DataTensor::from_slice_in(&[4], &[1.0f32, 2.0, 3.0, 4.0], &arena).unwrap(),
552            &arena,
553        );
554        assert_ne!(v, other, "shape participates in equality");
555        assert_ne!(v, DataValue::Null);
556    }
557}