Skip to main content

omnist/
document.rs

1//! The Document model — a canonical tree of ordered, labeled edges.
2//!
3//! Ported from `~/dev/omnist/omnist/document.py` (see issue #4). A Document
4//! **node** is either a **leaf** holding a [`Scalar`], or an **internal
5//! node** holding an *ordered list of edges*, each a `(label, child)` pair.
6//! **Labels may repeat** — "many members" is the label `member` appearing
7//! several times, not a field pointing to an array.
8//!
9//! ## Architecture (per issue #1, "architecture freedom")
10//!
11//! This port uses an arena: nodes live in a `Vec<Entry>` inside [`Doc`],
12//! referenced by [`NodeId`] (an index newtype), not `Rc<RefCell<_>>`. Two
13//! consequences that don't mirror Python/TypeScript 1:1:
14//!
15//! - **No cycle detection.** Python/TS guard against cycles because a plain
16//!   `dict`/object can be made self-referential through shared mutable
17//!   references. [`Value`] (this port's "plain value" input type, analogous
18//!   to a parsed JSON value) is a plain owned tree — building a
19//!   self-referential `Value` without `unsafe` or `Rc<RefCell<_>>` isn't
20//!   possible, so the whole bug class is closed by the type system rather
21//!   than checked at runtime (see the workflow playbook's "what NOT to
22//!   carry over unexamined").
23//! - **Integer-digit security cap.** Python's `_check_int_digits` defends
24//!   against `str()`-converting an arbitrary-precision `int` with
25//!   thousands of digits (a superlinear operation). Since issue #104,
26//!   `Scalar::Int` is backed by arbitrary-precision `num_bigint::BigInt`,
27//!   and format decoders (JSON, YAML, TOML) enforce the security cap
28//!   (`crate::limits::MAX_INT_DIGITS`, 4300 digits) during lexing/parsing
29//!   before BigInt construction.
30//!
31//! Observable behavior for everything else (construction, depth guard,
32//! edge ordering, mutation semantics) matches the Python spec.
33
34use indexmap::IndexMap;
35use std::fmt;
36
37use crate::error::DocumentError;
38use crate::limits::{Limits, Resolved, int_exceeds};
39
40/// Default maximum nesting depth for a Document node (matches Python's
41/// `_MAX_DEPTH`). Configurable per read through [`Limits::max_depth`].
42pub const MAX_DEPTH: usize = crate::limits::DEFAULT_MAX_DEPTH as usize;
43
44/// Default maximum node count for a single Document (matches the reference
45/// default in omnist-spec docs/02-document-model.md Sec2.4: a depth limit
46/// alone doesn't bound a shallow-but-enormous document, e.g. a million
47/// sibling edges at depth 1). A node is a container (an edge list), as D-9
48/// counts them: keys and scalar values are not nodes. Enforced once, in
49/// `push`, the single arena choke point every construction path
50/// (`build_node`, `push_raw`) funnels through -- see omnist-rs#78.
51/// Configurable per read through [`Limits::max_nodes`].
52pub const MAX_NODES: usize = crate::limits::DEFAULT_MAX_NODES as usize;
53
54/// An index into a [`Doc`]'s arena. Opaque outside this module's crate.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub struct NodeId(usize);
57
58/// A leaf value.
59///
60/// `Int` is arbitrary-precision (`BigInt`), not a fixed-width integer --
61/// omnist-spec §2.2 defines `integer` as arbitrary-precision, bounded only
62/// by the shared digit-count cap (`MAX_INT_DIGITS`), the same way Python's
63/// native `int` and Go's `*big.Int` already are (issue #104; previously
64/// `i64`, which silently rejected any literal past ~19 digits -- a
65/// spec-conformance bug, not a permitted narrower-limit variation, since no
66/// digit-count override was in play).
67///
68/// `Date`/`Time`/`Datetime` (issue #105) each hold their already
69/// shape-validated, canonical ISO spelling -- no `chrono`/`time` crate
70/// dependency, since the algebra never does temporal arithmetic, only
71/// equality and canonical rendering (the same reasoning issue #104 applied
72/// to `Int`). Constructing one always goes through
73/// `crate::schema::is_iso_date`/`is_iso_time`/`is_iso_datetime` (shape
74/// validation) and, for `Time`/`Datetime`,
75/// `crate::schema::canonicalize_iso_time`/`canonicalize_iso_datetime`
76/// (canonicalization) -- there is no code path that constructs one of
77/// these holding un-validated or non-canonical text. This closed the
78/// architecture gap issue #16 originally left open (previously cited as a
79/// reason `Scalar` had no temporal variant at all); see issue #99's
80/// `RawNode::TemporalLeaf`, now removed, for the write-hint mechanism this
81/// replaces.
82#[derive(Debug, Clone, PartialEq)]
83pub enum Scalar {
84    /// Null scalar value (spec §2.2).
85    Null,
86    /// Boolean scalar value (spec §2.2).
87    Bool(bool),
88    /// Arbitrary-precision integer scalar value (spec §2.2).
89    Int(num_bigint::BigInt),
90    /// IEEE 754 floating point scalar value (spec §2.2).
91    Float(f64),
92    /// UTF-8 string scalar value (spec §2.2).
93    Str(String),
94    /// ISO 8601 calendar date (`YYYY-MM-DD`, spec §2.2).
95    Date(String),
96    /// ISO 8601 clock time (`hh:mm:ss`, spec §2.2).
97    Time(String),
98    /// ISO 8601 date-time (`YYYY-MM-DDThh:mm:ss`, spec §2.2).
99    Datetime(String),
100}
101
102impl fmt::Display for Scalar {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match self {
105            Scalar::Null => write!(f, "null"),
106            Scalar::Bool(b) => write!(f, "{b}"),
107            Scalar::Int(i) => write!(f, "{i}"),
108            Scalar::Float(x) => write!(f, "{x}"),
109            Scalar::Str(s) => write!(f, "{s:?}"),
110            Scalar::Date(s) | Scalar::Time(s) | Scalar::Datetime(s) => write!(f, "{s}"),
111        }
112    }
113}
114
115/// A plain input value (analogous to a parsed JSON/YAML/TOML value), the
116/// thing [`Doc::of`]/[`Doc::add`]/[`Doc::set`] turn into canonical nodes.
117///
118/// `Object` uses [`IndexMap`] (per issue #1 §2) so key order — which the
119/// Document model treats as data, not incidental — survives construction.
120#[derive(Debug, Clone, PartialEq)]
121pub enum Value {
122    /// Null value.
123    Null,
124    /// Boolean value.
125    Bool(bool),
126    /// Arbitrary-precision integer value.
127    Int(num_bigint::BigInt),
128    /// Floating point value.
129    Float(f64),
130    /// String value.
131    Str(String),
132    /// See [`Scalar::Date`]/[`Scalar::Time`]/[`Scalar::Datetime`] (issue
133    /// #105) -- always already shape-validated and canonical, same
134    /// invariant.
135    Date(String),
136    /// ISO time value, matching [`Scalar::Time`].
137    Time(String),
138    /// ISO datetime value, matching [`Scalar::Datetime`].
139    Datetime(String),
140    /// Heterogeneous array value.
141    Array(Vec<Value>),
142    /// Key-value object mapping.
143    Object(IndexMap<String, Value>),
144}
145
146impl Value {
147    /// Borrow the underlying map if this is an `Object`, else `None`.
148    /// Used by tests to inspect `to_grouped()`'s output without a
149    /// match arm that (for any one call site) is only ever exercised on
150    /// one side -- see the test suite for both an `Object` and a
151    /// non-`Object` call site.
152    #[cfg(test)]
153    fn as_object(&self) -> Option<&IndexMap<String, Value>> {
154        match self {
155            Value::Object(m) => Some(m),
156            _ => None,
157        }
158    }
159}
160
161impl From<Scalar> for Value {
162    fn from(s: Scalar) -> Self {
163        match s {
164            Scalar::Null => Value::Null,
165            Scalar::Bool(b) => Value::Bool(b),
166            Scalar::Int(i) => Value::Int(i),
167            Scalar::Float(x) => Value::Float(x),
168            Scalar::Str(s) => Value::Str(s),
169            Scalar::Date(s) => Value::Date(s),
170            Scalar::Time(s) => Value::Time(s),
171            Scalar::Datetime(s) => Value::Datetime(s),
172        }
173    }
174}
175
176#[derive(Debug, Clone)]
177enum NodeData {
178    Leaf(Scalar),
179    Internal(Vec<(String, NodeId)>),
180}
181
182#[derive(Debug, Clone)]
183struct Entry {
184    data: NodeData,
185    /// This node's own depth relative to the document root (root = 0).
186    /// Recorded at construction time so a later mutation rooted at this
187    /// node (add/set) can seed the depth guard from *here*, not from 0 --
188    /// this is the exact bug class from omnist-ts#37 (a depth check reset
189    /// to 0 on every mutation instead of accounting for how deep the
190    /// mutation's attachment point already is) and omnist-ts#70 (a second
191    /// writer that had the same reset bug because the guard lived in one
192    /// place, not one shared helper called from every entry point).
193    depth: usize,
194}
195
196/// The shared depth guard. Every construction/mutation path that can grow
197/// the tree calls this before creating a node -- see the module-level test
198/// `every_tree_mutating_entry_point_enforces_the_depth_guard` for the audit
199/// that walks every public entry point and confirms each one does.
200pub(crate) fn check_write_depth(
201    depth: usize,
202    path: &str,
203    max_depth: usize,
204) -> Result<(), DocumentError> {
205    if depth > max_depth {
206        return Err(DocumentError::with_code(
207            path,
208            "document.limit.depth",
209            format!("nesting exceeds the maximum depth ({max_depth})"),
210        ));
211    }
212    Ok(())
213}
214
215/// The node arena plus what the guards need: the count of containers (the
216/// nodes D-9 counts) and the limits the document was built under, so a later
217/// mutation enforces the same ones.
218#[derive(Debug, Clone)]
219struct Arena {
220    entries: Vec<Entry>,
221    containers: usize,
222    limits: Resolved,
223}
224
225impl Arena {
226    fn new(limits: Resolved) -> Arena {
227        Arena {
228            entries: Vec::new(),
229            containers: 0,
230            limits,
231        }
232    }
233}
234
235pub(crate) fn join(path: &str, key: &str) -> String {
236    let mut s = String::with_capacity(path.len() + key.len() + 4);
237    s.push_str(path);
238    crate::report::push_label_segment(&mut s, key);
239    s
240}
241
242/// One (path, depth, value) triple to be turned into a child node -- mirrors
243/// Python's `_children` generator, including its extra depth level for
244/// items of an array value (an array value sits one level deeper than a
245/// plain scalar/object value under the same key).
246struct ChildSpec<'a> {
247    path: String,
248    depth: usize,
249    value: &'a Value,
250}
251
252fn child_specs<'a>(
253    v: &'a Value,
254    path: &str,
255    depth: usize,
256) -> Result<Vec<ChildSpec<'a>>, DocumentError> {
257    match v {
258        Value::Array(items) => {
259            let mut out = Vec::with_capacity(items.len());
260            for (i, item) in items.iter().enumerate() {
261                // E-10: the index appears only when the label repeats.
262                let ip = if items.len() == 1 {
263                    path.to_string()
264                } else {
265                    format!("{path}[{i}]")
266                };
267                if matches!(item, Value::Array(_)) {
268                    return Err(DocumentError::with_code(
269                        ip,
270                        "document.unlabeled-element",
271                        "an array of arrays has no labeled-edge form",
272                    ));
273                }
274                out.push(ChildSpec {
275                    path: ip,
276                    depth: depth + 1,
277                    value: item,
278                });
279            }
280            Ok(out)
281        }
282        other => Ok(vec![ChildSpec {
283            path: path.to_string(),
284            depth,
285            value: other,
286        }]),
287    }
288}
289
290/// Turn a plain [`Value`] into a canonical node inside `arena`, returning
291/// its [`NodeId`]. Mirrors Python's `build_node`.
292fn build_node(
293    arena: &mut Arena,
294    value: &Value,
295    path: &str,
296    depth: usize,
297) -> Result<NodeId, DocumentError> {
298    check_write_depth(depth, path, arena.limits.max_depth)?;
299    match value {
300        Value::Object(map) => {
301            let mut edges = Vec::new();
302            for (k, v) in map {
303                let kp = join(path, k);
304                for spec in child_specs(v, &kp, depth + 1)? {
305                    let cid = build_node(arena, spec.value, &spec.path, spec.depth)?;
306                    edges.push((k.clone(), cid));
307                }
308            }
309            push(arena, NodeData::Internal(edges), depth, path)
310        }
311        Value::Array(_) => Err(DocumentError::with_code(
312            path,
313            "document.unlabeled-element",
314            "a bare array has no labeled-edge form (arrays appear only as a repeated field)",
315        )),
316        // Scalars are matched directly as top-level arms of this match
317        // (rather than through a `scalar => { match scalar { ... } }`
318        // catch-all) so every arm is a real, exhaustive possibility --
319        // no `Value::Array(_) | Value::Object(_) => unreachable!()` arm
320        // to justify, since those two variants are already handled above.
321        Value::Null => push(arena, NodeData::Leaf(Scalar::Null), depth, path),
322        Value::Bool(b) => push(arena, NodeData::Leaf(Scalar::Bool(*b)), depth, path),
323        Value::Int(i) => {
324            if int_exceeds(i, arena.limits.max_int_digits) {
325                return Err(arena.limits.int_digits_error(path));
326            }
327            push(arena, NodeData::Leaf(Scalar::Int(i.clone())), depth, path)
328        }
329        Value::Float(x) => push(arena, NodeData::Leaf(Scalar::Float(*x)), depth, path),
330        Value::Date(s) => push(arena, NodeData::Leaf(Scalar::Date(s.clone())), depth, path),
331        Value::Time(s) => push(arena, NodeData::Leaf(Scalar::Time(s.clone())), depth, path),
332        Value::Datetime(s) => push(
333            arena,
334            NodeData::Leaf(Scalar::Datetime(s.clone())),
335            depth,
336            path,
337        ),
338        Value::Str(s) => push(arena, NodeData::Leaf(Scalar::Str(s.clone())), depth, path),
339    }
340}
341
342/// The shared node-count guard, checked once here since every construction
343/// path (`build_node`, `push_raw`) funnels through this single function.
344fn push(
345    arena: &mut Arena,
346    data: NodeData,
347    depth: usize,
348    path: &str,
349) -> Result<NodeId, DocumentError> {
350    if matches!(data, NodeData::Internal(_)) {
351        if arena.containers >= arena.limits.max_nodes {
352            return Err(DocumentError::with_code(
353                path,
354                "document.limit.nodes",
355                format!(
356                    "document exceeds the maximum node count ({})",
357                    arena.limits.max_nodes
358                ),
359            ));
360        }
361        arena.containers += 1;
362    }
363    let id = NodeId(arena.entries.len());
364    arena.entries.push(Entry { data, depth });
365    Ok(id)
366}
367
368/// A guarded handle on a Document tree: an arena of nodes plus the root.
369#[derive(Debug, Clone)]
370pub struct Doc {
371    arena: Arena,
372    root: NodeId,
373}
374
375impl Doc {
376    /// Build a `Doc` from a plain [`Value`].
377    pub fn of(value: &Value) -> Result<Doc, DocumentError> {
378        Doc::of_resolved(value, Resolved::DEFAULT)
379    }
380
381    /// Build a `Doc` from a plain [`Value`] under explicit [`Limits`]; see
382    /// [`Doc::of`]. Returns the [`Limits::validate`] error unchanged when
383    /// the limits are out of range. The document remembers the limits, so a
384    /// later [`Doc::add`] / [`Doc::set`] enforces the same ones.
385    pub fn of_with(value: &Value, limits: &Limits) -> Result<Doc, DocumentError> {
386        Doc::of_resolved(value, limits.resolve()?)
387    }
388
389    pub(crate) fn of_resolved(value: &Value, limits: Resolved) -> Result<Doc, DocumentError> {
390        let mut arena = Arena::new(limits);
391        let root = build_node(&mut arena, value, "$", 0)?;
392        Ok(Doc { arena, root })
393    }
394
395    /// A cursor to the document root, at path `"$"`.
396    pub fn root(&self) -> Cursor<'_> {
397        Cursor {
398            doc: self,
399            id: self.root,
400            path: "$".to_string(),
401        }
402    }
403
404    fn entry(&self, id: NodeId) -> &Entry {
405        &self.arena.entries[id.0]
406    }
407
408    /// Append an edge `(label, value)` under the node at `at`/`path`. A
409    /// repeated label is how an array grows. Returns the new edge's
410    /// `NodeId`.
411    pub fn add(
412        &mut self,
413        at: NodeId,
414        path: &str,
415        label: &str,
416        value: &Value,
417    ) -> Result<NodeId, DocumentError> {
418        self.require_internal(at, path, "add")?;
419        let attach_depth = self.entry(at).depth;
420        let child_path = join(path, label);
421        let cid = build_node(&mut self.arena, value, &child_path, attach_depth + 1)?;
422        let edges = self.internal_edges_mut(at, path, "add")?;
423        edges.push((label.to_string(), cid));
424        Ok(cid)
425    }
426
427    /// Replace all edges under `label` with a single new edge, positioned
428    /// at the first old occurrence (`set` = `remove` + `add`).
429    pub fn set(
430        &mut self,
431        at: NodeId,
432        path: &str,
433        label: &str,
434        value: &Value,
435    ) -> Result<NodeId, DocumentError> {
436        self.require_internal(at, path, "set")?;
437        let attach_depth = self.entry(at).depth;
438        let child_path = join(path, label);
439        let cid = build_node(&mut self.arena, value, &child_path, attach_depth + 1)?;
440        let edges = self.internal_edges_mut(at, path, "set")?;
441        let mut first: Option<usize> = None;
442        let mut kept: Vec<(String, NodeId)> = Vec::with_capacity(edges.len());
443        for (lbl, child) in edges.drain(..) {
444            if lbl == label {
445                if first.is_none() {
446                    first = Some(kept.len());
447                    kept.push((label.to_string(), cid));
448                }
449                // later duplicates are dropped
450            } else {
451                kept.push((lbl, child));
452            }
453        }
454        if first.is_none() {
455            kept.push((label.to_string(), cid));
456        }
457        *edges = kept;
458        Ok(cid)
459    }
460
461    /// Remove every edge under `label`.
462    pub fn remove(&mut self, at: NodeId, path: &str, label: &str) -> Result<(), DocumentError> {
463        self.require_internal(at, path, "remove")?;
464        let edges = self.internal_edges_mut(at, path, "remove")?;
465        edges.retain(|(lbl, _)| lbl != label);
466        Ok(())
467    }
468
469    fn require_internal(&self, id: NodeId, path: &str, op: &str) -> Result<(), DocumentError> {
470        match self.entry(id).data {
471            NodeData::Internal(_) => Ok(()),
472            NodeData::Leaf(_) => Err(DocumentError::new(path, format!("cannot {op} on a leaf"))),
473        }
474    }
475
476    /// The mutable edge list at `at`, or an error if it's a leaf.
477    ///
478    /// `add`/`set`/`remove` all call `require_internal` first (preserving
479    /// Python's check-leaf-before-build-value error ordering), so in
480    /// practice this function's `Leaf` arm never fires through the public
481    /// API -- it exists so the actual mutation site is a real two-armed
482    /// match (both arms reachable and independently tested, see
483    /// `internal_edges_mut_rejects_a_leaf_directly` below) instead of an
484    /// `unreachable!()`/if-let-without-else that would leave a
485    /// structurally-dead branch for `cargo llvm-cov` to flag.
486    fn internal_edges_mut(
487        &mut self,
488        at: NodeId,
489        path: &str,
490        op: &str,
491    ) -> Result<&mut Vec<(String, NodeId)>, DocumentError> {
492        match &mut self.arena.entries[at.0].data {
493            NodeData::Internal(edges) => Ok(edges),
494            NodeData::Leaf(_) => Err(DocumentError::new(path, format!("cannot {op} on a leaf"))),
495        }
496    }
497
498    /// A JSON-shaped projection of the whole document: same-label edges
499    /// grouped into an array; a label seen once stays a single value.
500    pub fn to_grouped(&self) -> Value {
501        self.grouped_at(self.root)
502    }
503
504    /// `true` iff [`Doc::to_grouped`]'s grouping (same-label edges
505    /// collapsed into an array, in first-seen order) changes the relative
506    /// order of edges anywhere in the document -- i.e. some node has two
507    /// edges under the same label with a *different* label's edge between
508    /// them (`[(m,A),(x,X),(m,B)]`), as opposed to a label merely
509    /// repeating contiguously (`[(m,A),(m,B),(x,X)]`), which grouping
510    /// reorders nothing for. Used by the JSON-family writers
511    /// (`json.rs`/`yaml.rs`/`toml.rs`, all built on `to_grouped`) to emit
512    /// `format.interleaving-lost` (spec Sec8.3.8, D-3) at the whole-document
513    /// path `$`, since the loss isn't localized to one label's edges.
514    pub(crate) fn has_interleaving_loss(&self) -> bool {
515        self.node_has_interleaving_loss(self.root)
516    }
517
518    fn node_has_interleaving_loss(&self, id: NodeId) -> bool {
519        match &self.entry(id).data {
520            NodeData::Leaf(_) => false,
521            NodeData::Internal(edges) => {
522                let mut closed: std::collections::HashSet<&str> = std::collections::HashSet::new();
523                let mut prev_label: Option<&str> = None;
524                for (label, _) in edges {
525                    if let Some(p) = prev_label
526                        && p != label.as_str()
527                    {
528                        closed.insert(p);
529                    }
530                    if closed.contains(label.as_str()) {
531                        return true;
532                    }
533                    prev_label = Some(label.as_str());
534                }
535                edges
536                    .iter()
537                    .any(|(_, child)| self.node_has_interleaving_loss(*child))
538            }
539        }
540    }
541
542    // Note: unlike `build_node`/`data_at`, this has no depth parameter --
543    // it walks an already-validated tree (every node was depth-checked at
544    // construction time), so there's nothing left to guard here.
545    fn grouped_at(&self, id: NodeId) -> Value {
546        match &self.entry(id).data {
547            NodeData::Leaf(s) => Value::from(s.clone()),
548            NodeData::Internal(edges) => {
549                let mut counts: IndexMap<&str, usize> = IndexMap::new();
550                for (label, _) in edges {
551                    *counts.entry(label.as_str()).or_insert(0) += 1;
552                }
553                let mut out: IndexMap<String, Value> = IndexMap::new();
554                for (label, child) in edges {
555                    let g = self.grouped_at(*child);
556                    if counts[label.as_str()] > 1 {
557                        match out.get_mut(label.as_str()) {
558                            Some(Value::Array(arr)) => arr.push(g),
559                            _ => {
560                                out.insert(label.clone(), Value::Array(vec![g]));
561                            }
562                        }
563                    } else {
564                        out.insert(label.clone(), g);
565                    }
566                }
567                Value::Object(out)
568            }
569        }
570    }
571
572    /// A lossless copy of the whole document back into [`Value`] form.
573    pub fn to_data(&self) -> Value {
574        self.data_at(self.root)
575    }
576
577    fn data_at(&self, id: NodeId) -> Value {
578        match &self.entry(id).data {
579            NodeData::Leaf(s) => Value::from(s.clone()),
580            NodeData::Internal(edges) => {
581                let mut map = IndexMap::new();
582                // Structural copy only -- this intentionally does NOT
583                // re-group repeated labels (see to_grouped for that); a
584                // repeated label here just overwrites in the IndexMap,
585                // which is why to_data is for equality/structural
586                // comparison, not for JSON-shaped export of repeated
587                // fields. Kept simple: only used by `eq` in this issue.
588                for (label, child) in edges {
589                    map.insert(label.clone(), self.data_at(*child));
590                }
591                Value::Object(map)
592            }
593        }
594    }
595
596    /// Structural equality between two documents (same shape, same edge
597    /// order, same labels, same leaf values).
598    pub fn eq_doc(&self, other: &Doc) -> bool {
599        self.node_eq(self.root, other, other.root)
600    }
601
602    fn node_eq(&self, a: NodeId, other: &Doc, b: NodeId) -> bool {
603        match (&self.entry(a).data, &other.entry(b).data) {
604            (NodeData::Leaf(x), NodeData::Leaf(y)) => x == y,
605            (NodeData::Internal(xs), NodeData::Internal(ys)) => {
606                xs.len() == ys.len()
607                    && xs
608                        .iter()
609                        .zip(ys.iter())
610                        .all(|((la, ca), (lb, cb))| la == lb && self.node_eq(*ca, other, *cb))
611            }
612            _ => false,
613        }
614    }
615}
616
617/// A read-only cursor into a [`Doc`]'s tree, tracking its own path.
618///
619/// Path is owned per-cursor (rather than borrowed) because it's built
620/// incrementally (`$.a.b[1]`) as cursors descend; it's only needed for
621/// error messages and equality with Python's `Doc.path`, not perf-critical
622/// traversal.
623#[derive(Debug, Clone)]
624pub struct Cursor<'a> {
625    doc: &'a Doc,
626    id: NodeId,
627    /// The path of this cursor within the document.
628    pub path: String,
629}
630
631impl<'a> Cursor<'a> {
632    /// The internal [`NodeId`] of the referenced node.
633    pub fn id(&self) -> NodeId {
634        self.id
635    }
636
637    /// Returns `true` iff this node is a leaf holding a scalar.
638    pub fn is_leaf(&self) -> bool {
639        matches!(self.doc.entry(self.id).data, NodeData::Leaf(_))
640    }
641
642    /// Returns the scalar value if this is a leaf node, or `Err` if it is an internal edge node.
643    pub fn value(&self) -> Result<&'a Scalar, DocumentError> {
644        match &self.doc.entry(self.id).data {
645            NodeData::Leaf(s) => Ok(s),
646            NodeData::Internal(_) => Err(DocumentError::new(&self.path, "not a leaf; use edges()")),
647        }
648    }
649
650    /// Returns the outgoing labeled edges if this is an internal node, or `Err` if it is a leaf.
651    pub fn edges(&self) -> Result<Vec<(String, Cursor<'a>)>, DocumentError> {
652        match &self.doc.entry(self.id).data {
653            NodeData::Internal(edges) => {
654                let mut out = Vec::with_capacity(edges.len());
655                for (label, index, child) in with_occurrence_indices(edges) {
656                    let cp = crate::report::child_path(&self.path, label, index);
657                    out.push((
658                        label.to_string(),
659                        Cursor {
660                            doc: self.doc,
661                            id: child,
662                            path: cp,
663                        },
664                    ));
665                }
666                Ok(out)
667            }
668            NodeData::Leaf(_) => Err(DocumentError::new(&self.path, "a leaf has no edges")),
669        }
670    }
671
672    /// Like [`Cursor::edges`], but doesn't build a path `String` for every
673    /// child up front (issue #44) -- returns each edge's label, its
674    /// same-label occurrence index, and its `NodeId`, leaving path
675    /// construction to the caller. Paired with [`Cursor::seek`], used by
676    /// `schema.rs`'s `conform_record` (the validate hot path), which reuses
677    /// one path buffer for the whole tree walk instead of allocating one
678    /// `String` per edge regardless of whether that edge ever needs one.
679    pub(crate) fn internal_edges(&self) -> Result<&'a [(String, NodeId)], DocumentError> {
680        match &self.doc.entry(self.id).data {
681            NodeData::Internal(edges) => Ok(edges),
682            NodeData::Leaf(_) => Err(DocumentError::new(&self.path, "a leaf has no edges")),
683        }
684    }
685
686    /// Each edge's label, its E-10 index (`Some(i)` exactly when the label
687    /// occurs more than once in this node) and its `NodeId`.
688    pub(crate) fn raw_edges(&self) -> Result<Vec<RawEdge<'a>>, DocumentError> {
689        match &self.doc.entry(self.id).data {
690            NodeData::Internal(edges) => Ok(with_occurrence_indices(edges).collect()),
691            NodeData::Leaf(_) => Err(DocumentError::new(&self.path, "a leaf has no edges")),
692        }
693    }
694
695    /// Build a cursor for `id` without a meaningful `path` -- only valid for
696    /// callers (like `schema.rs`'s buffer-threaded `conform`) that never
697    /// read the returned cursor's `path` field directly, tracking the real
698    /// path in their own reused buffer instead. Paired with [`Cursor::raw_edges`].
699    pub(crate) fn seek(&self, id: NodeId) -> Cursor<'a> {
700        Cursor {
701            doc: self.doc,
702            id,
703            path: String::new(),
704        }
705    }
706
707    /// Returns a deduplicated list of child edge labels.
708    pub fn labels(&self) -> Vec<String> {
709        let mut seen = std::collections::HashSet::new();
710        let mut out = Vec::new();
711        if let NodeData::Internal(edges) = &self.doc.entry(self.id).data {
712            for (label, _) in edges {
713                if seen.insert(label.clone()) {
714                    out.push(label.clone());
715                }
716            }
717        }
718        out
719    }
720
721    /// Returns all child cursors along edges with the given `label`.
722    pub fn get(&self, label: &str) -> Vec<Cursor<'a>> {
723        self.edges()
724            .into_iter()
725            .flatten()
726            .filter(|(lbl, _)| lbl == label)
727            .map(|(_, c)| c)
728            .collect()
729    }
730
731    /// Returns the single child cursor for `label`, or `Err` if there are 0 or >1 matching edges.
732    pub fn get_one(&self, label: &str) -> Result<Cursor<'a>, DocumentError> {
733        let mut cs = self.get(label);
734        if cs.len() != 1 {
735            return Err(DocumentError::new(
736                &self.path,
737                format!("expected exactly one {label:?}, found {}", cs.len()),
738            ));
739        }
740        Ok(cs.remove(0))
741    }
742
743    /// Returns the number of edges with the given `label`.
744    pub fn count(&self, label: &str) -> usize {
745        if let NodeData::Internal(edges) = &self.doc.entry(self.id).data {
746            edges.iter().filter(|(lbl, _)| lbl == label).count()
747        } else {
748            0
749        }
750    }
751
752    /// A cursor to the single child under `label`.
753    pub fn child(&self, label: &str) -> Result<Cursor<'a>, DocumentError> {
754        self.get_one(label)
755    }
756
757    /// A lossless [`RawNode`] copy of the subtree rooted at this cursor,
758    /// preserving edge order and interleaving exactly. Used by
759    /// `crate::materialize` to carry an untouched subtree forward (e.g. an
760    /// unexpected field it still has to emit for a caller inspecting the
761    /// materialized-but-erroring result) without a second, hand-rolled
762    /// tree-copy routine.
763    pub fn to_raw(&self) -> RawNode {
764        self.doc.raw_at(self.id)
765    }
766}
767
768/// The *raw* canonical Document node: either a leaf scalar, or an ordered
769/// list of `(label, child)` edges that may repeat **and interleave** a label
770/// arbitrarily (`[("b",1),("c",2),("b",3)]` is representable exactly).
771///
772/// This is distinct from [`Value`]: `Value::Object`'s `IndexMap` can't hold
773/// a repeated key, so its "repeated label" convention (a `Value::Array`
774/// under one key, per `child_specs`) only ever expands to a *contiguous*
775/// run of same-label edges. OML is the one format that must round-trip
776/// arbitrary interleaving losslessly (per its own docs: "no adjustment ever
777/// needed"), so its reader/writer (`crate::oml`) builds/walks a `Doc`
778/// through this type instead of going through `Value`.
779#[derive(Debug, Clone, PartialEq)]
780pub enum RawNode {
781    /// A leaf node holding a scalar value.
782    Leaf(Scalar),
783    /// An internal node holding labeled outgoing edges.
784    Edges(Vec<(String, RawNode)>),
785}
786
787impl Doc {
788    /// Build a `Doc` from a [`RawNode`], preserving edge order and
789    /// interleaving exactly. Depth-guarded via the same
790    /// `check_write_depth` every other construction path uses.
791    pub fn from_raw(root: RawNode) -> Result<Doc, DocumentError> {
792        Doc::from_raw_resolved(root, Resolved::DEFAULT)
793    }
794
795    /// [`Doc::from_raw`] under explicit [`Limits`]. Returns the
796    /// [`Limits::validate`] error unchanged when the limits are out of range;
797    /// otherwise a limit crossed is `document.limit.depth` /
798    /// `document.limit.nodes` at `$` or `document.limit.int-digits` at the
799    /// path of the over-long integer (E-11). The document remembers the
800    /// limits.
801    pub fn from_raw_with(root: RawNode, limits: &Limits) -> Result<Doc, DocumentError> {
802        Doc::from_raw_resolved(root, limits.resolve()?)
803    }
804
805    pub(crate) fn from_raw_resolved(root: RawNode, limits: Resolved) -> Result<Doc, DocumentError> {
806        let mut arena = Arena::new(limits);
807        let root_id = push_raw(&mut arena, root, 0)?;
808        Ok(Doc {
809            arena,
810            root: root_id,
811        })
812    }
813
814    /// The inverse of [`Doc::from_raw`]: a lossless walk back into
815    /// [`RawNode`] form, preserving edge order and interleaving exactly.
816    pub fn to_raw(&self) -> RawNode {
817        self.raw_at(self.root)
818    }
819
820    fn raw_at(&self, id: NodeId) -> RawNode {
821        let entry = self.entry(id);
822        match &entry.data {
823            NodeData::Leaf(s) => RawNode::Leaf(s.clone()),
824            NodeData::Internal(edges) => RawNode::Edges(
825                edges
826                    .iter()
827                    .map(|(label, child)| (label.clone(), self.raw_at(*child)))
828                    .collect(),
829            ),
830        }
831    }
832}
833
834/// Name-keyed dispatch through [`crate::registry`] (issue #31), mirroring
835/// Python's `Doc.from_format`/`to_format`/`check_format` in
836/// `~/dev/omnist/omnist/document.py`. Kept as its own `impl Doc` block
837/// (rather than folded into the constructors/export blocks above) since it
838/// is the one place `Doc` depends on the registry module rather than a
839/// single format module directly.
840impl Doc {
841    /// Read `text` as the registered format `name` (an
842    /// [`crate::error::OmnistError::Format`] if `name` isn't registered).
843    /// Mirrors Python's `Doc.from_format(name, text)`.
844    pub fn from_format(name: &str, text: &str) -> Result<Doc, crate::error::OmnistError> {
845        let fmt = crate::registry::get_format(name)?;
846        // D-23: the default maximum bounds a registered (plugin) format too,
847        // whose reader is not ours to change; the built-in readers repeat
848        // the same first check themselves.
849        crate::limits::Resolved::DEFAULT.check_input_size(text.len())?;
850        (fmt.read)(text)
851    }
852
853    /// Write `self` as the registered format `name`. Mirrors Python's
854    /// `Doc.to_format(name)`.
855    pub fn to_format(&self, name: &str) -> Result<String, crate::error::OmnistError> {
856        let fmt = crate::registry::get_format(name)?;
857        (fmt.write)(self)
858    }
859
860    /// Simulate writing `self` as the registered format `name`, without
861    /// producing output. Mirrors Python's `Doc.check_format(name)`: an
862    /// [`crate::error::OmnistError::Document`] if `name`'s registered
863    /// [`crate::registry::Format`] has no `check` callable (a plugin
864    /// registered via [`crate::registry::Format::new`] alone) -- not a
865    /// panic.
866    pub fn check_format(
867        &self,
868        name: &str,
869    ) -> Result<crate::report::WriteReport, crate::error::OmnistError> {
870        let fmt = crate::registry::get_format(name)?;
871        match &fmt.check {
872            Some(check) => Ok(check(self)),
873            None => Err(DocumentError::new(
874                "$",
875                format!("format {name:?} has no check() -- cannot simulate a write"),
876            )
877            .into()),
878        }
879    }
880}
881
882fn push_raw(arena: &mut Arena, node: RawNode, depth: usize) -> Result<NodeId, DocumentError> {
883    // Depth and node count are properties of the whole document and carry
884    // path `$` (E-11, the `document-model/limits` vectors); an over-long
885    // integer carries its own path, spliced in as the error unwinds.
886    if depth > arena.limits.max_depth {
887        return Err(arena.limits.depth_error());
888    }
889    match node {
890        RawNode::Leaf(s) => {
891            if let Scalar::Int(i) = &s
892                && int_exceeds(i, arena.limits.max_int_digits)
893            {
894                return Err(arena.limits.int_digits_error("$"));
895            }
896            push(arena, NodeData::Leaf(s), depth, "$")
897        }
898        RawNode::Edges(edges) => {
899            let mut out: Vec<(String, NodeId)> = Vec::with_capacity(edges.len());
900            let mut rest = edges.into_iter();
901            while let Some((label, child)) = rest.next() {
902                match push_raw(arena, child, depth + 1) {
903                    Ok(cid) => out.push((label, cid)),
904                    Err(e) => {
905                        return Err(splice_edge_path(e, &label, &out, rest.map(|(l, _)| l)));
906                    }
907                }
908            }
909            push(arena, NodeData::Internal(out), depth, "$")
910        }
911    }
912}
913
914/// A node's edge for a path-buffer walker: label, E-10 index, child.
915pub(crate) type RawEdge<'a> = (&'a str, Option<usize>, NodeId);
916
917/// Pairs every edge of a node with its E-10 index: `Some(i)`, the 0-based
918/// position among the node's edges of the same label, when the label occurs
919/// more than once in the node, `None` when it occurs once. One counting pass
920/// plus one assigning pass, one small map per node.
921fn with_occurrence_indices(
922    edges: &[(String, NodeId)],
923) -> impl Iterator<Item = (&str, Option<usize>, NodeId)> {
924    let mut counts: IndexMap<&str, (usize, usize)> = IndexMap::new();
925    for (label, _) in edges {
926        counts.entry(label.as_str()).or_insert((0, 0)).0 += 1;
927    }
928    edges.iter().map(move |(label, child)| {
929        let (total, seen) = counts
930            .get_mut(label.as_str())
931            .expect("every label was counted");
932        let index = crate::report::occurrence_index(*seen, *total);
933        *seen += 1;
934        (label.as_str(), index, *child)
935    })
936}
937
938/// Prefixes the path of an over-long-integer error with the edge it
939/// unwinds through: `.label`, plus `[i]` when the label occurs more than
940/// once in the node (E-10). `done` are the edges already built, `after` the
941/// labels still to come; together with `label` they are the whole node.
942/// Other limit errors stay at `$`.
943fn splice_edge_path(
944    mut e: DocumentError,
945    label: &str,
946    done: &[(String, NodeId)],
947    after: impl Iterator<Item = String>,
948) -> DocumentError {
949    if e.code.as_deref() != Some("document.limit.int-digits") {
950        return e;
951    }
952    let before = done.iter().filter(|(l, _)| l == label).count();
953    let later = after.filter(|l| l == label).count();
954    let index = if before + later > 0 {
955        format!("[{before}]")
956    } else {
957        String::new()
958    };
959    e.path = format!("${}{index}{}", join("", label), &e.path[1..]);
960    e
961}
962
963#[cfg(test)]
964mod tests {
965    use super::*;
966
967    fn obj(pairs: &[(&str, Value)]) -> Value {
968        let mut m = IndexMap::new();
969        for (k, v) in pairs {
970            m.insert((*k).to_string(), v.clone());
971        }
972        Value::Object(m)
973    }
974
975    /// `levels` nested objects (each `{"a": ...}`) wrapping a leaf. Per
976    /// `build_node`'s depth arithmetic, the leaf ends up at depth `levels`
977    /// (each object level adds exactly 1 -- no array involved).
978    fn nest(levels: usize) -> Value {
979        let mut v = Value::Int((0).into());
980        for _ in 0..levels {
981            v = obj(&[("a", v)]);
982        }
983        v
984    }
985
986    // -- basic construction ---------------------------------------------
987
988    #[test]
989    fn constructs_a_scalar_leaf() {
990        let doc = Doc::of(&Value::Int((42).into())).unwrap();
991        let root = doc.root();
992        assert!(root.is_leaf());
993        assert_eq!(root.value().unwrap(), &Scalar::Int((42).into()));
994    }
995
996    #[test]
997    fn constructs_an_object_as_ordered_edges() {
998        let v = obj(&[("b", Value::Int((1).into())), ("a", Value::Int((2).into()))]);
999        let doc = Doc::of(&v).unwrap();
1000        let root = doc.root();
1001        assert!(!root.is_leaf());
1002        let edges = root.edges().unwrap();
1003        let labels: Vec<&str> = edges.iter().map(|(l, _)| l.as_str()).collect();
1004        // Insertion order preserved, NOT sorted -- "b" before "a".
1005        assert_eq!(labels, vec!["b", "a"]);
1006    }
1007
1008    #[test]
1009    fn a_list_value_expands_into_repeated_edges() {
1010        let v = obj(&[(
1011            "member",
1012            Value::Array(vec![
1013                Value::Int((1).into()),
1014                Value::Int((2).into()),
1015                Value::Int((3).into()),
1016            ]),
1017        )]);
1018        let doc = Doc::of(&v).unwrap();
1019        let root = doc.root();
1020        assert_eq!(root.count("member"), 3);
1021        let members = root.get("member");
1022        let vals: Vec<&Scalar> = members.iter().map(|c| c.value().unwrap()).collect();
1023        assert_eq!(
1024            vals,
1025            vec![
1026                &Scalar::Int((1).into()),
1027                &Scalar::Int((2).into()),
1028                &Scalar::Int((3).into())
1029            ]
1030        );
1031    }
1032
1033    #[test]
1034    fn a_bare_top_level_array_is_rejected() {
1035        let err = Doc::of(&Value::Array(vec![Value::Int((1).into())])).unwrap_err();
1036        assert!(err.message.contains("bare array"));
1037        assert_eq!(err.path, "$");
1038    }
1039
1040    #[test]
1041    fn an_array_of_arrays_is_rejected() {
1042        let v = obj(&[(
1043            "a",
1044            Value::Array(vec![Value::Array(vec![Value::Int((1).into())])]),
1045        )]);
1046        let err = Doc::of(&v).unwrap_err();
1047        assert!(err.message.contains("array of arrays"));
1048        // E-10: the label occurs once, so no index.
1049        assert_eq!(err.path, "$.a");
1050        let two = obj(&[(
1051            "a",
1052            Value::Array(vec![
1053                Value::Array(vec![Value::Int((1).into())]),
1054                Value::Array(vec![Value::Int((2).into())]),
1055            ]),
1056        )]);
1057        assert_eq!(Doc::of(&two).unwrap_err().path, "$.a[0]");
1058    }
1059
1060    // -- depth guard: max-depth boundary ---------------------------------
1061
1062    #[test]
1063    fn depth_guard_accepts_exactly_max_depth() {
1064        // nest(MAX_DEPTH) puts the leaf at depth == MAX_DEPTH, which is
1065        // the accept boundary (guard rejects only depth > MAX_DEPTH).
1066        let v = nest(MAX_DEPTH);
1067        assert!(Doc::of(&v).is_ok());
1068    }
1069
1070    #[test]
1071    fn depth_guard_rejects_one_past_max_depth() {
1072        let v = nest(MAX_DEPTH + 1);
1073        let err = Doc::of(&v).unwrap_err();
1074        assert!(err.message.contains("maximum depth"));
1075    }
1076
1077    // -- node-count guard: max-nodes boundary (omnist-rs#78) -------------
1078    //
1079    // A shallow document can still be enormous -- depth alone doesn't bound
1080    // total memory, e.g. a single label repeated a million times is depth 1.
1081    // `wide(n)` builds `{"a": [{}, {}, ..., {}]}` with `n` empty objects, for
1082    // a total node count of `n + 1` (the root object, plus one container per
1083    // array element -- the array itself desugars into repeated edges, not
1084    // its own node; scalar leaves are not nodes, D-9).
1085
1086    fn wide(n: usize) -> Value {
1087        obj(&[(
1088            "a",
1089            Value::Array(vec![Value::Object(Default::default()); n]),
1090        )])
1091    }
1092
1093    #[test]
1094    fn node_guard_accepts_exactly_max_nodes() {
1095        let v = wide(MAX_NODES - 1);
1096        assert!(Doc::of(&v).is_ok());
1097    }
1098
1099    #[test]
1100    fn node_guard_rejects_one_past_max_nodes() {
1101        let v = wide(MAX_NODES);
1102        let err = Doc::of(&v).unwrap_err();
1103        assert!(err.message.contains("maximum node count"));
1104    }
1105
1106    #[test]
1107    fn an_array_value_consumes_an_extra_depth_level() {
1108        // A scalar directly under a key sits one level deeper than the
1109        // object (depth 1). The same scalar wrapped in a one-item array
1110        // under that key sits *two* levels deeper (depth 2) -- the array
1111        // itself costs an extra level, mirroring Python's `_children`
1112        // (see the module doc comment / build_node's `depth + 1` for the
1113        // list branch on top of the caller's own `depth + 1`).
1114        let direct = obj(&[("a", Value::Int((1).into()))]);
1115        let via_array = obj(&[("a", Value::Array(vec![Value::Int((1).into())]))]);
1116        let doc_direct = Doc::of(&direct).unwrap();
1117        let doc_array = Doc::of(&via_array).unwrap();
1118        let leaf_direct = doc_direct.root().child("a").unwrap();
1119        let leaf_array = doc_array.root().child("a").unwrap();
1120        assert_eq!(doc_direct.entry(leaf_direct.id()).depth, 1);
1121        assert_eq!(doc_array.entry(leaf_array.id()).depth, 2);
1122    }
1123
1124    // -- depth guard: every public tree-mutating entry point -------------
1125
1126    #[test]
1127    fn every_tree_mutating_entry_point_enforces_the_depth_guard() {
1128        // Doc::of
1129        assert!(Doc::of(&nest(MAX_DEPTH + 1)).is_err());
1130
1131        // Doc::add, attaching at the root (depth 0): the pushed subtree's
1132        // own internal depth is what must exceed MAX_DEPTH.
1133        let mut doc = Doc::of(&obj(&[("seed", Value::Int((0).into()))])).unwrap();
1134        let root_id = doc.root().id();
1135        let root_path = doc.root().path.clone();
1136        assert!(
1137            doc.add(root_id, &root_path, "b", &nest(MAX_DEPTH + 1))
1138                .is_err()
1139        );
1140
1141        // Doc::set: same guard, different mutating call site.
1142        let mut doc2 = Doc::of(&obj(&[("seed", Value::Int((0).into()))])).unwrap();
1143        let root_id2 = doc2.root().id();
1144        let root_path2 = doc2.root().path.clone();
1145        assert!(
1146            doc2.set(root_id2, &root_path2, "b", &nest(MAX_DEPTH + 1))
1147                .is_err()
1148        );
1149    }
1150
1151    // -- depth guard: hand-constructed deep subtree bypassing a
1152    //    construction-time-only check (omnist-ts#37 / omnist-ts#70) -------
1153
1154    #[test]
1155    fn add_at_a_deep_cursor_accounts_for_the_cursors_own_depth() {
1156        // Build a document 200 objects deep (leaf at depth 200), then walk
1157        // a cursor down 190 levels to an *internal* node sitting at depth
1158        // 190. If `add()` (re-)started the depth guard's counter at 0 for
1159        // the pushed subtree -- the exact omnist-ts#37 bug, where
1160        // `buildNode` was called with no depth argument on every mutation
1161        // -- a modest 15-level subtree would wrongly be accepted here
1162        // (15 < MAX_DEPTH). Threading the cursor's own depth through
1163        // means 190 + 1 (attach) + 15 = 206 > MAX_DEPTH is correctly
1164        // rejected instead.
1165        let mut doc = Doc::of(&nest(MAX_DEPTH)).unwrap();
1166        let mut cursor = doc.root();
1167        for _ in 0..190 {
1168            cursor = cursor.child("a").unwrap();
1169        }
1170        assert_eq!(doc.entry(cursor.id()).depth, 190);
1171        let id = cursor.id();
1172        let path = cursor.path.clone();
1173
1174        let too_deep = nest(15);
1175        assert!(doc.add(id, &path, "b", &too_deep).is_err());
1176
1177        // Positive control: a shallow enough subtree at the same depth
1178        // succeeds, proving the rejection above is about depth, not some
1179        // unrelated failure.
1180        let shallow = nest(5);
1181        assert!(doc.set(id, &path, "b", &shallow).is_ok());
1182    }
1183
1184    // -- IndexMap ordering preserved on iteration ------------------------
1185
1186    #[test]
1187    fn labels_and_get_preserve_first_seen_and_insertion_order() {
1188        // Repeated labels come from an `Array` value under one key (a dict
1189        // key can't repeat) -- "z" appears twice because its value is a
1190        // 2-item array, not because the object has two "z" entries.
1191        let v = obj(&[
1192            (
1193                "z",
1194                Value::Array(vec![Value::Int((1).into()), Value::Int((3).into())]),
1195            ),
1196            ("a", Value::Int((2).into())),
1197            ("m", Value::Int((4).into())),
1198        ]);
1199        let doc = Doc::of(&v).unwrap();
1200        let root = doc.root();
1201        assert_eq!(root.labels(), vec!["z", "a", "m"]);
1202        let z_vals: Vec<&Scalar> = root.get("z").iter().map(|c| c.value().unwrap()).collect();
1203        assert_eq!(
1204            z_vals,
1205            vec![&Scalar::Int((1).into()), &Scalar::Int((3).into())]
1206        );
1207    }
1208
1209    #[test]
1210    fn labels_and_count_on_a_leaf_are_empty() {
1211        let doc = Doc::of(&Value::Int((1).into())).unwrap();
1212        let root = doc.root();
1213        assert!(root.labels().is_empty());
1214        assert_eq!(root.count("anything"), 0);
1215    }
1216
1217    #[test]
1218    fn to_grouped_preserves_first_seen_key_order() {
1219        let v = obj(&[
1220            (
1221                "z",
1222                Value::Array(vec![Value::Int((1).into()), Value::Int((3).into())]),
1223            ),
1224            ("a", Value::Int((2).into())),
1225        ]);
1226        let doc = Doc::of(&v).unwrap();
1227        let grouped = doc.to_grouped();
1228        let expected = obj(&[
1229            (
1230                "z",
1231                Value::Array(vec![Value::Int((1).into()), Value::Int((3).into())]),
1232            ),
1233            ("a", Value::Int((2).into())),
1234        ]);
1235        assert_eq!(grouped, expected);
1236        // `assert_eq!` above is order-insensitive (`IndexMap`'s `PartialEq`
1237        // ignores insertion order), so separately confirm key order here.
1238        let keys: Vec<&str> = grouped
1239            .as_object()
1240            .unwrap()
1241            .keys()
1242            .map(|s| s.as_str())
1243            .collect();
1244        assert_eq!(keys, vec!["z", "a"]);
1245    }
1246
1247    #[test]
1248    fn value_as_object_is_none_for_a_non_object() {
1249        assert!(Value::Int((1).into()).as_object().is_none());
1250    }
1251
1252    // -- mutation semantics: add / set / remove / count / get -----------
1253
1254    #[test]
1255    fn add_appends_and_get_one_requires_exactly_one() {
1256        let mut doc = Doc::of(&obj(&[])).unwrap();
1257        let root_id = doc.root().id();
1258        let root_path = doc.root().path.clone();
1259        doc.add(root_id, &root_path, "x", &Value::Int((1).into()))
1260            .unwrap();
1261        doc.add(root_id, &root_path, "x", &Value::Int((2).into()))
1262            .unwrap();
1263        let root = doc.root();
1264        assert_eq!(root.count("x"), 2);
1265        assert!(root.get_one("x").is_err());
1266    }
1267
1268    #[test]
1269    fn set_replaces_all_occurrences_at_first_position() {
1270        let mut doc = Doc::of(&obj(&[
1271            ("x", Value::Int((1).into())),
1272            ("y", Value::Int((9).into())),
1273            ("x", Value::Int((2).into())),
1274        ]))
1275        .unwrap();
1276        let root_id = doc.root().id();
1277        let root_path = doc.root().path.clone();
1278        doc.set(root_id, &root_path, "x", &Value::Int((100).into()))
1279            .unwrap();
1280        let root = doc.root();
1281        let labels: Vec<String> = root.edges().unwrap().into_iter().map(|(l, _)| l).collect();
1282        assert_eq!(labels, vec!["x", "y"]);
1283        assert_eq!(
1284            root.get_one("x").unwrap().value().unwrap(),
1285            &Scalar::Int((100).into())
1286        );
1287    }
1288
1289    #[test]
1290    fn remove_drops_every_edge_with_that_label() {
1291        let mut doc = Doc::of(&obj(&[
1292            ("x", Value::Int((1).into())),
1293            ("x", Value::Int((2).into())),
1294        ]))
1295        .unwrap();
1296        let root_id = doc.root().id();
1297        let root_path = doc.root().path.clone();
1298        doc.remove(root_id, &root_path, "x").unwrap();
1299        assert_eq!(doc.root().count("x"), 0);
1300    }
1301
1302    #[test]
1303    fn internal_edges_mut_rejects_a_leaf_directly() {
1304        // White-box test of the private helper: `add`/`set`/`remove` all
1305        // gate through `require_internal` first, so this helper's `Leaf`
1306        // arm is unreachable via the public API -- call it directly to
1307        // prove the arm itself is correct (see its doc comment).
1308        let mut doc = Doc::of(&Value::Int((1).into())).unwrap();
1309        let root_id = doc.root().id();
1310        let err = doc.internal_edges_mut(root_id, "$", "poke").unwrap_err();
1311        assert_eq!(err.path, "$");
1312        assert!(err.message.contains("cannot poke on a leaf"));
1313    }
1314
1315    #[test]
1316    fn mutation_on_a_leaf_is_rejected() {
1317        let mut doc = Doc::of(&Value::Int((1).into())).unwrap();
1318        let root_id = doc.root().id();
1319        let root_path = doc.root().path.clone();
1320        assert!(
1321            doc.add(root_id, &root_path, "x", &Value::Int((1).into()))
1322                .is_err()
1323        );
1324        assert!(
1325            doc.set(root_id, &root_path, "x", &Value::Int((1).into()))
1326                .is_err()
1327        );
1328        assert!(doc.remove(root_id, &root_path, "x").is_err());
1329    }
1330
1331    #[test]
1332    fn value_on_an_internal_node_is_rejected() {
1333        let doc = Doc::of(&obj(&[("x", Value::Int((1).into()))])).unwrap();
1334        assert!(doc.root().value().is_err());
1335    }
1336
1337    #[test]
1338    fn edges_on_a_leaf_is_rejected() {
1339        let doc = Doc::of(&Value::Int((1).into())).unwrap();
1340        assert!(doc.root().edges().is_err());
1341    }
1342
1343    #[test]
1344    fn raw_edges_on_a_leaf_is_rejected() {
1345        // Mirrors `edges_on_a_leaf_is_rejected` for the lazy-path variant
1346        // `edges()` piggybacks its own path-numbering on -- see issue #44.
1347        let doc = Doc::of(&Value::Int((1).into())).unwrap();
1348        assert!(doc.root().raw_edges().is_err());
1349    }
1350
1351    // -- export / equality ------------------------------------------------
1352
1353    #[test]
1354    fn to_data_round_trips_structure() {
1355        let v = obj(&[
1356            ("a", Value::Int((1).into())),
1357            ("b", Value::Str("hi".to_string())),
1358        ]);
1359        let doc = Doc::of(&v).unwrap();
1360        assert_eq!(doc.to_data(), v);
1361    }
1362
1363    #[test]
1364    fn to_data_round_trips_every_scalar_variant() {
1365        // Covers every `Value`/`Scalar` leaf variant through both
1366        // `build_node` (construction) and `From<Scalar> for Value`
1367        // (export via to_data), not just Int/Str.
1368        let v = obj(&[
1369            ("n", Value::Null),
1370            ("b", Value::Bool(true)),
1371            ("i", Value::Int((7).into())),
1372            ("f", Value::Float(1.5)),
1373            ("s", Value::Str("hi".to_string())),
1374        ]);
1375        let doc = Doc::of(&v).unwrap();
1376        assert_eq!(doc.to_data(), v);
1377        assert_eq!(doc.to_grouped(), v);
1378    }
1379
1380    #[test]
1381    fn join_quotes_a_non_identifier_key() {
1382        // A key that isn't a valid identifier (starts with a digit) takes
1383        // the `path["key"]` form, not `path.key`.
1384        let v = obj(&[(
1385            "1bad",
1386            Value::Array(vec![
1387                Value::Array(vec![Value::Int((1).into())]),
1388                Value::Array(vec![Value::Int((2).into())]),
1389            ]),
1390        )]);
1391        let err = Doc::of(&v).unwrap_err();
1392        // E-36: a label that needs no quoting is bare, whatever its spelling.
1393        assert_eq!(err.path, "$.1bad[0]");
1394    }
1395
1396    #[test]
1397    fn eq_doc_compares_structurally() {
1398        let a = Doc::of(&obj(&[("a", Value::Int((1).into()))])).unwrap();
1399        let b = Doc::of(&obj(&[("a", Value::Int((1).into()))])).unwrap();
1400        let c = Doc::of(&obj(&[("a", Value::Int((2).into()))])).unwrap();
1401        assert!(a.eq_doc(&b));
1402        assert!(!a.eq_doc(&c));
1403    }
1404
1405    #[test]
1406    fn eq_doc_is_false_when_shapes_differ() {
1407        // A leaf is never equal to an internal node, regardless of
1408        // content -- exercises `node_eq`'s (Leaf, Internal) mismatch arm.
1409        let leaf = Doc::of(&Value::Int((1).into())).unwrap();
1410        let internal = Doc::of(&obj(&[("a", Value::Int((1).into()))])).unwrap();
1411        assert!(!leaf.eq_doc(&internal));
1412        assert!(!internal.eq_doc(&leaf));
1413    }
1414
1415    #[test]
1416    fn scalar_display_covers_every_variant() {
1417        assert_eq!(Scalar::Null.to_string(), "null");
1418        assert_eq!(Scalar::Bool(true).to_string(), "true");
1419        assert_eq!(Scalar::Int((1).into()).to_string(), "1");
1420        assert_eq!(Scalar::Float(1.5).to_string(), "1.5");
1421        assert_eq!(Scalar::Str("x".to_string()).to_string(), "\"x\"");
1422    }
1423
1424    // -- RawNode / from_raw / to_raw (interleaved-edge round trip) ---------
1425
1426    #[test]
1427    fn from_raw_to_raw_round_trips_interleaved_repeated_labels() {
1428        // ("b",1),("c",2),("b",3): interleaved, not a contiguous run --
1429        // exactly the shape `Value`/`IndexMap` cannot represent, which is
1430        // why the OML codec goes through RawNode instead.
1431        let raw = RawNode::Edges(vec![
1432            ("b".to_string(), RawNode::Leaf(Scalar::Int((1).into()))),
1433            ("c".to_string(), RawNode::Leaf(Scalar::Int((2).into()))),
1434            ("b".to_string(), RawNode::Leaf(Scalar::Int((3).into()))),
1435        ]);
1436        let doc = Doc::from_raw(raw.clone()).unwrap();
1437        assert_eq!(doc.to_raw(), raw);
1438        let labels: Vec<String> = doc
1439            .root()
1440            .edges()
1441            .unwrap()
1442            .into_iter()
1443            .map(|(l, _)| l)
1444            .collect();
1445        assert_eq!(labels, vec!["b", "c", "b"]);
1446    }
1447
1448    #[test]
1449    fn from_raw_leaf_round_trips() {
1450        let raw = RawNode::Leaf(Scalar::Str("hi".to_string()));
1451        let doc = Doc::from_raw(raw.clone()).unwrap();
1452        assert!(doc.root().is_leaf());
1453        assert_eq!(doc.to_raw(), raw);
1454    }
1455
1456    #[test]
1457    fn from_raw_with_names_a_repeated_label_in_an_int_digits_path() {
1458        let leaf = |i: i64| RawNode::Leaf(Scalar::Int(i.into()));
1459        let limits = Limits::default().with_max_int_digits(3);
1460        let raw = RawNode::Edges(vec![("n".into(), leaf(1)), ("n".into(), leaf(1000))]);
1461        let e = Doc::from_raw_with(raw, &limits).unwrap_err();
1462        assert_eq!(e.path, "$.n[1]");
1463        let raw = RawNode::Edges(vec![("n".into(), leaf(1000)), ("n".into(), leaf(1))]);
1464        assert_eq!(Doc::from_raw_with(raw, &limits).unwrap_err().path, "$.n[0]");
1465        let raw = RawNode::Edges(vec![("n".into(), leaf(1000))]);
1466        assert_eq!(Doc::from_raw_with(raw, &limits).unwrap_err().path, "$.n");
1467    }
1468
1469    #[test]
1470    fn from_raw_enforces_the_depth_guard() {
1471        fn nest_raw(levels: usize) -> RawNode {
1472            let mut n = RawNode::Leaf(Scalar::Int((0).into()));
1473            for _ in 0..levels {
1474                n = RawNode::Edges(vec![("a".to_string(), n)]);
1475            }
1476            n
1477        }
1478        assert!(Doc::from_raw(nest_raw(MAX_DEPTH)).is_ok());
1479        let err = Doc::from_raw(nest_raw(MAX_DEPTH + 1)).unwrap_err();
1480        assert!(err.message.contains("maximum depth"));
1481    }
1482}