Skip to main content

fionn_core/
tape_source.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Format-agnostic tape traversal abstraction
3//!
4//! This module provides the [`TapeSource`] trait for format-agnostic tape traversal,
5//! enabling gron, diff, and other operations to work with any parsed format
6//! (JSON, YAML, TOML, CSV, ISON, TOON).
7//!
8//! # Key Types
9//!
10//! - [`TapeSource`] - Trait for format-agnostic tape traversal
11//! - [`TapeValue`] - Common value representation across all formats
12//! - [`TapeNodeKind`] - Node type classification
13//! - [`TapeNodeRef`] - Reference to a tape node with metadata
14//!
15//! # Example
16//!
17//! ```ignore
18//! use fionn_core::tape_source::{TapeSource, TapeNodeKind};
19//!
20//! fn process_tape<T: TapeSource>(tape: &T) {
21//!     for i in 0..tape.len() {
22//!         if let Some(node) = tape.node_at(i) {
23//!             match node.kind {
24//!                 TapeNodeKind::ObjectStart { count } => println!("Object with {} fields", count),
25//!                 TapeNodeKind::Value => println!("Value: {:?}", node.value),
26//!                 _ => {}
27//!             }
28//!         }
29//!     }
30//! }
31//! ```
32
33use crate::Result;
34use crate::format::FormatKind;
35use std::borrow::Cow;
36use std::fmt;
37
38// ============================================================================
39// TapeValue - Common value representation
40// ============================================================================
41
42/// Common value representation for all tape formats
43///
44/// This enum provides a unified way to represent scalar and raw values
45/// across different data formats. It uses `Cow<'a, str>` for strings
46/// to support both borrowed and owned data.
47#[derive(Debug, Clone, PartialEq)]
48pub enum TapeValue<'a> {
49    /// Null/nil/none value
50    Null,
51    /// Boolean value
52    Bool(bool),
53    /// 64-bit signed integer
54    Int(i64),
55    /// 64-bit floating point
56    Float(f64),
57    /// String value (borrowed or owned)
58    String(Cow<'a, str>),
59    /// Raw number string (preserves original representation)
60    RawNumber(Cow<'a, str>),
61}
62
63impl TapeValue<'_> {
64    /// Check if this value is null
65    #[must_use]
66    pub const fn is_null(&self) -> bool {
67        matches!(self, Self::Null)
68    }
69
70    /// Check if this value is a boolean
71    #[must_use]
72    pub const fn is_bool(&self) -> bool {
73        matches!(self, Self::Bool(_))
74    }
75
76    /// Check if this value is numeric (int, float, or raw number)
77    #[must_use]
78    pub const fn is_number(&self) -> bool {
79        matches!(self, Self::Int(_) | Self::Float(_) | Self::RawNumber(_))
80    }
81
82    /// Check if this value is a string
83    #[must_use]
84    pub const fn is_string(&self) -> bool {
85        matches!(self, Self::String(_))
86    }
87
88    /// Get as boolean if this is a Bool
89    #[must_use]
90    pub const fn as_bool(&self) -> Option<bool> {
91        match self {
92            Self::Bool(b) => Some(*b),
93            _ => None,
94        }
95    }
96
97    /// Get as i64 if this is an Int
98    #[must_use]
99    pub const fn as_int(&self) -> Option<i64> {
100        match self {
101            Self::Int(n) => Some(*n),
102            _ => None,
103        }
104    }
105
106    /// Get as f64 if this is a Float
107    #[must_use]
108    pub const fn as_float(&self) -> Option<f64> {
109        match self {
110            Self::Float(n) => Some(*n),
111            _ => None,
112        }
113    }
114
115    /// Get as string reference if this is a String
116    #[must_use]
117    pub fn as_str(&self) -> Option<&str> {
118        match self {
119            Self::String(s) => Some(s),
120            _ => None,
121        }
122    }
123
124    /// Convert to owned version (static lifetime)
125    #[must_use]
126    pub fn into_owned(self) -> TapeValue<'static> {
127        match self {
128            Self::Null => TapeValue::Null,
129            Self::Bool(b) => TapeValue::Bool(b),
130            Self::Int(n) => TapeValue::Int(n),
131            Self::Float(n) => TapeValue::Float(n),
132            Self::String(s) => TapeValue::String(Cow::Owned(s.into_owned())),
133            Self::RawNumber(s) => TapeValue::RawNumber(Cow::Owned(s.into_owned())),
134        }
135    }
136
137    /// Format value for display (gron-style output)
138    #[must_use]
139    pub fn format_for_output(&self) -> String {
140        match self {
141            Self::Null => "null".to_string(),
142            Self::Bool(true) => "true".to_string(),
143            Self::Bool(false) => "false".to_string(),
144            Self::Int(n) => n.to_string(),
145            Self::Float(n) => {
146                // JSON doesn't support NaN or Infinity, map to null
147                if n.is_nan() || n.is_infinite() {
148                    "null".to_string()
149                } else {
150                    n.to_string()
151                }
152            }
153            Self::String(s) => format!("\"{}\"", escape_json_string(s)),
154            Self::RawNumber(s) => s.to_string(),
155        }
156    }
157}
158
159impl fmt::Display for TapeValue<'_> {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        write!(f, "{}", self.format_for_output())
162    }
163}
164
165// ============================================================================
166// TapeNodeKind - Node type classification
167// ============================================================================
168
169/// Node type classification for tape traversal
170///
171/// Provides a uniform way to identify node types across all tape formats.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173pub enum TapeNodeKind {
174    /// Start of an object/map with field count
175    ObjectStart {
176        /// Number of key-value pairs in the object
177        count: usize,
178    },
179    /// End of an object/map
180    ObjectEnd,
181    /// Start of an array/sequence with element count
182    ArrayStart {
183        /// Number of elements in the array
184        count: usize,
185    },
186    /// End of an array/sequence
187    ArrayEnd,
188    /// Key in a key-value pair
189    Key,
190    /// Scalar value
191    Value,
192}
193
194impl TapeNodeKind {
195    /// Check if this is a container start node
196    #[must_use]
197    pub const fn is_container_start(&self) -> bool {
198        matches!(self, Self::ObjectStart { .. } | Self::ArrayStart { .. })
199    }
200
201    /// Check if this is a container end node
202    #[must_use]
203    pub const fn is_container_end(&self) -> bool {
204        matches!(self, Self::ObjectEnd | Self::ArrayEnd)
205    }
206
207    /// Check if this is an object-related node
208    #[must_use]
209    pub const fn is_object_related(&self) -> bool {
210        matches!(self, Self::ObjectStart { .. } | Self::ObjectEnd | Self::Key)
211    }
212
213    /// Check if this is an array-related node
214    #[must_use]
215    pub const fn is_array_related(&self) -> bool {
216        matches!(self, Self::ArrayStart { .. } | Self::ArrayEnd)
217    }
218
219    /// Get the element count for container nodes
220    #[must_use]
221    pub const fn element_count(&self) -> Option<usize> {
222        match self {
223            Self::ObjectStart { count } | Self::ArrayStart { count } => Some(*count),
224            _ => None,
225        }
226    }
227}
228
229// ============================================================================
230// TapeNodeRef - Reference to a tape node
231// ============================================================================
232
233/// Reference to a tape node with metadata
234///
235/// Returned by [`TapeSource::node_at`] to provide information about a node
236/// without copying the underlying data.
237#[derive(Debug, Clone)]
238pub struct TapeNodeRef<'a> {
239    /// The kind of this node
240    pub kind: TapeNodeKind,
241    /// The value if this is a Value or Key node
242    pub value: Option<TapeValue<'a>>,
243    /// The source format of the tape
244    pub format: FormatKind,
245}
246
247impl<'a> TapeNodeRef<'a> {
248    /// Create a new node reference
249    #[must_use]
250    pub const fn new(kind: TapeNodeKind, value: Option<TapeValue<'a>>, format: FormatKind) -> Self {
251        Self {
252            kind,
253            value,
254            format,
255        }
256    }
257
258    /// Create an object start node
259    #[must_use]
260    pub const fn object_start(count: usize, format: FormatKind) -> Self {
261        Self {
262            kind: TapeNodeKind::ObjectStart { count },
263            value: None,
264            format,
265        }
266    }
267
268    /// Create an object end node
269    #[must_use]
270    pub const fn object_end(format: FormatKind) -> Self {
271        Self {
272            kind: TapeNodeKind::ObjectEnd,
273            value: None,
274            format,
275        }
276    }
277
278    /// Create an array start node
279    #[must_use]
280    pub const fn array_start(count: usize, format: FormatKind) -> Self {
281        Self {
282            kind: TapeNodeKind::ArrayStart { count },
283            value: None,
284            format,
285        }
286    }
287
288    /// Create an array end node
289    #[must_use]
290    pub const fn array_end(format: FormatKind) -> Self {
291        Self {
292            kind: TapeNodeKind::ArrayEnd,
293            value: None,
294            format,
295        }
296    }
297
298    /// Create a key node
299    #[must_use]
300    pub const fn key(name: Cow<'a, str>, format: FormatKind) -> Self {
301        Self {
302            kind: TapeNodeKind::Key,
303            value: Some(TapeValue::String(name)),
304            format,
305        }
306    }
307
308    /// Create a value node
309    #[must_use]
310    pub const fn value(val: TapeValue<'a>, format: FormatKind) -> Self {
311        Self {
312            kind: TapeNodeKind::Value,
313            value: Some(val),
314            format,
315        }
316    }
317}
318
319// ============================================================================
320// TapeSource Trait
321// ============================================================================
322
323/// Trait for format-agnostic tape traversal
324///
325/// This trait provides a common interface for traversing parsed data tapes
326/// from any format (JSON, YAML, TOML, CSV, ISON, TOON). It enables operations
327/// like gron, diff, and query to work uniformly across all formats.
328///
329/// # Implementation Notes
330///
331/// Implementors should provide efficient implementations for:
332/// - `node_at` - O(1) random access is ideal
333/// - `skip_value` - Skip nested structures efficiently
334/// - `resolve_path` - Path-based navigation
335///
336/// # Example Implementation
337///
338/// ```ignore
339/// impl TapeSource for MyTape {
340///     fn format(&self) -> FormatKind { FormatKind::Json }
341///     fn len(&self) -> usize { self.nodes.len() }
342///     fn node_at(&self, index: usize) -> Option<TapeNodeRef<'_>> {
343///         self.nodes.get(index).map(|n| convert_to_ref(n))
344///     }
345///     // ... other methods
346/// }
347/// ```
348pub trait TapeSource {
349    /// Get the source format of this tape
350    fn format(&self) -> FormatKind;
351
352    /// Get the total number of nodes in the tape
353    fn len(&self) -> usize;
354
355    /// Check if the tape is empty
356    fn is_empty(&self) -> bool {
357        self.len() == 0
358    }
359
360    /// Read a node at the given index
361    ///
362    /// Returns `None` if the index is out of bounds.
363    fn node_at(&self, index: usize) -> Option<TapeNodeRef<'_>>;
364
365    /// Get the value at index if it's a value node
366    ///
367    /// Convenience method that extracts just the value from a Value node.
368    fn value_at(&self, index: usize) -> Option<TapeValue<'_>> {
369        self.node_at(index).and_then(|n| {
370            if matches!(n.kind, TapeNodeKind::Value) {
371                n.value
372            } else {
373                None
374            }
375        })
376    }
377
378    /// Get the key string at index if it's a key node
379    ///
380    /// Convenience method that extracts the key name from a Key node.
381    fn key_at(&self, index: usize) -> Option<Cow<'_, str>> {
382        self.node_at(index).and_then(|n| {
383            if matches!(n.kind, TapeNodeKind::Key) {
384                n.value.and_then(|v| {
385                    if let TapeValue::String(s) = v {
386                        Some(s)
387                    } else {
388                        None
389                    }
390                })
391            } else {
392                None
393            }
394        })
395    }
396
397    /// Skip to the end of a value starting at the given index
398    ///
399    /// For scalar values, returns `start_index + 1`.
400    /// For containers, returns the index after the closing node.
401    ///
402    /// # Errors
403    ///
404    /// Returns an error if the index is invalid or the tape structure is malformed.
405    fn skip_value(&self, start_index: usize) -> Result<usize>;
406
407    /// Resolve a JSON-pointer-style path to a tape index
408    ///
409    /// Returns `Ok(Some(index))` if the path exists, `Ok(None)` if not found,
410    /// or an error if the path is invalid.
411    ///
412    /// # Errors
413    ///
414    /// Returns an error if the path syntax is invalid.
415    fn resolve_path(&self, path: &str) -> Result<Option<usize>>;
416
417    /// Create an iterator over all nodes
418    fn iter(&self) -> TapeIterator<'_, Self>
419    where
420        Self: Sized,
421    {
422        TapeIterator::new(self)
423    }
424
425    /// Get an iterator that yields (path, node) pairs
426    ///
427    /// Useful for gron-like operations that need path context.
428    fn path_iter(&self) -> PathIterator<'_, Self>
429    where
430        Self: Sized,
431    {
432        PathIterator::new(self)
433    }
434}
435
436// ============================================================================
437// Iterators
438// ============================================================================
439
440/// Iterator over tape nodes
441pub struct TapeIterator<'a, T: TapeSource> {
442    tape: &'a T,
443    index: usize,
444}
445
446impl<'a, T: TapeSource> TapeIterator<'a, T> {
447    /// Create a new iterator starting at index 0
448    #[must_use]
449    pub const fn new(tape: &'a T) -> Self {
450        Self { tape, index: 0 }
451    }
452}
453
454impl<'a, T: TapeSource> Iterator for TapeIterator<'a, T> {
455    type Item = TapeNodeRef<'a>;
456
457    fn next(&mut self) -> Option<Self::Item> {
458        if self.index >= self.tape.len() {
459            return None;
460        }
461        let node = self.tape.node_at(self.index);
462        self.index += 1;
463        node
464    }
465
466    fn size_hint(&self) -> (usize, Option<usize>) {
467        let remaining = self.tape.len().saturating_sub(self.index);
468        (remaining, Some(remaining))
469    }
470}
471
472impl<T: TapeSource> ExactSizeIterator for TapeIterator<'_, T> {}
473
474/// Path component for path iteration
475#[derive(Debug, Clone)]
476pub enum PathComponent {
477    /// Object field name
478    Field(String),
479    /// Array index
480    Index(usize),
481}
482
483impl PathComponent {
484    /// Format as path segment
485    #[must_use]
486    pub fn as_path_segment(&self) -> String {
487        match self {
488            Self::Field(name) => {
489                // Simple fields don't need brackets
490                if name.chars().all(|c| c.is_alphanumeric() || c == '_') {
491                    format!(".{name}")
492                } else {
493                    format!("[\"{}\"]", escape_json_string(name))
494                }
495            }
496            Self::Index(i) => format!("[{i}]"),
497        }
498    }
499}
500
501/// State for path iteration
502struct PathState {
503    components: Vec<PathComponent>,
504    array_indices: Vec<usize>,
505}
506
507impl PathState {
508    fn new() -> Self {
509        Self {
510            components: Vec::with_capacity(16),
511            array_indices: Vec::with_capacity(8),
512        }
513    }
514
515    fn current_path(&self, prefix: &str) -> String {
516        let mut path = prefix.to_string();
517        for component in &self.components {
518            path.push_str(&component.as_path_segment());
519        }
520        path
521    }
522
523    fn push_field(&mut self, name: &str) {
524        self.components.push(PathComponent::Field(name.to_string()));
525    }
526
527    fn push_index(&mut self, index: usize) {
528        self.components.push(PathComponent::Index(index));
529    }
530
531    fn pop(&mut self) {
532        self.components.pop();
533    }
534
535    fn enter_array(&mut self) {
536        self.array_indices.push(0);
537    }
538
539    fn exit_array(&mut self) {
540        self.array_indices.pop();
541    }
542
543    fn next_array_index(&mut self) -> usize {
544        let idx = self.array_indices.last().copied().unwrap_or(0);
545        if let Some(last) = self.array_indices.last_mut() {
546            *last += 1;
547        }
548        idx
549    }
550}
551
552/// Iterator yielding (path, node) pairs for gron-style output
553pub struct PathIterator<'a, T: TapeSource> {
554    tape: &'a T,
555    index: usize,
556    state: PathState,
557    prefix: String,
558}
559
560impl<'a, T: TapeSource> PathIterator<'a, T> {
561    /// Create a new path iterator with default prefix "json"
562    #[must_use]
563    pub fn new(tape: &'a T) -> Self {
564        Self::with_prefix(tape, "json")
565    }
566
567    /// Create a new path iterator with custom prefix
568    #[must_use]
569    pub fn with_prefix(tape: &'a T, prefix: &str) -> Self {
570        Self {
571            tape,
572            index: 0,
573            state: PathState::new(),
574            prefix: prefix.to_string(),
575        }
576    }
577
578    /// Get the current path string
579    #[must_use]
580    pub fn current_path(&self) -> String {
581        self.state.current_path(&self.prefix)
582    }
583}
584
585impl<'a, T: TapeSource> Iterator for PathIterator<'a, T> {
586    type Item = (String, TapeNodeRef<'a>);
587
588    fn next(&mut self) -> Option<Self::Item> {
589        if self.index >= self.tape.len() {
590            return None;
591        }
592
593        let node = self.tape.node_at(self.index)?;
594        let path = self.state.current_path(&self.prefix);
595        self.index += 1;
596
597        // Update state for next iteration based on node type
598        match node.kind {
599            TapeNodeKind::ObjectStart { .. } => {
600                // Path already set, will be updated when we see keys
601            }
602            TapeNodeKind::ObjectEnd => {
603                self.state.pop();
604            }
605            TapeNodeKind::ArrayStart { .. } => {
606                self.state.enter_array();
607            }
608            TapeNodeKind::ArrayEnd => {
609                self.state.exit_array();
610                self.state.pop();
611            }
612            TapeNodeKind::Key => {
613                if let Some(TapeValue::String(ref name)) = node.value {
614                    self.state.push_field(name);
615                }
616            }
617            TapeNodeKind::Value => {
618                // After a value in an array, we need to track position
619                if self.state.array_indices.is_empty() {
620                    self.state.pop(); // Remove field after value
621                } else {
622                    self.state.pop(); // Remove current index
623                    let next_idx = self.state.next_array_index();
624                    self.state.push_index(next_idx);
625                }
626            }
627        }
628
629        Some((path, node))
630    }
631}
632
633// ============================================================================
634// Utility Functions
635// ============================================================================
636
637/// Escape a string for JSON output
638#[must_use]
639pub fn escape_json_string(s: &str) -> String {
640    use std::fmt::Write;
641    let mut result = String::with_capacity(s.len());
642    for ch in s.chars() {
643        match ch {
644            '"' => result.push_str("\\\""),
645            '\\' => result.push_str("\\\\"),
646            '\n' => result.push_str("\\n"),
647            '\r' => result.push_str("\\r"),
648            '\t' => result.push_str("\\t"),
649            c if c.is_control() => {
650                let _ = write!(result, "\\u{:04x}", c as u32);
651            }
652            c => result.push(c),
653        }
654    }
655    result
656}
657
658/// Unescape a JSON string
659#[must_use]
660pub fn unescape_json_string(s: &str) -> String {
661    let mut result = String::with_capacity(s.len());
662    let mut chars = s.chars();
663
664    while let Some(ch) = chars.next() {
665        if ch == '\\' {
666            match chars.next() {
667                Some('"') => result.push('"'),
668                Some('\\') | None => result.push('\\'),
669                Some('/') => result.push('/'),
670                Some('n') => result.push('\n'),
671                Some('r') => result.push('\r'),
672                Some('t') => result.push('\t'),
673                Some('b') => result.push('\u{0008}'),
674                Some('f') => result.push('\u{000C}'),
675                Some('u') => {
676                    let hex: String = chars.by_ref().take(4).collect();
677                    if let Ok(code) = u32::from_str_radix(&hex, 16)
678                        && let Some(c) = char::from_u32(code)
679                    {
680                        result.push(c);
681                    }
682                }
683                Some(c) => {
684                    result.push('\\');
685                    result.push(c);
686                }
687            }
688        } else {
689            result.push(ch);
690        }
691    }
692    result
693}
694
695// ============================================================================
696// Tests
697// ============================================================================
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702    use crate::DsonError;
703
704    // Mock tape for testing
705    struct MockTape {
706        nodes: Vec<(TapeNodeKind, Option<TapeValue<'static>>)>,
707        format: FormatKind,
708    }
709
710    impl MockTape {
711        fn new(format: FormatKind) -> Self {
712            Self {
713                nodes: Vec::new(),
714                format,
715            }
716        }
717
718        fn push(&mut self, kind: TapeNodeKind, value: Option<TapeValue<'static>>) {
719            self.nodes.push((kind, value));
720        }
721    }
722
723    impl TapeSource for MockTape {
724        fn format(&self) -> FormatKind {
725            self.format
726        }
727
728        fn len(&self) -> usize {
729            self.nodes.len()
730        }
731
732        fn node_at(&self, index: usize) -> Option<TapeNodeRef<'_>> {
733            self.nodes.get(index).map(|(kind, value)| TapeNodeRef {
734                kind: *kind,
735                value: value.clone(),
736                format: self.format,
737            })
738        }
739
740        fn skip_value(&self, start_index: usize) -> Result<usize> {
741            let node = self
742                .node_at(start_index)
743                .ok_or_else(|| DsonError::InvalidField("Index out of bounds".to_string()))?;
744
745            match node.kind {
746                TapeNodeKind::ObjectStart { count } => {
747                    // Skip object: key + value for each field, plus ObjectEnd
748                    let mut idx = start_index + 1;
749                    for _ in 0..count {
750                        idx += 1; // key
751                        idx = self.skip_value(idx)?; // value
752                    }
753                    Ok(idx + 1) // ObjectEnd
754                }
755                TapeNodeKind::ArrayStart { count } => {
756                    // Skip array: each element, plus ArrayEnd
757                    let mut idx = start_index + 1;
758                    for _ in 0..count {
759                        idx = self.skip_value(idx)?;
760                    }
761                    Ok(idx + 1) // ArrayEnd
762                }
763                _ => Ok(start_index + 1),
764            }
765        }
766
767        fn resolve_path(&self, _path: &str) -> Result<Option<usize>> {
768            // Simple mock: always returns None
769            Ok(None)
770        }
771    }
772
773    #[test]
774    fn test_tape_value_null() {
775        let val = TapeValue::Null;
776        assert!(val.is_null());
777        assert!(!val.is_bool());
778        assert_eq!(val.format_for_output(), "null");
779    }
780
781    #[test]
782    fn test_tape_value_bool() {
783        let val = TapeValue::Bool(true);
784        assert!(val.is_bool());
785        assert_eq!(val.as_bool(), Some(true));
786        assert_eq!(val.format_for_output(), "true");
787
788        let val = TapeValue::Bool(false);
789        assert_eq!(val.format_for_output(), "false");
790    }
791
792    #[test]
793    fn test_tape_value_int() {
794        let val = TapeValue::Int(42);
795        assert!(val.is_number());
796        assert_eq!(val.as_int(), Some(42));
797        assert_eq!(val.format_for_output(), "42");
798    }
799
800    #[test]
801    fn test_tape_value_float() {
802        let val = TapeValue::Float(3.15);
803        assert!(val.is_number());
804        assert_eq!(val.as_float(), Some(3.15));
805    }
806
807    #[test]
808    fn test_tape_value_string() {
809        let val = TapeValue::String(Cow::Borrowed("hello"));
810        assert!(val.is_string());
811        assert_eq!(val.as_str(), Some("hello"));
812        assert_eq!(val.format_for_output(), "\"hello\"");
813    }
814
815    #[test]
816    fn test_tape_value_string_escape() {
817        let val = TapeValue::String(Cow::Borrowed("hello\nworld"));
818        assert_eq!(val.format_for_output(), "\"hello\\nworld\"");
819    }
820
821    #[test]
822    fn test_tape_value_into_owned() {
823        let val = TapeValue::String(Cow::Borrowed("test"));
824        let owned = val.into_owned();
825        assert!(matches!(owned, TapeValue::String(Cow::Owned(_))));
826    }
827
828    #[test]
829    fn test_tape_node_kind_container_start() {
830        assert!(TapeNodeKind::ObjectStart { count: 2 }.is_container_start());
831        assert!(TapeNodeKind::ArrayStart { count: 3 }.is_container_start());
832        assert!(!TapeNodeKind::Value.is_container_start());
833    }
834
835    #[test]
836    fn test_tape_node_kind_element_count() {
837        assert_eq!(
838            TapeNodeKind::ObjectStart { count: 5 }.element_count(),
839            Some(5)
840        );
841        assert_eq!(TapeNodeKind::Value.element_count(), None);
842    }
843
844    #[test]
845    fn test_mock_tape_basic() {
846        let mut tape = MockTape::new(FormatKind::Json);
847        tape.push(TapeNodeKind::ObjectStart { count: 1 }, None);
848        tape.push(
849            TapeNodeKind::Key,
850            Some(TapeValue::String(Cow::Borrowed("name"))),
851        );
852        tape.push(
853            TapeNodeKind::Value,
854            Some(TapeValue::String(Cow::Borrowed("test"))),
855        );
856        tape.push(TapeNodeKind::ObjectEnd, None);
857
858        assert_eq!(tape.len(), 4);
859        assert!(!tape.is_empty());
860        assert_eq!(tape.format(), FormatKind::Json);
861    }
862
863    #[test]
864    fn test_tape_iterator() {
865        let mut tape = MockTape::new(FormatKind::Json);
866        tape.push(TapeNodeKind::Value, Some(TapeValue::Int(1)));
867        tape.push(TapeNodeKind::Value, Some(TapeValue::Int(2)));
868        tape.push(TapeNodeKind::Value, Some(TapeValue::Int(3)));
869
870        assert_eq!(tape.iter().count(), 3);
871    }
872
873    #[test]
874    fn test_tape_iterator_exact_size() {
875        let mut tape = MockTape::new(FormatKind::Json);
876        tape.push(TapeNodeKind::Value, Some(TapeValue::Int(1)));
877        tape.push(TapeNodeKind::Value, Some(TapeValue::Int(2)));
878
879        let iter = tape.iter();
880        assert_eq!(iter.len(), 2);
881    }
882
883    #[test]
884    fn test_skip_value_scalar() {
885        let mut tape = MockTape::new(FormatKind::Json);
886        tape.push(TapeNodeKind::Value, Some(TapeValue::Int(42)));
887
888        assert_eq!(tape.skip_value(0).unwrap(), 1);
889    }
890
891    #[test]
892    fn test_skip_value_object() {
893        let mut tape = MockTape::new(FormatKind::Json);
894        tape.push(TapeNodeKind::ObjectStart { count: 1 }, None);
895        tape.push(
896            TapeNodeKind::Key,
897            Some(TapeValue::String(Cow::Borrowed("a"))),
898        );
899        tape.push(TapeNodeKind::Value, Some(TapeValue::Int(1)));
900        tape.push(TapeNodeKind::ObjectEnd, None);
901
902        assert_eq!(tape.skip_value(0).unwrap(), 4);
903    }
904
905    #[test]
906    fn test_skip_value_array() {
907        let mut tape = MockTape::new(FormatKind::Json);
908        tape.push(TapeNodeKind::ArrayStart { count: 2 }, None);
909        tape.push(TapeNodeKind::Value, Some(TapeValue::Int(1)));
910        tape.push(TapeNodeKind::Value, Some(TapeValue::Int(2)));
911        tape.push(TapeNodeKind::ArrayEnd, None);
912
913        assert_eq!(tape.skip_value(0).unwrap(), 4);
914    }
915
916    #[test]
917    fn test_value_at() {
918        let mut tape = MockTape::new(FormatKind::Json);
919        tape.push(TapeNodeKind::Value, Some(TapeValue::Int(42)));
920
921        assert_eq!(tape.value_at(0), Some(TapeValue::Int(42)));
922    }
923
924    #[test]
925    fn test_key_at() {
926        let mut tape = MockTape::new(FormatKind::Json);
927        tape.push(
928            TapeNodeKind::Key,
929            Some(TapeValue::String(Cow::Borrowed("test"))),
930        );
931
932        assert_eq!(tape.key_at(0), Some(Cow::Borrowed("test")));
933    }
934
935    #[test]
936    fn test_escape_json_string() {
937        assert_eq!(escape_json_string("hello"), "hello");
938        assert_eq!(escape_json_string("hello\nworld"), "hello\\nworld");
939        assert_eq!(escape_json_string("quote\"here"), "quote\\\"here");
940        assert_eq!(escape_json_string("back\\slash"), "back\\\\slash");
941    }
942
943    #[test]
944    fn test_unescape_json_string() {
945        assert_eq!(unescape_json_string("hello"), "hello");
946        assert_eq!(unescape_json_string("hello\\nworld"), "hello\nworld");
947        assert_eq!(unescape_json_string("quote\\\"here"), "quote\"here");
948        assert_eq!(unescape_json_string("back\\\\slash"), "back\\slash");
949    }
950
951    #[test]
952    fn test_escape_unescape_roundtrip() {
953        let original = "Hello\n\"World\"\t\\Test";
954        let escaped = escape_json_string(original);
955        let unescaped = unescape_json_string(&escaped);
956        assert_eq!(unescaped, original);
957    }
958
959    #[test]
960    fn test_path_component_field() {
961        let comp = PathComponent::Field("name".to_string());
962        assert_eq!(comp.as_path_segment(), ".name");
963
964        let comp = PathComponent::Field("with space".to_string());
965        assert_eq!(comp.as_path_segment(), "[\"with space\"]");
966    }
967
968    #[test]
969    fn test_path_component_index() {
970        let comp = PathComponent::Index(42);
971        assert_eq!(comp.as_path_segment(), "[42]");
972    }
973
974    #[test]
975    fn test_tape_node_ref_constructors() {
976        let node = TapeNodeRef::object_start(5, FormatKind::Json);
977        assert!(matches!(node.kind, TapeNodeKind::ObjectStart { count: 5 }));
978
979        let node = TapeNodeRef::array_start(3, FormatKind::Json);
980        assert!(matches!(node.kind, TapeNodeKind::ArrayStart { count: 3 }));
981
982        let node = TapeNodeRef::value(TapeValue::Null, FormatKind::Json);
983        assert!(matches!(node.kind, TapeNodeKind::Value));
984        assert_eq!(node.value, Some(TapeValue::Null));
985    }
986}