Skip to main content

asmjson/dom/
mod.rs

1pub mod json_ref;
2
3use crate::sax::Sax;
4
5// ---------------------------------------------------------------------------
6// DomEntryKind — top 4 bits of the tag word
7// ---------------------------------------------------------------------------
8
9/// Discriminant stored in bits 63–60 of `DomEntry::tag_payload`.
10///
11/// The numeric values are fixed and part of the public ABI (the hand-written
12/// assembly in `parse_json_zmm_dom.S` depends on them).
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(u8)]
15pub enum DomEntryKind {
16    Null = 0,
17    Bool = 1,
18    Number = 2,
19    String = 3,
20    EscapedString = 4,
21    Key = 5,
22    EscapedKey = 6,
23    StartObject = 7,
24    EndObject = 8,
25    StartArray = 9,
26    EndArray = 10,
27}
28
29// Bit-field constants
30const KIND_SHIFT: u64 = 60;
31const PAYLOAD_MASK: u64 = u64::MAX >> 4; // low 60 bits
32
33// ---------------------------------------------------------------------------
34// DomEntry — exactly 16 bytes
35// ---------------------------------------------------------------------------
36
37/// A single token in a [`Dom`].
38///
39/// The representation is a fixed-size 16-byte struct:
40///
41/// | word | bits | meaning |
42/// |------|------|---------|
43/// | 0 (offset 0) | 63–60 | [`DomEntryKind`] discriminant |
44/// | 0 (offset 0) | 59–0  | string/key length **or** object/array end-index |
45/// | 1 (offset 8) | 63–0  | pointer to string bytes (null for non-string kinds) |
46///
47/// For `EscapedString` / `EscapedKey` the pointer is the data pointer of a
48/// `Box<str>` whose ownership is transferred into (and out of) this entry.
49/// [`DomEntry`] implements [`Drop`] to free that allocation.
50///
51/// For `Bool` the low bit of the payload encodes the value (`0` = false, `1` = true).
52/// For `Null`, `EndObject`, `EndArray` both payload and pointer are zero.
53#[repr(C)]
54pub struct DomEntry<'a> {
55    /// Bits 63–60: kind.  Bits 59–0: length or end-index.
56    pub(crate) tag_payload: u64,
57    /// Pointer to string bytes, or null.
58    pub(crate) ptr: *const u8,
59    _marker: std::marker::PhantomData<&'a str>,
60}
61
62// SAFETY: the only non-Send/Sync component is the raw pointer; we track
63// ownership of the pointed-to data through the 'a lifetime or through the
64// Box<str> path (EscapedString/EscapedKey), so sharing is safe.
65unsafe impl<'a> Send for DomEntry<'a> {}
66unsafe impl<'a> Sync for DomEntry<'a> {}
67
68impl<'a> Drop for DomEntry<'a> {
69    fn drop(&mut self) {
70        let kind = self.kind();
71        if kind == DomEntryKind::EscapedString || kind == DomEntryKind::EscapedKey {
72            if !self.ptr.is_null() {
73                let len = self.payload() as usize;
74                // SAFETY: these were originally created by Box::into_raw(s.into_boxed_str()).
75                unsafe {
76                    let slice = std::slice::from_raw_parts_mut(self.ptr as *mut u8, len);
77                    drop(Box::from_raw(slice as *mut [u8] as *mut str));
78                }
79            }
80        }
81    }
82}
83
84impl<'a> Clone for DomEntry<'a> {
85    fn clone(&self) -> Self {
86        let kind = self.kind();
87        if kind == DomEntryKind::EscapedString || kind == DomEntryKind::EscapedKey {
88            // Deep-copy the heap allocation.
89            let s = self.as_escaped_str_unchecked();
90            let boxed: Box<str> = s.into();
91            let len = boxed.len() as u64;
92            let ptr = Box::into_raw(boxed) as *mut u8 as *const u8;
93            Self {
94                tag_payload: ((kind as u64) << KIND_SHIFT) | (len & PAYLOAD_MASK),
95                ptr,
96                _marker: std::marker::PhantomData,
97            }
98        } else {
99            Self {
100                tag_payload: self.tag_payload,
101                ptr: self.ptr,
102                _marker: std::marker::PhantomData,
103            }
104        }
105    }
106}
107
108/// Custom `Debug` that renders the same variant names as the old enum.
109impl<'a> std::fmt::Debug for DomEntry<'a> {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self.kind() {
112            DomEntryKind::Null => write!(f, "Null"),
113            DomEntryKind::Bool => write!(f, "Bool({})", self.payload() != 0),
114            DomEntryKind::Number => write!(f, "Number({:?})", self.as_str_unchecked()),
115            DomEntryKind::String => write!(f, "String({:?})", self.as_str_unchecked()),
116            DomEntryKind::EscapedString => {
117                write!(f, "EscapedString({:?})", self.as_escaped_str_unchecked())
118            }
119            DomEntryKind::Key => write!(f, "Key({:?})", self.as_str_unchecked()),
120            DomEntryKind::EscapedKey => {
121                write!(f, "EscapedKey({:?})", self.as_escaped_str_unchecked())
122            }
123            DomEntryKind::StartObject => write!(f, "StartObject({})", self.payload()),
124            DomEntryKind::EndObject => write!(f, "EndObject"),
125            DomEntryKind::StartArray => write!(f, "StartArray({})", self.payload()),
126            DomEntryKind::EndArray => write!(f, "EndArray"),
127        }
128    }
129}
130
131/// Equality.  For `EscapedString`/`EscapedKey` we compare the string content.
132impl<'a> PartialEq for DomEntry<'a> {
133    fn eq(&self, other: &Self) -> bool {
134        if self.kind() != other.kind() {
135            return false;
136        }
137        match self.kind() {
138            DomEntryKind::Null | DomEntryKind::EndObject | DomEntryKind::EndArray => true,
139            DomEntryKind::Bool => self.payload() == other.payload(),
140            DomEntryKind::StartObject | DomEntryKind::StartArray => {
141                self.payload() == other.payload()
142            }
143            DomEntryKind::Number | DomEntryKind::String | DomEntryKind::Key => {
144                self.as_str_unchecked() == other.as_str_unchecked()
145            }
146            DomEntryKind::EscapedString | DomEntryKind::EscapedKey => {
147                self.as_escaped_str_unchecked() == other.as_escaped_str_unchecked()
148            }
149        }
150    }
151}
152
153// ---------------------------------------------------------------------------
154// DomEntry constructors and accessors
155// ---------------------------------------------------------------------------
156
157impl<'a> DomEntry<'a> {
158    // ---- private helpers ----
159
160    #[inline]
161    fn make(kind: DomEntryKind, payload: u64, ptr: *const u8) -> Self {
162        Self {
163            tag_payload: ((kind as u64) << KIND_SHIFT) | (payload & PAYLOAD_MASK),
164            ptr,
165            _marker: std::marker::PhantomData,
166        }
167    }
168
169    /// The discriminant.
170    #[inline]
171    pub fn kind(&self) -> DomEntryKind {
172        // SAFETY: we only ever write valid DomEntryKind values into the top 4 bits.
173        unsafe { std::mem::transmute((self.tag_payload >> KIND_SHIFT) as u8) }
174    }
175
176    /// The payload field (low 28 bits of the tag word).
177    #[inline]
178    pub(crate) fn payload(&self) -> u64 {
179        self.tag_payload & PAYLOAD_MASK
180    }
181
182    /// Borrowed str for Number/String/Key variants (UB if called on others).
183    #[inline]
184    fn as_str_unchecked(&self) -> &'a str {
185        let len = self.payload() as usize;
186        unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.ptr, len)) }
187    }
188
189    /// Borrowed str for EscapedString/EscapedKey variants (UB if called on others).
190    #[inline]
191    fn as_escaped_str_unchecked(&self) -> &str {
192        let len = self.payload() as usize;
193        unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.ptr, len)) }
194    }
195
196    // ---- public constructors matching the old enum variants ----
197
198    #[inline]
199    pub fn null_entry() -> Self {
200        Self::make(DomEntryKind::Null, 0, std::ptr::null())
201    }
202    #[inline]
203    pub fn bool_entry(v: bool) -> Self {
204        Self::make(DomEntryKind::Bool, v as u64, std::ptr::null())
205    }
206    #[inline]
207    pub fn number_entry(s: &'a str) -> Self {
208        Self::make(DomEntryKind::Number, s.len() as u64, s.as_ptr())
209    }
210    #[inline]
211    pub fn string_entry(s: &'a str) -> Self {
212        Self::make(DomEntryKind::String, s.len() as u64, s.as_ptr())
213    }
214    #[inline]
215    pub fn escaped_string_entry(s: Box<str>) -> Self {
216        let len = s.len() as u64;
217        let ptr = Box::into_raw(s) as *mut u8 as *const u8;
218        Self::make(DomEntryKind::EscapedString, len, ptr)
219    }
220    #[inline]
221    pub fn key_entry(s: &'a str) -> Self {
222        Self::make(DomEntryKind::Key, s.len() as u64, s.as_ptr())
223    }
224    #[inline]
225    pub fn escaped_key_entry(s: Box<str>) -> Self {
226        let len = s.len() as u64;
227        let ptr = Box::into_raw(s) as *mut u8 as *const u8;
228        Self::make(DomEntryKind::EscapedKey, len, ptr)
229    }
230    /// `payload` will be backfilled with the end-index later.
231    #[inline]
232    pub fn start_object_entry(end_idx: usize) -> Self {
233        Self::make(DomEntryKind::StartObject, end_idx as u64, std::ptr::null())
234    }
235    #[inline]
236    pub fn end_object_entry() -> Self {
237        Self::make(DomEntryKind::EndObject, 0, std::ptr::null())
238    }
239    /// `payload` will be backfilled with the end-index later.
240    #[inline]
241    pub fn start_array_entry(end_idx: usize) -> Self {
242        Self::make(DomEntryKind::StartArray, end_idx as u64, std::ptr::null())
243    }
244    #[inline]
245    pub fn end_array_entry() -> Self {
246        Self::make(DomEntryKind::EndArray, 0, std::ptr::null())
247    }
248
249    // ---- backfill helper (used by DomWriter::end_object / end_array) ----
250
251    /// Overwrite the payload field (low 60 bits) without changing the kind.
252    #[inline]
253    pub(crate) fn set_payload(&mut self, v: usize) {
254        self.tag_payload = (self.tag_payload & !(PAYLOAD_MASK)) | ((v as u64) & PAYLOAD_MASK);
255    }
256
257    // ---- pattern-match helpers matching the old enum syntax ----
258
259    /// Returns `Some(end_index)` if this is `StartObject`, else `None`.
260    #[inline]
261    pub fn as_start_object(&self) -> Option<usize> {
262        if self.kind() == DomEntryKind::StartObject {
263            Some(self.payload() as usize)
264        } else {
265            None
266        }
267    }
268    /// Returns `Some(end_index)` if this is `StartArray`, else `None`.
269    #[inline]
270    pub fn as_start_array(&self) -> Option<usize> {
271        if self.kind() == DomEntryKind::StartArray {
272            Some(self.payload() as usize)
273        } else {
274            None
275        }
276    }
277    /// Returns `Some(b)` if this is `Bool`, else `None`.
278    #[inline]
279    pub fn as_bool(&self) -> Option<bool> {
280        if self.kind() == DomEntryKind::Bool {
281            Some(self.payload() != 0)
282        } else {
283            None
284        }
285    }
286    /// Returns the number text if this is `Number`, else `None`.
287    #[inline]
288    pub fn as_number(&self) -> Option<&'a str> {
289        if self.kind() == DomEntryKind::Number {
290            Some(self.as_str_unchecked())
291        } else {
292            None
293        }
294    }
295    /// Returns the string text if this is `String` or `EscapedString`, else `None`.
296    #[inline]
297    pub fn as_string(&self) -> Option<&str> {
298        match self.kind() {
299            DomEntryKind::String => Some(self.as_str_unchecked()),
300            DomEntryKind::EscapedString => Some(self.as_escaped_str_unchecked()),
301            _ => None,
302        }
303    }
304
305    /// For `String` entries: returns the text with the source-JSON lifetime `'a`,
306    /// enabling zero-copy deserialization.  Returns `None` for `EscapedString`
307    /// (heap-allocated) and for all non-string kinds.
308    #[cfg(feature = "serde")]
309    #[inline]
310    pub(crate) fn source_string(&self) -> Option<&'a str> {
311        if self.kind() == DomEntryKind::String {
312            Some(self.as_str_unchecked())
313        } else {
314            None
315        }
316    }
317
318    /// Returns the key text if this is `Key` or `EscapedKey`, else `None`.
319    #[inline]
320    pub fn as_key(&self) -> Option<&str> {
321        match self.kind() {
322            DomEntryKind::Key => Some(self.as_str_unchecked()),
323            DomEntryKind::EscapedKey => Some(self.as_escaped_str_unchecked()),
324            _ => None,
325        }
326    }
327}
328
329/// Convenience constructors using the old enum-variant names so existing
330/// test/user code keeps a familiar style.
331#[allow(non_snake_case, non_upper_case_globals)]
332impl<'a> DomEntry<'a> {
333    /// Alias: `DomEntry::Null` → `DomEntry::null_entry()`.
334    pub const Null: DomEntry<'static> = DomEntry {
335        tag_payload: 0,
336        ptr: std::ptr::null(),
337        _marker: std::marker::PhantomData,
338    };
339    /// Alias: `DomEntry::EndObject` → `DomEntry::end_object_entry()`.
340    pub const EndObject: DomEntry<'static> = DomEntry {
341        tag_payload: (DomEntryKind::EndObject as u64) << KIND_SHIFT,
342        ptr: std::ptr::null(),
343        _marker: std::marker::PhantomData,
344    };
345    /// Alias: `DomEntry::EndArray` → `DomEntry::end_array_entry()`.
346    pub const EndArray: DomEntry<'static> = DomEntry {
347        tag_payload: (DomEntryKind::EndArray as u64) << KIND_SHIFT,
348        ptr: std::ptr::null(),
349        _marker: std::marker::PhantomData,
350    };
351
352    /// Construct a `Bool` entry.  Replaces `DomEntry::Bool(v)`.
353    #[inline]
354    pub fn Bool(v: bool) -> Self {
355        Self::bool_entry(v)
356    }
357    /// Construct a `Number` entry.  Replaces `DomEntry::Number(s)`.
358    #[inline]
359    pub fn Number(s: &'a str) -> Self {
360        Self::number_entry(s)
361    }
362    /// Construct a `String` entry.  Replaces `DomEntry::String(s)`.
363    #[inline]
364    pub fn String(s: &'a str) -> Self {
365        Self::string_entry(s)
366    }
367    /// Construct an `EscapedString` entry.  Replaces `DomEntry::EscapedString(b)`.
368    #[inline]
369    pub fn EscapedString(s: Box<str>) -> Self {
370        Self::escaped_string_entry(s)
371    }
372    /// Construct a `Key` entry.  Replaces `DomEntry::Key(s)`.
373    #[inline]
374    pub fn Key(s: &'a str) -> Self {
375        Self::key_entry(s)
376    }
377    /// Construct an `EscapedKey` entry.  Replaces `DomEntry::EscapedKey(b)`.
378    #[inline]
379    pub fn EscapedKey(s: Box<str>) -> Self {
380        Self::escaped_key_entry(s)
381    }
382    /// Construct a `StartObject` entry.  Replaces `DomEntry::StartObject(n)`.
383    #[inline]
384    pub fn StartObject(end_idx: usize) -> Self {
385        Self::start_object_entry(end_idx)
386    }
387    /// Construct a `StartArray` entry.  Replaces `DomEntry::StartArray(n)`.
388    #[inline]
389    pub fn StartArray(end_idx: usize) -> Self {
390        Self::start_array_entry(end_idx)
391    }
392}
393
394/// A flat sequence of [`DomEntry`] tokens produced by [`crate::parse_to_dom`].
395///
396/// Each `StartObject(n)` / `StartArray(n)` carries the index of its matching
397/// closer, enabling O(1) structural skips:
398///
399/// ```ignore
400/// if let DomEntry::StartObject(end) = tape.entries[i] {
401///     i = end + 1; // jump past the entire object
402/// }
403/// ```
404#[derive(Debug)]
405pub struct Dom<'a> {
406    pub entries: Vec<DomEntry<'a>>,
407    /// True if any entry in the tape is `EscapedString` or `EscapedKey`
408    /// (i.e. owns a heap-allocated `Box<str>`).  When false, `Drop` can skip
409    /// per-element destructor calls and free the backing allocation directly.
410    pub(crate) has_escapes: bool,
411}
412
413impl<'a> Drop for Dom<'a> {
414    fn drop(&mut self) {
415        if !self.has_escapes {
416            // No entry owns heap memory: skip per-element Drop calls and just
417            // free the Vec's backing allocation.
418            // SAFETY: every DomEntry either borrows from the source JSON
419            // (String/Key/Number) or contains no pointer (Null/Bool/StartObject
420            // etc).  None own a Box<str>, so there is nothing to free per-element.
421            unsafe { self.entries.set_len(0) };
422        }
423        // self.entries drops here; with len==0 no element destructors run.
424    }
425}
426
427// ---------------------------------------------------------------------------
428// DomWriter — builds the flat Dom
429// ---------------------------------------------------------------------------
430
431pub(crate) struct DomWriter<'a> {
432    entries: Vec<DomEntry<'a>>,
433    /// Indices of unmatched `StartObject` / `StartArray` waiting for backfill.
434    open: Vec<usize>,
435    /// Set to `true` when any escaped string or key is pushed.
436    has_escapes: bool,
437}
438
439impl<'a> DomWriter<'a> {
440    pub(crate) fn with_capacity(cap: usize) -> Self {
441        Self {
442            entries: Vec::with_capacity(cap),
443            open: Vec::new(),
444            has_escapes: false,
445        }
446    }
447}
448
449impl<'a> Sax<'a> for DomWriter<'a> {
450    type Output = Dom<'a>;
451
452    fn null(&mut self) {
453        self.entries.push(DomEntry::null_entry());
454    }
455    fn bool_val(&mut self, v: bool) {
456        self.entries.push(DomEntry::bool_entry(v));
457    }
458    fn number(&mut self, s: &'a str) {
459        self.entries.push(DomEntry::number_entry(s));
460    }
461    fn string(&mut self, s: &'a str) {
462        self.entries.push(DomEntry::string_entry(s));
463    }
464    fn escaped_string(&mut self, s: &str) {
465        self.has_escapes = true;
466        let mut buf = String::new();
467        crate::unescape_str(s, &mut buf);
468        self.entries
469            .push(DomEntry::escaped_string_entry(buf.into_boxed_str()));
470    }
471    fn key(&mut self, s: &'a str) {
472        self.entries.push(DomEntry::key_entry(s));
473    }
474    fn escaped_key(&mut self, s: &str) {
475        self.has_escapes = true;
476        let mut buf = String::new();
477        crate::unescape_str(s, &mut buf);
478        self.entries
479            .push(DomEntry::escaped_key_entry(buf.into_boxed_str()));
480    }
481    fn start_object(&mut self) {
482        let idx = self.entries.len();
483        self.open.push(idx);
484        self.entries.push(DomEntry::start_object_entry(0)); // backfilled in end_object
485    }
486    fn end_object(&mut self) {
487        let end_idx = self.entries.len();
488        self.entries.push(DomEntry::end_object_entry());
489        if let Some(start_idx) = self.open.pop() {
490            self.entries[start_idx].set_payload(end_idx);
491        }
492    }
493    fn start_array(&mut self) {
494        let idx = self.entries.len();
495        self.open.push(idx);
496        self.entries.push(DomEntry::start_array_entry(0)); // backfilled in end_array
497    }
498    fn end_array(&mut self) {
499        let end_idx = self.entries.len();
500        self.entries.push(DomEntry::end_array_entry());
501        if let Some(start_idx) = self.open.pop() {
502            self.entries[start_idx].set_payload(end_idx);
503        }
504    }
505    fn finish(self) -> Option<Dom<'a>> {
506        if self.open.is_empty() {
507            Some(Dom {
508                entries: self.entries,
509                has_escapes: self.has_escapes,
510            })
511        } else {
512            None
513        }
514    }
515}
516
517// ---------------------------------------------------------------------------
518// DomRef — lightweight cursor into a Dom
519// ---------------------------------------------------------------------------
520
521/// A lightweight cursor into a [`Dom`], pointing at a single entry by index.
522///
523/// `'t` is the lifetime of the borrow of the tape; `'src` is the lifetime of
524/// the source JSON bytes (`'src: 't`).  Both lifetimes collapse to the same
525/// `'a` in the common case where you borrow the tape and the source in the
526/// same scope.
527///
528/// Created via [`Dom::root`].  Implements [`crate::JsonRef`].
529#[derive(Clone, Copy)]
530pub struct DomRef<'t, 'src: 't> {
531    pub(crate) tape: &'t [DomEntry<'src>],
532    pub(crate) pos: usize,
533}
534
535impl<'src> Dom<'src> {
536    /// Returns a [`DomRef`] cursor at the root (entry 0), or `None` if the
537    /// tape is empty.
538    pub fn root<'t>(&'t self) -> Option<DomRef<'t, 'src>> {
539        if self.entries.is_empty() {
540            None
541        } else {
542            Some(DomRef {
543                tape: &self.entries,
544                pos: 0,
545            })
546        }
547    }
548}
549
550/// Advance past the entry at `pos`, returning the index of the next sibling.
551///
552/// `StartObject(end)` / `StartArray(end)` jump over the entire subtree.
553pub(crate) fn dom_skip(entries: &[DomEntry<'_>], pos: usize) -> usize {
554    let e = &entries[pos];
555    match e.kind() {
556        DomEntryKind::StartObject | DomEntryKind::StartArray => e.payload() as usize + 1,
557        _ => pos + 1,
558    }
559}
560
561// ---------------------------------------------------------------------------
562// DomObjectIter / DomArrayIter
563// ---------------------------------------------------------------------------
564
565/// Iterator over the key-value pairs of a JSON object in a [`Dom`].
566///
567/// Yields `(&str, DomRef)` pairs in document order.  Created by
568/// [`DomRef::object_iter`].
569pub struct DomObjectIter<'t, 'src: 't> {
570    tape: &'t [DomEntry<'src>],
571    pos: usize,
572    end: usize,
573}
574
575impl<'t, 'src: 't> Iterator for DomObjectIter<'t, 'src> {
576    type Item = (&'t str, DomRef<'t, 'src>);
577
578    fn next(&mut self) -> Option<Self::Item> {
579        if self.pos >= self.end {
580            return None;
581        }
582        let key: &'t str = self.tape[self.pos].as_key()?;
583        let val_pos = self.pos + 1;
584        self.pos = dom_skip(self.tape, val_pos);
585        Some((
586            key,
587            DomRef {
588                tape: self.tape,
589                pos: val_pos,
590            },
591        ))
592    }
593}
594
595/// Iterator over the elements of a JSON array in a [`Dom`].
596///
597/// Yields one [`DomRef`] per element in document order.  Created by
598/// [`DomRef::array_iter`].
599pub struct DomArrayIter<'t, 'src: 't> {
600    tape: &'t [DomEntry<'src>],
601    pos: usize,
602    end: usize,
603}
604
605impl<'t, 'src: 't> Iterator for DomArrayIter<'t, 'src> {
606    type Item = DomRef<'t, 'src>;
607
608    fn next(&mut self) -> Option<Self::Item> {
609        if self.pos >= self.end {
610            return None;
611        }
612        let item = DomRef {
613            tape: self.tape,
614            pos: self.pos,
615        };
616        self.pos = dom_skip(self.tape, self.pos);
617        Some(item)
618    }
619}
620
621// ---------------------------------------------------------------------------
622// DomRef inherent methods
623// ---------------------------------------------------------------------------
624
625impl<'t, 'src: 't> DomRef<'t, 'src> {
626    /// Returns an iterator over the key-value pairs if this value is a JSON
627    /// object, or `None` otherwise.
628    ///
629    /// # Example
630    ///
631    /// ```rust
632    /// use asmjson::{parse_to_dom, JsonRef};
633    ///
634    /// let tape = parse_to_dom(r#"{"a":1,"b":2}"#, None).unwrap();
635    /// let root = tape.root().unwrap();
636    /// for (key, val) in root.object_iter().unwrap() {
637    ///     println!("{key}: {}", val.as_number_str().unwrap());
638    /// }
639    /// ```
640    pub fn object_iter(self) -> Option<DomObjectIter<'t, 'src>> {
641        self.tape[self.pos]
642            .as_start_object()
643            .map(|end| DomObjectIter {
644                tape: self.tape,
645                pos: self.pos + 1,
646                end,
647            })
648    }
649
650    /// Returns an iterator over the elements if this value is a JSON array,
651    /// or `None` otherwise.
652    ///
653    /// # Example
654    ///
655    /// ```rust
656    /// use asmjson::{parse_to_dom, JsonRef};
657    ///
658    /// let tape = parse_to_dom(r#"[1,2,3]"#, None).unwrap();
659    /// let root = tape.root().unwrap();
660    /// for elem in root.array_iter().unwrap() {
661    ///     println!("{}", elem.as_number_str().unwrap());
662    /// }
663    /// ```
664    pub fn array_iter(self) -> Option<DomArrayIter<'t, 'src>> {
665        self.tape[self.pos]
666            .as_start_array()
667            .map(|end| DomArrayIter {
668                tape: self.tape,
669                pos: self.pos + 1,
670                end,
671            })
672    }
673}
674
675// ---------------------------------------------------------------------------
676// Unit tests
677// ---------------------------------------------------------------------------
678
679#[cfg(test)]
680mod tests {
681    use crate::{JsonRef, parse_to_dom};
682
683    use super::{Dom, DomEntry};
684
685    fn run_tape(json: &'static str) -> Option<Dom<'static>> {
686        parse_to_dom(json, None)
687    }
688
689    fn te_str(s: &'static str) -> DomEntry<'static> {
690        DomEntry::String(s)
691    }
692    fn te_key(s: &'static str) -> DomEntry<'static> {
693        DomEntry::Key(s)
694    }
695    fn te_num(s: &'static str) -> DomEntry<'static> {
696        DomEntry::Number(s)
697    }
698
699    #[test]
700    fn tape_scalar_values() {
701        assert_eq!(run_tape("null").unwrap().entries, vec![DomEntry::Null]);
702        assert_eq!(
703            run_tape("true").unwrap().entries,
704            vec![DomEntry::Bool(true)]
705        );
706        assert_eq!(
707            run_tape("false").unwrap().entries,
708            vec![DomEntry::Bool(false)]
709        );
710        assert_eq!(run_tape("42").unwrap().entries, vec![te_num("42")]);
711        assert_eq!(run_tape(r#""hi""#).unwrap().entries, vec![te_str("hi")]);
712    }
713
714    #[test]
715    fn tape_empty_object() {
716        let t = run_tape("{}").unwrap();
717        // StartObject(1) EndObject
718        assert_eq!(
719            t.entries,
720            vec![DomEntry::StartObject(1), DomEntry::EndObject]
721        );
722        // StartObject payload points at EndObject
723        assert_eq!(t.entries[0], DomEntry::StartObject(1));
724    }
725
726    #[test]
727    fn tape_empty_array() {
728        let t = run_tape("[]").unwrap();
729        assert_eq!(t.entries, vec![DomEntry::StartArray(1), DomEntry::EndArray]);
730        assert_eq!(t.entries[0], DomEntry::StartArray(1));
731    }
732
733    #[test]
734    fn tape_simple_object() {
735        // {"a":1} → StartObject Key("a") Number("1") EndObject
736        let t = run_tape(r#"{"a":1}"#).unwrap();
737        assert_eq!(
738            t.entries,
739            vec![
740                DomEntry::StartObject(3),
741                te_key("a"),
742                te_num("1"),
743                DomEntry::EndObject,
744            ]
745        );
746        // StartObject carries index of EndObject
747        assert_eq!(t.entries[0], DomEntry::StartObject(3));
748    }
749
750    #[test]
751    fn tape_simple_array() {
752        // [1,2,3] → StartArray Num Num Num EndArray
753        let t = run_tape(r#"[1,2,3]"#).unwrap();
754        assert_eq!(
755            t.entries,
756            vec![
757                DomEntry::StartArray(4),
758                te_num("1"),
759                te_num("2"),
760                te_num("3"),
761                DomEntry::EndArray,
762            ]
763        );
764    }
765
766    #[test]
767    fn tape_nested() {
768        // {"a":[1,2]} → StartObject Key StartArray Num Num EndArray EndObject
769        let t = run_tape(r#"{"a":[1,2]}"#).unwrap();
770        assert_eq!(
771            t.entries,
772            vec![
773                DomEntry::StartObject(6), // 0
774                te_key("a"),              // 1
775                DomEntry::StartArray(5),  // 2
776                te_num("1"),              // 3
777                te_num("2"),              // 4
778                DomEntry::EndArray,       // 5
779                DomEntry::EndObject,      // 6
780            ]
781        );
782        assert_eq!(t.entries[0], DomEntry::StartObject(6));
783        assert_eq!(t.entries[2], DomEntry::StartArray(5));
784    }
785
786    #[test]
787    fn tape_multi_key_object() {
788        let t = run_tape(r#"{"x":1,"y":2}"#).unwrap();
789        assert_eq!(
790            t.entries,
791            vec![
792                DomEntry::StartObject(5), // 0 — points to EndObject at index 5
793                te_key("x"),              // 1
794                te_num("1"),              // 2
795                te_key("y"),              // 3
796                te_num("2"),              // 4
797                DomEntry::EndObject,      // 5
798            ]
799        );
800        assert_eq!(t.entries[0], DomEntry::StartObject(5));
801    }
802
803    #[test]
804    fn tape_invalid_returns_none() {
805        assert!(run_tape("[1,2,]").is_none());
806        assert!(run_tape(r#"{"a":1,}"#).is_none());
807        assert!(run_tape("{bad}").is_none());
808    }
809
810    #[test]
811    fn tape_skip_object() {
812        // Verify the skip-forward idiom works.
813        let t = run_tape(r#"[{"x":1},2]"#).unwrap();
814        // entries: StartArray StartObject Key Num EndObject Num EndArray
815        //          0          1           2   3   4         5   6
816        assert_eq!(t.entries.len(), 7);
817        // Skip from StartObject(4) to index 5 (after EndObject).
818        let end = t.entries[1]
819            .as_start_object()
820            .expect("expected StartObject at index 1");
821        assert_eq!(end, 4);
822        // After the object the next item is at end + 1 = 5.
823        assert_eq!(t.entries[5], te_num("2"));
824    }
825
826    #[test]
827    fn tape_object_iter() {
828        let t = run_tape(r#"{"x":1,"y":true,"z":"hi"}"#).unwrap();
829        let root = t.root().unwrap();
830        let pairs: Vec<_> = root
831            .object_iter()
832            .expect("should be object")
833            .map(|(k, v)| (k.to_string(), (v.as_number_str(), v.as_bool(), v.as_str())))
834            .collect();
835        assert_eq!(pairs.len(), 3);
836        assert_eq!(pairs[0].0, "x");
837        assert_eq!(pairs[0].1, (Some("1"), None, None));
838        assert_eq!(pairs[1].0, "y");
839        assert_eq!(pairs[1].1, (None, Some(true), None));
840        assert_eq!(pairs[2].0, "z");
841        assert_eq!(pairs[2].1, (None, None, Some("hi")));
842        // Non-object returns None.
843        let at = parse_to_dom("[1]", None).unwrap();
844        assert!(at.root().unwrap().object_iter().is_none());
845    }
846
847    #[test]
848    fn tape_array_iter() {
849        let t = run_tape(r#"[1,"two",false,null]"#).unwrap();
850        let root = t.root().unwrap();
851        let items: Vec<_> = root.array_iter().expect("should be array").collect();
852        assert_eq!(items.len(), 4);
853        assert_eq!(items[0].as_number_str(), Some("1"));
854        assert_eq!(items[1].as_str(), Some("two"));
855        assert_eq!(items[2].as_bool(), Some(false));
856        assert!(items[3].is_null());
857        // Nested structures count as single elements.
858        let nt = run_tape(r#"[[1,2],{"a":3}]"#).unwrap();
859        let nelems: Vec<_> = nt.root().unwrap().array_iter().unwrap().collect();
860        assert_eq!(nelems.len(), 2);
861        assert!(nelems[0].is_array());
862        assert!(nelems[1].is_object());
863        // Non-array returns None.
864        let ot = parse_to_dom(r#"{"a":1}"#, None).unwrap();
865        assert!(ot.root().unwrap().array_iter().is_none());
866    }
867}