Skip to main content

ite_cli/
json_tree.rs

1//! Transforms JSON input into source-neutral tree data.
2//!
3//! The complete JSON boundary: the input document is read to the end and the
4//! tree retains the raw bytes. Structure is discovered *shallowly*: scanning
5//! a container records only its immediate children's byte spans (each child's
6//! subtree is structurally skipped, which validates it and yields its child
7//! count for free); deeper levels materialize on demand through
8//! [`materialize`], driven by expansion or the app's background sweep. JSONL
9//! defers even record validation — the newline scan is the only startup cost —
10//! so corrupt records surface later as error nodes rather than load failures.
11//! Labels, previews, pointers, and outputs are derived from spans through the
12//! crate-private helpers at the bottom. No JSON values escape this module.
13
14use std::fmt;
15use std::io::{self, Read, Write};
16use std::ops::Range;
17use std::sync::Arc;
18
19use serde_json::Value;
20
21use crate::tree::{JsonKey, NodeId, Tree};
22
23/// Enough text to fill an unusually wide terminal without retaining an
24/// unbounded second representation of every object.
25const MAX_OBJECT_PREVIEW_BYTES: usize = 512;
26/// Bounds the search for previewable scalars when early members are containers.
27const MAX_OBJECT_PREVIEW_MEMBERS: usize = 32;
28/// Matches serde_json's default recursion limit, which the scanner replaced.
29const MAX_DEPTH: usize = 128;
30
31#[derive(Debug)]
32pub struct Error(String);
33
34impl fmt::Display for Error {
35    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(formatter, "invalid JSON input: {}", self.0)
37    }
38}
39
40impl std::error::Error for Error {}
41
42/// Read the input, detect JSON vs JSONL from the content (one uniform rule,
43/// however the input arrived), and build the tree.
44pub fn from_reader(mut reader: impl Read) -> Result<Tree, Error> {
45    let _span = crate::profile::span("json_tree::load");
46    let bytes = slurp(reader.by_ref())?;
47    if detect_jsonl(&bytes) {
48        jsonl_from_bytes(bytes)
49    } else {
50        json_from_bytes(bytes)
51    }
52}
53
54/// Read the input as JSONL regardless of what detection would say — the
55/// `--jsonl` escape hatch for content that is also valid JSON (e.g. a single
56/// record).
57pub fn jsonl_from_reader(mut reader: impl Read) -> Result<Tree, Error> {
58    let _span = crate::profile::span("json_tree::load");
59    let bytes = slurp(reader.by_ref())?;
60    jsonl_from_bytes(bytes)
61}
62
63fn slurp(reader: &mut impl Read) -> Result<Vec<u8>, Error> {
64    let mut bytes = Vec::new();
65    reader
66        .read_to_end(&mut bytes)
67        .map_err(|error| Error(error.to_string()))?;
68    if u32::try_from(bytes.len()).is_err() {
69        return Err(Error(
70            "input exceeds the 4 GiB the span index addresses".to_owned(),
71        ));
72    }
73    if let Err(error) = std::str::from_utf8(&bytes) {
74        return Err(Error(error.to_string()));
75    }
76    Ok(bytes)
77}
78
79/// Content detection: JSONL iff non-whitespace content follows the first
80/// newline AND the first line is exactly one complete JSON value. Pretty
81/// JSON's first line is incomplete; a minified document has nothing after its
82/// only newline; the genuinely ambiguous single-record case reads as JSON,
83/// which `--jsonl` overrides.
84fn detect_jsonl(bytes: &[u8]) -> bool {
85    let Some(start) = bytes
86        .iter()
87        .position(|b| !matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
88    else {
89        return false;
90    };
91    let content = &bytes[start..];
92    let Some(newline) = content.iter().position(|&b| b == b'\n') else {
93        return false;
94    };
95    let (first_line, rest) = content.split_at(newline);
96    if rest
97        .iter()
98        .all(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
99    {
100        return false;
101    }
102    is_complete_value(first_line)
103}
104
105/// Whether `bytes` holds exactly one complete JSON value (plus whitespace).
106fn is_complete_value(bytes: &[u8]) -> bool {
107    let mut scanner = Scanner::new(bytes);
108    if scanner.skip_value().is_err() {
109        return false;
110    }
111    scanner.skip_ws();
112    scanner.pos == bytes.len()
113}
114
115fn json_from_bytes(bytes: Vec<u8>) -> Result<Tree, Error> {
116    let mut tree = Tree::new();
117    let mut scanner = Scanner::new(&bytes);
118    scanner.skip_ws();
119    let mut single_root = None;
120    // A non-empty top-level object spreads its members as forest roots (how
121    // the tree has always presented documents); every other document gets a
122    // single `$` root.
123    let spread = scanner.peek() == Some(b'{') && {
124        let probe = scanner.pos;
125        scanner.pos += 1;
126        scanner.skip_ws();
127        let has_members = scanner.peek() != Some(b'}');
128        scanner.pos = probe;
129        has_members
130    };
131    if spread {
132        scanner.scan_members_shallow(&mut tree, None)?;
133    } else {
134        single_root = Some(scanner.scan_child(&mut tree, None, JsonKey::Root)?);
135    }
136    scanner.skip_ws();
137    if scanner.pos != bytes.len() {
138        return Err(scanner.syntax_error("trailing characters"));
139    }
140    tree.set_json_source(Arc::new(bytes));
141    // A single-rooted document opens with its first level showing, so build
142    // that level now; the skip above already validated the whole document.
143    if let Some(root) = single_root {
144        tree.ensure_children(root);
145    }
146    Ok(tree)
147}
148
149/// Build the JSONL tree: a virtual array root over one record per non-blank
150/// line, discovered by the newline scan alone — records are not parsed here.
151/// Indices are record ordinals, not line numbers (error messages carry line
152/// numbers). The classic corruption is a truncated final record, so only the
153/// tail is validated eagerly and dropped when it does not scan; any other
154/// corrupt record surfaces later as an error node with a banner message.
155fn jsonl_from_bytes(bytes: Vec<u8>) -> Result<Tree, Error> {
156    let mut lines = non_blank_lines(&bytes);
157    if let Some(last) = lines.last()
158        && !is_complete_value(&bytes[last.clone()])
159    {
160        lines.pop();
161    }
162    let mut tree = Tree::new();
163    let root = tree.push_json(
164        None,
165        0..bytes.len() as u32,
166        JsonKey::JsonlRoot,
167        true,
168        Some(lines.len() as u32),
169        true,
170    );
171    for (ordinal, line) in lines.iter().enumerate() {
172        let span = trim_ws(&bytes, line.clone());
173        let is_container = matches!(bytes[span.start], b'{' | b'[');
174        tree.push_json(
175            Some(root),
176            span.start as u32..span.end as u32,
177            JsonKey::Index(ordinal as u32),
178            is_container,
179            None,
180            false,
181        );
182    }
183    tree.set_json_source(Arc::new(bytes));
184    Ok(tree)
185}
186
187/// Load the immediate children of JSON node `id` — or, for a pending scalar
188/// record, validate it — converting the node to an error leaf when its span
189/// does not scan. The single work unit behind on-demand expansion and the
190/// arena sweep.
191pub(crate) fn materialize(tree: &mut Tree, id: NodeId) {
192    let (Some(bytes), Some(span)) = (tree.json_source_arc(), tree.json_span(id)) else {
193        tree.mark_children_loaded(id);
194        return;
195    };
196    let end = span.end as usize;
197    let mark = tree.len();
198    let mut scanner = Scanner::new(&bytes[..end]);
199    scanner.pos = span.start as usize;
200    match scan_span(&mut scanner, tree, id, end) {
201        Ok(()) => tree.mark_children_loaded(id),
202        Err(error) => {
203            tree.truncate(mark);
204            tree.set_json_error(id, error.to_string());
205        }
206    }
207}
208
209fn scan_span(scanner: &mut Scanner, tree: &mut Tree, id: NodeId, end: usize) -> Result<(), Error> {
210    scanner.skip_ws();
211    match scanner.peek() {
212        Some(b'{') => scanner.scan_members_shallow(tree, Some(id))?,
213        Some(b'[') => scanner.scan_elements_shallow(tree, id)?,
214        _ => {
215            scanner.scan_scalar()?;
216        }
217    }
218    scanner.skip_ws();
219    if scanner.pos != end {
220        return Err(scanner.syntax_error("trailing characters"));
221    }
222    Ok(())
223}
224
225/// Byte ranges of the lines holding non-whitespace content, in order.
226fn non_blank_lines(bytes: &[u8]) -> Vec<Range<usize>> {
227    let mut lines = Vec::new();
228    let mut start = 0;
229    for (position, &byte) in bytes.iter().enumerate() {
230        if byte == b'\n' {
231            push_if_non_blank(bytes, start..position, &mut lines);
232            start = position + 1;
233        }
234    }
235    push_if_non_blank(bytes, start..bytes.len(), &mut lines);
236    lines
237}
238
239fn push_if_non_blank(bytes: &[u8], range: Range<usize>, lines: &mut Vec<Range<usize>>) {
240    if bytes[range.clone()]
241        .iter()
242        .any(|b| !matches!(b, b' ' | b'\t' | b'\r'))
243    {
244        lines.push(range);
245    }
246}
247
248fn trim_ws(bytes: &[u8], mut range: Range<usize>) -> Range<usize> {
249    while range.start < range.end && matches!(bytes[range.start], b' ' | b'\t' | b'\r') {
250        range.start += 1;
251    }
252    while range.end > range.start && matches!(bytes[range.end - 1], b' ' | b'\t' | b'\r') {
253        range.end -= 1;
254    }
255    range
256}
257
258/// A structural scanner: validates the document grammar while recording byte
259/// spans, without building values. Scalar contents are re-parsed lazily at
260/// display time, so validation here is strict about shape (strings, escapes,
261/// number grammar, literals) and leaves range checks to derivation.
262struct Scanner<'a> {
263    bytes: &'a [u8],
264    pos: usize,
265    depth: usize,
266}
267
268impl<'a> Scanner<'a> {
269    fn new(bytes: &'a [u8]) -> Self {
270        Self {
271            bytes,
272            pos: 0,
273            depth: 0,
274        }
275    }
276
277    fn peek(&self) -> Option<u8> {
278        self.bytes.get(self.pos).copied()
279    }
280
281    fn skip_ws(&mut self) {
282        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
283            self.pos += 1;
284        }
285    }
286
287    fn expect(&mut self, byte: u8) -> Result<(), Error> {
288        if self.peek() == Some(byte) {
289            self.pos += 1;
290            Ok(())
291        } else {
292            Err(self.syntax_error(&format!("expected `{}`", byte as char)))
293        }
294    }
295
296    fn syntax_error(&self, message: &str) -> Error {
297        let consumed = &self.bytes[..self.pos.min(self.bytes.len())];
298        let line = 1 + consumed.iter().filter(|&&b| b == b'\n').count();
299        let line_start = consumed
300            .iter()
301            .rposition(|&b| b == b'\n')
302            .map_or(0, |i| i + 1);
303        let column = self.pos - line_start + 1;
304        Error(format!("{message} at line {line} column {column}"))
305    }
306
307    fn descend(&mut self) -> Result<(), Error> {
308        self.depth += 1;
309        if self.depth > MAX_DEPTH {
310            return Err(self.syntax_error("recursion limit exceeded"));
311        }
312        Ok(())
313    }
314
315    /// Scan one child value shallowly: skip (and validate) its whole span,
316    /// then record a single node carrying the span and the discovered
317    /// immediate-child count. Container children start unloaded unless empty.
318    fn scan_child(
319        &mut self,
320        tree: &mut Tree,
321        parent: Option<NodeId>,
322        key: JsonKey,
323    ) -> Result<NodeId, Error> {
324        self.skip_ws();
325        let start = self.pos as u32;
326        let child_count = self.skip_value()?;
327        let span = start..self.pos as u32;
328        Ok(match child_count {
329            Some(count) => tree.push_json(parent, span, key, true, Some(count), count == 0),
330            None => tree.push_json(parent, span, key, false, None, true),
331        })
332    }
333
334    /// Scan `{ "key": value, ... }`, pushing each member shallowly under
335    /// `parent` (`None` spreads the members as forest roots).
336    fn scan_members_shallow(
337        &mut self,
338        tree: &mut Tree,
339        parent: Option<NodeId>,
340    ) -> Result<(), Error> {
341        self.expect(b'{')?;
342        self.skip_ws();
343        if self.peek() == Some(b'}') {
344            self.pos += 1;
345            return Ok(());
346        }
347        loop {
348            self.skip_ws();
349            let key_start = self.pos as u32;
350            self.scan_string()?;
351            let key_span = key_start..self.pos as u32;
352            self.skip_ws();
353            self.expect(b':')?;
354            self.scan_child(tree, parent, JsonKey::Member { key_span })?;
355            self.skip_ws();
356            match self.peek() {
357                Some(b',') => self.pos += 1,
358                Some(b'}') => {
359                    self.pos += 1;
360                    return Ok(());
361                }
362                _ => return Err(self.syntax_error("expected `,` or `}`")),
363            }
364        }
365    }
366
367    fn scan_elements_shallow(&mut self, tree: &mut Tree, parent: NodeId) -> Result<(), Error> {
368        self.expect(b'[')?;
369        self.skip_ws();
370        if self.peek() == Some(b']') {
371            self.pos += 1;
372            return Ok(());
373        }
374        let mut index = 0;
375        loop {
376            self.scan_child(tree, Some(parent), JsonKey::Index(index))?;
377            index += 1;
378            self.skip_ws();
379            match self.peek() {
380                Some(b',') => self.pos += 1,
381                Some(b']') => {
382                    self.pos += 1;
383                    return Ok(());
384                }
385                _ => return Err(self.syntax_error("expected `,` or `]`")),
386            }
387        }
388    }
389
390    /// Skip one complete value without recording nodes, returning the
391    /// immediate-child count for containers (`None` for scalars).
392    fn skip_value(&mut self) -> Result<Option<u32>, Error> {
393        self.skip_ws();
394        match self.peek() {
395            Some(b'{') => {
396                self.descend()?;
397                let count = self.skip_object()?;
398                self.depth -= 1;
399                Ok(Some(count))
400            }
401            Some(b'[') => {
402                self.descend()?;
403                let count = self.skip_array()?;
404                self.depth -= 1;
405                Ok(Some(count))
406            }
407            _ => {
408                self.scan_scalar()?;
409                Ok(None)
410            }
411        }
412    }
413
414    fn skip_object(&mut self) -> Result<u32, Error> {
415        self.expect(b'{')?;
416        self.skip_ws();
417        if self.peek() == Some(b'}') {
418            self.pos += 1;
419            return Ok(0);
420        }
421        let mut count = 0;
422        loop {
423            self.skip_ws();
424            self.scan_string()?;
425            self.skip_ws();
426            self.expect(b':')?;
427            self.skip_value()?;
428            count += 1;
429            self.skip_ws();
430            match self.peek() {
431                Some(b',') => self.pos += 1,
432                Some(b'}') => {
433                    self.pos += 1;
434                    return Ok(count);
435                }
436                _ => return Err(self.syntax_error("expected `,` or `}`")),
437            }
438        }
439    }
440
441    fn skip_array(&mut self) -> Result<u32, Error> {
442        self.expect(b'[')?;
443        self.skip_ws();
444        if self.peek() == Some(b']') {
445            self.pos += 1;
446            return Ok(0);
447        }
448        let mut count = 0;
449        loop {
450            self.skip_value()?;
451            count += 1;
452            self.skip_ws();
453            match self.peek() {
454                Some(b',') => self.pos += 1,
455                Some(b']') => {
456                    self.pos += 1;
457                    return Ok(count);
458                }
459                _ => return Err(self.syntax_error("expected `,` or `]`")),
460            }
461        }
462    }
463
464    fn scan_scalar(&mut self) -> Result<(), Error> {
465        match self.peek() {
466            Some(b'"') => self.scan_string(),
467            Some(b't') => self.scan_literal("true"),
468            Some(b'f') => self.scan_literal("false"),
469            Some(b'n') => self.scan_literal("null"),
470            Some(b'-' | b'0'..=b'9') => self.scan_number(),
471            Some(other) => {
472                Err(self.syntax_error(&format!("unexpected character `{}`", other as char)))
473            }
474            None => Err(self.syntax_error("unexpected end of input")),
475        }
476    }
477
478    fn scan_literal(&mut self, literal: &str) -> Result<(), Error> {
479        if self.bytes[self.pos..].starts_with(literal.as_bytes()) {
480            self.pos += literal.len();
481            Ok(())
482        } else {
483            Err(self.syntax_error(&format!("expected `{literal}`")))
484        }
485    }
486
487    fn scan_string(&mut self) -> Result<(), Error> {
488        self.expect(b'"')?;
489        loop {
490            match self.peek() {
491                None => return Err(self.syntax_error("unterminated string")),
492                Some(b'"') => {
493                    self.pos += 1;
494                    return Ok(());
495                }
496                Some(b'\\') => {
497                    self.pos += 1;
498                    match self.peek() {
499                        Some(b'"' | b'\\' | b'/' | b'b' | b'f' | b'n' | b'r' | b't') => {
500                            self.pos += 1;
501                        }
502                        Some(b'u') => {
503                            self.pos += 1;
504                            for _ in 0..4 {
505                                if !self.peek().is_some_and(|b| b.is_ascii_hexdigit()) {
506                                    return Err(self.syntax_error("invalid \\u escape"));
507                                }
508                                self.pos += 1;
509                            }
510                        }
511                        _ => return Err(self.syntax_error("invalid escape")),
512                    }
513                }
514                Some(byte) if byte < 0x20 => {
515                    return Err(self.syntax_error("control character in string"));
516                }
517                Some(_) => self.pos += 1,
518            }
519        }
520    }
521
522    fn scan_number(&mut self) -> Result<(), Error> {
523        if self.peek() == Some(b'-') {
524            self.pos += 1;
525        }
526        match self.peek() {
527            Some(b'0') => self.pos += 1,
528            Some(b'1'..=b'9') => {
529                while self.peek().is_some_and(|b| b.is_ascii_digit()) {
530                    self.pos += 1;
531                }
532            }
533            _ => return Err(self.syntax_error("invalid number")),
534        }
535        if self.peek() == Some(b'.') {
536            self.pos += 1;
537            if !self.peek().is_some_and(|b| b.is_ascii_digit()) {
538                return Err(self.syntax_error("invalid number"));
539            }
540            while self.peek().is_some_and(|b| b.is_ascii_digit()) {
541                self.pos += 1;
542            }
543        }
544        if matches!(self.peek(), Some(b'e' | b'E')) {
545            self.pos += 1;
546            if matches!(self.peek(), Some(b'+' | b'-')) {
547                self.pos += 1;
548            }
549            if !self.peek().is_some_and(|b| b.is_ascii_digit()) {
550                return Err(self.syntax_error("invalid number"));
551            }
552            while self.peek().is_some_and(|b| b.is_ascii_digit()) {
553                self.pos += 1;
554            }
555        }
556        Ok(())
557    }
558}
559
560// --- Span-derivation helpers used by `Tree`'s accessors ---
561
562fn slice<'a>(bytes: &'a [u8], span: &Range<u32>) -> &'a [u8] {
563    &bytes[span.start as usize..span.end as usize]
564}
565
566/// The unescaped text of a raw key token (quotes included in the span).
567pub(crate) fn key_text(bytes: &[u8], span: &Range<u32>) -> String {
568    let raw = slice(bytes, span);
569    serde_json::from_slice(raw).unwrap_or_else(|_| String::from_utf8_lossy(raw).into_owned())
570}
571
572/// The compact serialization of the value at `span` — scalars for labels,
573/// whole subtrees for the alternate output. Falls back to the raw text when
574/// the span holds something serde rejects (e.g. an out-of-range number).
575pub(crate) fn value_text(bytes: &[u8], span: &Range<u32>) -> String {
576    let raw = slice(bytes, span);
577    match serde_json::from_slice::<Value>(raw) {
578        Ok(value) => {
579            serde_json::to_string(&value).expect("serializing a parsed JSON value cannot fail")
580        }
581        Err(_) => String::from_utf8_lossy(raw).into_owned(),
582    }
583}
584
585/// The verbatim (lossy) text of a raw span — error nodes' outputs.
586pub(crate) fn raw_text(bytes: &[u8], span: &Range<u32>) -> String {
587    String::from_utf8_lossy(slice(bytes, span)).into_owned()
588}
589
590pub(crate) fn append_pointer(parent: &str, token: &str) -> String {
591    let token = token.replace('~', "~0").replace('/', "~1");
592    format!("{parent}/{token}")
593}
594
595/// Preview of an object's leading scalar members: `Some((key span, value
596/// span))` per scalar member, `None` per container member — containers occupy
597/// one of the inspected slots but contribute no text.
598pub(crate) fn object_preview(
599    bytes: &[u8],
600    members: impl Iterator<Item = Option<(Range<u32>, Range<u32>)>>,
601) -> Option<String> {
602    let mut preview = Preview::new(MAX_OBJECT_PREVIEW_BYTES);
603    for member in members.take(MAX_OBJECT_PREVIEW_MEMBERS) {
604        let Some((key_span, value_span)) = member else {
605            continue;
606        };
607        if !preview.is_empty() && !preview.push_str(" · ") {
608            break;
609        }
610        if !preview.push_str(&key_text(bytes, &key_span)) || !preview.push_str(": ") {
611            break;
612        }
613        preview.push_value(bytes, &value_span);
614        if preview.is_exhausted() {
615            break;
616        }
617    }
618    preview.finish()
619}
620
621struct Preview {
622    text: String,
623    limit: usize,
624    exhausted: bool,
625}
626
627impl Preview {
628    fn new(limit: usize) -> Self {
629        Self {
630            text: String::with_capacity(limit),
631            limit,
632            exhausted: false,
633        }
634    }
635
636    fn is_empty(&self) -> bool {
637        self.text.is_empty()
638    }
639
640    fn is_exhausted(&self) -> bool {
641        self.exhausted
642    }
643
644    fn push_str(&mut self, text: &str) -> bool {
645        if self.exhausted {
646            return false;
647        }
648        let remaining = self.limit.saturating_sub(self.text.len());
649        if text.len() <= remaining {
650            self.text.push_str(text);
651            return true;
652        }
653
654        let mut end = remaining;
655        while !text.is_char_boundary(end) {
656            end -= 1;
657        }
658        self.text.push_str(&text[..end]);
659        self.exhausted = true;
660        false
661    }
662
663    fn push_value(&mut self, bytes: &[u8], span: &Range<u32>) {
664        match serde_json::from_slice::<Value>(slice(bytes, span)) {
665            Ok(value) => {
666                if serde_json::to_writer(&mut *self, &value).is_err() {
667                    self.exhausted = true;
668                }
669            }
670            Err(_) => {
671                self.push_str(&String::from_utf8_lossy(slice(bytes, span)));
672            }
673        }
674    }
675
676    fn finish(self) -> Option<String> {
677        (!self.text.is_empty()).then_some(self.text)
678    }
679}
680
681impl Write for Preview {
682    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
683        if bytes.is_empty() {
684            return Ok(0);
685        }
686        if self.exhausted {
687            return Err(io::ErrorKind::WriteZero.into());
688        }
689
690        let remaining = self.limit.saturating_sub(self.text.len());
691        let candidate = &bytes[..bytes.len().min(remaining)];
692        let end = std::str::from_utf8(candidate).map_or_else(|error| error.valid_up_to(), str::len);
693        if end == 0 {
694            self.exhausted = true;
695            return Err(io::ErrorKind::WriteZero.into());
696        }
697
698        self.text.push_str(
699            std::str::from_utf8(&candidate[..end])
700                .expect("a prefix ending on a UTF-8 boundary is valid"),
701        );
702        self.exhausted = end < bytes.len();
703        Ok(end)
704    }
705
706    fn flush(&mut self) -> io::Result<()> {
707        Ok(())
708    }
709}
710
711#[cfg(test)]
712mod tests {
713    use super::*;
714    use std::ffi::OsStr;
715
716    const DEMO_JSON: &str = include_str!("../examples/sample.json");
717
718    /// Parse and run the sweep to completion: structure-shape tests want the
719    /// whole tree present, exactly as the app sees it moments after startup.
720    fn parse(json: &str) -> Tree {
721        let mut tree = from_reader(json.as_bytes()).unwrap();
722        tree.index_all();
723        tree
724    }
725
726    fn names(tree: &Tree, ids: &[usize]) -> Vec<String> {
727        ids.iter().map(|&id| tree.name(id)).collect()
728    }
729
730    #[test]
731    fn object_members_become_ordered_roots_with_container_sizes_and_scalar_values() {
732        let tree = parse(
733            r#"{
734                "users": [{"id": 12, "name": "Ada"}, {"id": 27}, null],
735                "empty": [],
736                "settings": {},
737                "enabled": true
738            }"#,
739        );
740
741        assert_eq!(
742            names(&tree, tree.root_ids()),
743            ["users [3]", "empty []", "settings {}", "enabled: true"]
744        );
745        assert!(tree.is_container(tree.root_ids()[0]));
746        assert!(tree.is_container(tree.root_ids()[1]));
747        // Empty containers stay branches — they open to nothing.
748        assert!(!tree.is_leaf(tree.root_ids()[1]));
749    }
750
751    #[test]
752    fn json_containers_load_lazily_with_discovered_counts() {
753        let mut tree =
754            from_reader(r#"{"users": [{"id": 1}, {"id": 2}], "n": 3}"#.as_bytes()).unwrap();
755        let users = tree.root_ids()[0];
756
757        // The count was discovered while skipping the subtree, but no child
758        // nodes exist yet and the node still reads as expandable.
759        assert_eq!(tree.name(users), "users [2]");
760        assert!(tree.children_of(users).is_empty());
761        assert!(!tree.is_leaf(users));
762        assert!(!tree.fully_indexed());
763
764        assert!(tree.ensure_children(users));
765        assert_eq!(tree.children_of(users).len(), 2);
766        assert!(!tree.ensure_children(users), "loading is idempotent");
767
768        tree.index_all();
769        assert!(tree.fully_indexed());
770        assert!(tree.errors().is_empty());
771    }
772
773    #[test]
774    fn single_rooted_documents_materialize_level_one() {
775        let tree = from_reader("[1, [2], 3]".as_bytes()).unwrap();
776        let root = tree.root_ids()[0];
777        assert_eq!(tree.name(root), "$ [3]");
778        assert_eq!(
779            names(&tree, tree.children_of(root)),
780            ["[0]: 1", "[1] [1]", "[2]: 3"]
781        );
782    }
783
784    #[test]
785    fn array_elements_keep_their_order_and_include_object_previews() {
786        let tree = parse(r#"["rust", 7, {"id": 12, "name": "Ada"}, [null]]"#);
787        let root = tree.root_ids()[0];
788        let object = tree.children_of(root)[2];
789
790        assert_eq!(tree.name(root), "$ [4]");
791        assert_eq!(
792            names(&tree, tree.children_of(root)),
793            ["[0]: \"rust\"", "[1]: 7", "[2] {2}", "[3] [1]"]
794        );
795        assert_eq!(
796            tree.detail(object).as_deref(),
797            Some(r#"id: 12 · name: "Ada""#)
798        );
799    }
800
801    #[test]
802    fn object_previews_include_more_than_two_scalar_members() {
803        let tree = parse(r#"{"item":{"a":1,"b":2,"c":3}}"#);
804        let item = tree.root_ids()[0];
805
806        assert_eq!(tree.detail(item).as_deref(), Some("a: 1 · b: 2 · c: 3"));
807    }
808
809    #[test]
810    fn object_previews_are_bounded_and_remain_valid_utf8() {
811        let json = format!(r#"{{"item":{{"huge":"{}"}}}}"#, "😀".repeat(1_000));
812        let tree = parse(&json);
813        let item = tree.root_ids()[0];
814        let preview = tree.detail(item).unwrap();
815
816        assert!(preview.len() <= 512, "preview used {} bytes", preview.len());
817        assert!(preview.starts_with(r#"huge: ""#));
818    }
819
820    #[test]
821    fn object_previews_inspect_at_most_the_first_32_members() {
822        let mut object = serde_json::Map::new();
823        for index in 0..32 {
824            object.insert(format!("nested-{index}"), Value::Array(Vec::new()));
825        }
826        object.insert("too-late".to_owned(), Value::Bool(true));
827        let document = Value::Object(
828            [("item".to_owned(), Value::Object(object))]
829                .into_iter()
830                .collect(),
831        );
832        let tree = parse(&document.to_string());
833        let item = tree.root_ids()[0];
834
835        assert_eq!(tree.detail(item), None);
836    }
837
838    #[test]
839    fn every_node_outputs_its_canonical_json_pointer() {
840        let tree = parse(r#"["rust", {"a/b": {"~key": "value"}}]"#);
841        let root = tree.root_ids()[0];
842        let text = tree.children_of(root)[0];
843        let object = tree.children_of(root)[1];
844        let slash_key = tree.children_of(object)[0];
845        let tilde_key = tree.children_of(slash_key)[0];
846
847        assert_eq!(tree.output(root), OsStr::new(""));
848        assert_eq!(
849            tree.alternate_output(root),
850            OsStr::new(r#"["rust",{"a/b":{"~key":"value"}}]"#)
851        );
852        assert_eq!(tree.path(root), OsStr::new(""));
853        assert_eq!(tree.output(text), OsStr::new("/0"));
854        assert_eq!(tree.alternate_output(text), OsStr::new(r#""rust""#));
855        assert_eq!(tree.path(text), OsStr::new("/0"));
856        assert_eq!(tree.output(slash_key), OsStr::new("/1/a~1b"));
857        assert_eq!(tree.path(slash_key), OsStr::new("/1/a~1b"));
858        assert_eq!(tree.path(tilde_key), OsStr::new("/1/a~1b/~0key"));
859        assert_eq!(tree.relpath(tilde_key), tree.path(tilde_key));
860        assert_eq!(tree.output(tilde_key), OsStr::new("/1/a~1b/~0key"));
861        assert_eq!(tree.alternate_output(tilde_key), OsStr::new(r#""value""#));
862    }
863
864    #[test]
865    fn scalar_and_empty_object_roots_remain_selectable() {
866        let scalar = parse("null");
867        assert_eq!(names(&scalar, scalar.root_ids()), ["$: null"]);
868        assert_eq!(scalar.output(scalar.root_ids()[0]), OsStr::new(""));
869
870        let empty = parse("{}");
871        assert_eq!(names(&empty, empty.root_ids()), ["$ {}"]);
872        assert_eq!(empty.output(empty.root_ids()[0]), OsStr::new(""));
873        assert!(empty.is_container(empty.root_ids()[0]));
874        assert!(!empty.is_leaf(empty.root_ids()[0]));
875    }
876
877    #[test]
878    fn invalid_json_is_reported() {
879        let error = from_reader("{]".as_bytes()).unwrap_err();
880        assert!(error.to_string().starts_with("invalid JSON input:"));
881    }
882
883    #[test]
884    fn trailing_content_after_the_document_is_an_error() {
885        assert!(from_reader(r#"{"a": 1} extra"#.as_bytes()).is_err());
886        assert!(from_reader("[1, 2] [3]".as_bytes()).is_err());
887        assert!(from_reader("null null".as_bytes()).is_err());
888    }
889
890    #[test]
891    fn truncated_documents_are_an_error() {
892        assert!(from_reader(r#"{"a": "#.as_bytes()).is_err());
893        assert!(from_reader(r#"["unterminated"#.as_bytes()).is_err());
894        assert!(from_reader("".as_bytes()).is_err());
895    }
896
897    #[test]
898    fn escaped_keys_and_strings_render_unescaped_labels() {
899        let tree = parse("{\"k\\u0065y\": \"va\\u0041lue\"}");
900        assert_eq!(names(&tree, tree.root_ids()), [r#"key: "vaAlue""#]);
901    }
902
903    #[test]
904    fn numbers_render_through_json_value_normalization() {
905        let tree = parse(r#"[1e3, 0.5, -0]"#);
906        let root = tree.root_ids()[0];
907        assert_eq!(
908            names(&tree, tree.children_of(root)),
909            ["[0]: 1000.0", "[1]: 0.5", "[2]: -0.0"]
910        );
911    }
912
913    #[test]
914    fn jsonl_content_is_detected_and_presents_as_a_virtual_array() {
915        let tree = parse("{\"a\": 1}\n{\"a\": 2}\n");
916        let root = tree.root_ids()[0];
917
918        assert_eq!(names(&tree, tree.root_ids()), ["$ [2]"]);
919        assert!(tree.is_container(root));
920        assert_eq!(names(&tree, tree.children_of(root)), ["[0] {1}", "[1] {1}"]);
921    }
922
923    #[test]
924    fn jsonl_records_start_unvalidated_and_show_pending_counts() {
925        let tree = from_reader("{\"a\": 1}\n{\"a\": 2}\n".as_bytes()).unwrap();
926        let root = tree.root_ids()[0];
927
928        assert_eq!(tree.name(root), "$ [2]");
929        assert!(!tree.fully_indexed());
930        assert_eq!(names(&tree, tree.children_of(root)), ["[0] {…}", "[1] {…}"]);
931    }
932
933    #[test]
934    fn jsonl_detection_accepts_scalar_records() {
935        // Valid JSONL, invalid JSON — two number records.
936        let tree = parse("1\n2\n");
937        let root = tree.root_ids()[0];
938        assert_eq!(names(&tree, tree.root_ids()), ["$ [2]"]);
939        assert_eq!(names(&tree, tree.children_of(root)), ["[0]: 1", "[1]: 2"]);
940    }
941
942    #[test]
943    fn a_single_record_with_trailing_newline_stays_json() {
944        // Simultaneously valid JSON and one-record JSONL; detection picks
945        // JSON, and --jsonl exists to force the other reading.
946        let tree = parse("{\"a\": 1}\n");
947        assert_eq!(names(&tree, tree.root_ids()), ["a: 1"]);
948    }
949
950    #[test]
951    fn pretty_printed_json_is_not_mistaken_for_jsonl() {
952        let tree = parse("{\n  \"a\": 1,\n  \"b\": 2\n}\n");
953        assert_eq!(names(&tree, tree.root_ids()), ["a: 1", "b: 2"]);
954    }
955
956    #[test]
957    fn jsonl_skips_blank_lines_and_indices_are_record_ordinals() {
958        let tree = parse("1\n\n  \n2\n");
959        let root = tree.root_ids()[0];
960        assert_eq!(names(&tree, tree.root_ids()), ["$ [2]"]);
961        assert_eq!(names(&tree, tree.children_of(root)), ["[0]: 1", "[1]: 2"]);
962    }
963
964    #[test]
965    fn jsonl_drops_a_truncated_final_record() {
966        let tree = parse("{\"a\": 1}\n{\"b\": tru");
967        let root = tree.root_ids()[0];
968        assert_eq!(names(&tree, tree.root_ids()), ["$ [1]"]);
969        assert_eq!(names(&tree, tree.children_of(root)), ["[0] {1}"]);
970    }
971
972    #[test]
973    fn jsonl_surfaces_a_malformed_middle_record_as_an_error_node() {
974        // Under lazy validation a corrupt record no longer fails the load; it
975        // becomes a selectable error leaf plus a banner/exit message, and its
976        // neighbors are untouched.
977        let mut tree = from_reader("{\"a\": 1}\nxxx\n{\"b\": 2}\n".as_bytes()).unwrap();
978        tree.index_all();
979        let root = tree.root_ids()[0];
980        let records = tree.children_of(root).to_vec();
981
982        assert_eq!(records.len(), 3);
983        assert_eq!(tree.name(records[0]), "[0] {1}");
984        assert_eq!(tree.name(records[1]), "[1] ⚠");
985        assert_eq!(tree.detail(records[1]).as_deref(), Some("xxx"));
986        assert_eq!(tree.name(records[2]), "[2] {1}");
987        assert!(tree.is_leaf(records[1]));
988
989        assert_eq!(tree.errors().len(), 1);
990        assert!(
991            tree.errors()[0].contains("line 2"),
992            "error should carry the line number: {:?}",
993            tree.errors()[0]
994        );
995
996        // The error node keeps its addresses; its raw text is the alternate.
997        assert_eq!(tree.path(records[1]), OsStr::new("/1"));
998        assert_eq!(tree.relpath(records[1]), OsStr::new(""));
999        assert_eq!(tree.jump_key(records[1]), "/1");
1000        assert_eq!(tree.alternate_output(records[1]), OsStr::new("xxx"));
1001    }
1002
1003    #[test]
1004    fn jsonl_nodes_carry_global_and_record_relative_addresses() {
1005        let tree = parse("{\"user\": {\"name\": \"Ada\"}}\n{\"user\": {\"name\": \"Bo\"}}\n");
1006        let root = tree.root_ids()[0];
1007        let record = tree.children_of(root)[1];
1008        let user = tree.children_of(record)[0];
1009        let name = tree.children_of(user)[0];
1010
1011        assert_eq!(tree.path(root), OsStr::new(""));
1012        assert_eq!(tree.path(record), OsStr::new("/1"));
1013        assert_eq!(tree.relpath(record), OsStr::new(""));
1014        assert_eq!(tree.output(record), OsStr::new("/1"));
1015        assert_eq!(
1016            tree.alternate_output(record),
1017            OsStr::new(r#"{"user":{"name":"Bo"}}"#)
1018        );
1019        assert_eq!(tree.path(name), OsStr::new("/1/user/name"));
1020        assert_eq!(tree.relpath(name), OsStr::new("/user/name"));
1021        assert_eq!(tree.jump_key(name), "/1/user/name");
1022        assert_eq!(tree.jump_key(record), "/1");
1023    }
1024
1025    #[test]
1026    fn forced_jsonl_reads_a_single_record_as_one_element() {
1027        let mut tree = jsonl_from_reader("{\"a\": 1}\n".as_bytes()).unwrap();
1028        tree.index_all();
1029        let root = tree.root_ids()[0];
1030        assert_eq!(names(&tree, tree.root_ids()), ["$ [1]"]);
1031        assert_eq!(names(&tree, tree.children_of(root)), ["[0] {1}"]);
1032    }
1033
1034    #[test]
1035    fn jsonl_startup_builds_only_the_record_level() {
1036        let mut input = String::new();
1037        for i in 0..10_000 {
1038            input.push_str(&format!("{{\"id\": {i}, \"tags\": [1, 2, 3]}}\n"));
1039        }
1040        let mut tree = from_reader(input.as_bytes()).unwrap();
1041
1042        // Root + one node per record; nothing below records exists yet.
1043        assert_eq!(tree.len(), 1 + 10_000);
1044        assert!(!tree.fully_indexed());
1045
1046        tree.index_all();
1047        assert!(tree.fully_indexed());
1048        assert!(tree.errors().is_empty());
1049        // Each record gains 2 members, each tags array 3 elements.
1050        assert_eq!(tree.len(), 1 + 10_000 + 10_000 * 2 + 10_000 * 3);
1051    }
1052
1053    #[test]
1054    fn demo_sample_exercises_the_json_tree_shapes() {
1055        let tree = parse(DEMO_JSON);
1056
1057        assert_eq!(
1058            names(&tree, tree.root_ids()),
1059            ["project {4}", "users [3]", "settings {}", "version: 1"]
1060        );
1061        assert_eq!(
1062            tree.detail(tree.root_ids()[0]).as_deref(),
1063            Some(r#"name: "ite" · status: "experimental""#)
1064        );
1065        let users = tree.root_ids()[1];
1066        assert_eq!(tree.children_of(users).len(), 3);
1067        assert_eq!(tree.name(tree.children_of(users)[2]), "[2]: null");
1068    }
1069}