This is an automated email from the ASF dual-hosted git repository.

Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new e5c18fa5fe json: Add option `flatten_top_level_arrays` to 
`TapeDecoder` (#9496)
e5c18fa5fe is described below

commit e5c18fa5fe14d58e0bbccb03a72c3fc3cdf2c474
Author: Alexander Rafferty <[email protected]>
AuthorDate: Sat Aug 22 23:09:28 2026 +1000

    json: Add option `flatten_top_level_arrays` to `TapeDecoder` (#9496)
    
    # Which issue does this PR close?
    
    N/A
    
    # Rationale for this change
    
    There are JSON files in the wild that are structured as a single array
    at the root level, i.e. `[{...}, {....}, {...}]`. At present, this would
    be read by the tape decoder as one single array-valued record, but since
    each nested object is what constitutes a "row", this is not ideal.
    
    So, this PR extends the `TapeDecoder` to support these kinds of JSON
    files via an opt-in configuration option.
    
    A PR was recently merged into Datafusion to support this exact usecase,
    but it currently employs a streaming converter to transform "top-level
    array" JSON sources into ND-JSON. If this PR is merged, this would
    facilitate the refactoring of this feature to use the `TapeDecoder`
    directly, which should noticably improve performance. See
    https://github.com/apache/datafusion/issues/19920 for context.
    
    # What changes are included in this PR?
    
    This PR modestly refactors the `TapeDecoder` to facilitate this use
    case, by adding an option called `flatten_top_level_arrays`. When
    enabled, any top-level arrays are "flattened" such that their elements
    each become an individual row in the output batch, rather than the
    entire array becoming a single row as would otherwise happen.
    
    # Are these changes tested?
    
    Yes, these changes pass all existing unit tests, and I've added a new
    unit test for this feature specifically.
    
    # Are there any user-facing changes?
    
    The primary change is the addition of a `new_with_options` method on
    `TapeDecoder`, that allows the user to specify a value for the new
    configuration option. I figured a config struct was more future-proof,
    and we may want to mark it as `#[non_exhaustive]` but that's debatable.
    
    ---------
    
    Co-authored-by: Jeffrey Vo <[email protected]>
---
 arrow-json/src/reader/mod.rs  |  77 ++++++++--
 arrow-json/src/reader/tape.rs | 320 ++++++++++++++++++++++++++++++++++++++----
 2 files changed, 360 insertions(+), 37 deletions(-)

diff --git a/arrow-json/src/reader/mod.rs b/arrow-json/src/reader/mod.rs
index 0c6039d057..643e45f238 100644
--- a/arrow-json/src/reader/mod.rs
+++ b/arrow-json/src/reader/mod.rs
@@ -161,7 +161,7 @@ use crate::reader::run_end_array::RunEndEncodedArrayDecoder;
 use crate::reader::string_array::StringArrayDecoder;
 use crate::reader::string_view_array::StringViewArrayDecoder;
 use crate::reader::struct_array::StructArrayDecoder;
-use crate::reader::tape::{Tape, TapeDecoder};
+use crate::reader::tape::{Tape, TapeDecoder, TapeDecoderOptions};
 use crate::reader::timestamp_array::TimestampArrayDecoder;
 
 pub use schema::*;
@@ -185,6 +185,7 @@ mod timestamp_array;
 mod value_iter;
 
 /// A builder for [`Reader`] and [`Decoder`]
+#[derive(Clone)]
 pub struct ReaderBuilder {
     batch_size: usize,
     coerce_primitive: bool,
@@ -192,6 +193,7 @@ pub struct ReaderBuilder {
     ignore_type_conflicts: bool,
     is_field: bool,
     struct_mode: StructMode,
+    flatten_top_level_arrays: bool,
 
     schema: SchemaRef,
 }
@@ -213,6 +215,7 @@ impl ReaderBuilder {
             ignore_type_conflicts: false,
             is_field: false,
             struct_mode: Default::default(),
+            flatten_top_level_arrays: false,
             schema,
         }
     }
@@ -255,6 +258,7 @@ impl ReaderBuilder {
             ignore_type_conflicts: false,
             is_field: true,
             struct_mode: Default::default(),
+            flatten_top_level_arrays: false,
             schema: Arc::new(Schema::new([field.into()])),
         }
     }
@@ -313,6 +317,29 @@ impl ReaderBuilder {
             ..self
         }
     }
+    /// Sets whether to flatten top-level arrays.
+    ///
+    /// * When `true`, each element of a top-level array will be treated as 
its own row.
+    /// * When `false` (the default), the entire top-level array will be 
treated as one row.
+    ///
+    /// For example, consider this input file:
+    /// ```text
+    /// [{ "a": 1 }, { "a": 2 }, { "b": 3 }]
+    /// [{ "a": 4 }, { "a": 5 }, { "b": 6 }]
+    /// ```
+    ///
+    /// By default, this would be parsed as two rows, each an array containing 
three elements.
+    /// With this option set to `true`, however, this would be parsed as six 
rows.
+    ///
+    /// Note that even with this option set to `true`, top-level objects are 
still permitted
+    /// and will be parsed as individual rows in the exact same manner as when 
this option is
+    /// set to `false`. It is even possible to mix top-level arrays with 
top-level objects.
+    pub fn with_flatten(self, flatten_top_level_arrays: bool) -> Self {
+        Self {
+            flatten_top_level_arrays,
+            ..self
+        }
+    }
 
     /// Create a [`Reader`] with the provided [`BufRead`]
     pub fn build<R: BufRead>(self, reader: R) -> Result<Reader<R>, ArrowError> 
{
@@ -346,7 +373,11 @@ impl ReaderBuilder {
         Ok(Decoder {
             decoder,
             is_field: self.is_field,
-            tape_decoder: TapeDecoder::new(self.batch_size, num_fields),
+            tape_decoder: TapeDecoder::new(TapeDecoderOptions {
+                batch_size: self.batch_size,
+                num_fields,
+                flatten_top_level_arrays: self.flatten_top_level_arrays,
+            }),
             batch_size: self.batch_size,
             schema: self.schema,
         })
@@ -877,13 +908,21 @@ mod tests {
         strict_mode: bool,
         schema: SchemaRef,
     ) -> Vec<RecordBatch> {
+        let config = ReaderBuilder::new(schema)
+            .with_batch_size(batch_size)
+            .with_strict_mode(strict_mode)
+            .with_coerce_primitive(coerce_primitive);
+        do_read_config(buf, config)
+    }
+
+    fn do_read_config(buf: &str, builder: ReaderBuilder) -> Vec<RecordBatch> {
         let mut unbuffered = vec![];
 
         // Test with different batch sizes to test for boundary conditions
-        for batch_size in [1, 3, 100, batch_size] {
-            unbuffered = ReaderBuilder::new(schema.clone())
+        for batch_size in [1, 3, 100, builder.batch_size] {
+            unbuffered = builder
+                .clone()
                 .with_batch_size(batch_size)
-                .with_coerce_primitive(coerce_primitive)
                 .build(Cursor::new(buf.as_bytes()))
                 .unwrap()
                 .collect::<Result<Vec<_>, _>>()
@@ -895,10 +934,9 @@ mod tests {
 
             // Test with different buffer sizes to test for boundary conditions
             for b in [1, 3, 5] {
-                let buffered = ReaderBuilder::new(schema.clone())
+                let buffered = builder
+                    .clone()
                     .with_batch_size(batch_size)
-                    .with_coerce_primitive(coerce_primitive)
-                    .with_strict_mode(strict_mode)
                     .build(BufReader::with_capacity(b, 
Cursor::new(buf.as_bytes())))
                     .unwrap()
                     .collect::<Result<Vec<_>, _>>()
@@ -3793,4 +3831,27 @@ mod tests {
         assert_eq!(nested_values.value(0), "x");
         assert_eq!(nested_values.value(1), "y");
     }
+
+    #[test]
+    fn test_flatten_top_level_arrays() {
+        let buf = r#"
+            [
+                {"a": 1},
+                {"a": 2}
+            ]
+            {"a": 3}
+            [{"a": 4}, {"a": 5}, {"a": 6}]
+            "#;
+
+        let schema = Arc::new(Schema::new(vec![Field::new("a", 
DataType::Int32, true)]));
+        let batches = do_read_config(buf, 
ReaderBuilder::new(schema).with_flatten(true));
+        assert_eq!(batches.len(), 1);
+
+        let col = batches[0].column(0);
+        let col = col.as_any().downcast_ref::<Int32Array>().unwrap();
+        assert_eq!(col.len(), 6);
+        for i in 0..6 {
+            assert_eq!(col.value(i), (i as i32) + 1);
+        }
+    }
 }
diff --git a/arrow-json/src/reader/tape.rs b/arrow-json/src/reader/tape.rs
index 32f40024bf..d493f6cb70 100644
--- a/arrow-json/src/reader/tape.rs
+++ b/arrow-json/src/reader/tape.rs
@@ -216,6 +216,13 @@ impl<'a> Tape<'a> {
 /// States based on <https://www.json.org/json-en.html>
 #[derive(Debug, Copy, Clone)]
 enum DecoderState {
+    /// Decoding a top-level list, where each element is
+    /// treated as an individual row
+    ///
+    /// This can only appear as the first element on the stack,
+    /// and it is valid to flush a batch when this is the only
+    /// state on the stack
+    TopLevelList,
     /// Decoding an object
     ///
     /// Contains index of start [`TapeElement::StartObject`]
@@ -242,6 +249,7 @@ enum DecoderState {
 impl DecoderState {
     fn as_str(&self) -> &'static str {
         match self {
+            DecoderState::TopLevelList => "top-level list",
             DecoderState::Object(_) => "object",
             DecoderState::List(_) => "list",
             DecoderState::String => "string",
@@ -295,7 +303,9 @@ macro_rules! next {
 }
 
 /// Implements a state machine for decoding JSON to a tape
+#[derive(Debug)]
 pub struct TapeDecoder {
+    /// The decoded elements
     elements: Vec<TapeElement>,
 
     /// The number of rows decoded, including any in progress if 
`!stack.is_empty()`
@@ -304,6 +314,12 @@ pub struct TapeDecoder {
     /// Number of rows to read per batch
     batch_size: usize,
 
+    /// Whether to flatten top-level arrays into the stream of JSON rows,
+    /// meaning that each of their elements will be treated as an individual 
row
+    ///
+    /// When `false` (the default), the entire top-level array will be treated 
as one row
+    flatten_top_level_arrays: bool,
+
     /// A buffer of parsed string data
     ///
     /// Note: if part way through a record, i.e. `stack` is not empty,
@@ -317,10 +333,26 @@ pub struct TapeDecoder {
     stack: Vec<DecoderState>,
 }
 
+/// Configuration for a `TapeDecoder`.
+#[derive(Clone, Copy, Debug)]
+pub struct TapeDecoderOptions {
+    /// The batch size
+    pub batch_size: usize,
+    /// The estimated number of fields in each row
+    pub num_fields: usize,
+    /// Whether to flatten top-level arrays
+    pub flatten_top_level_arrays: bool,
+}
+
 impl TapeDecoder {
-    /// Create a new [`TapeDecoder`] with the provided batch size
-    /// and an estimated number of fields in each row
-    pub fn new(batch_size: usize, num_fields: usize) -> Self {
+    /// Create a new [`TapeDecoder`] with the provided options
+    pub fn new(options: TapeDecoderOptions) -> Self {
+        let TapeDecoderOptions {
+            batch_size,
+            num_fields,
+            flatten_top_level_arrays,
+        } = options;
+
         let tokens_per_row = 2 + num_fields * 2;
         let mut offsets = Vec::with_capacity(batch_size * (num_fields * 2) + 
1);
         offsets.push(0);
@@ -332,6 +364,7 @@ impl TapeDecoder {
             offsets,
             elements,
             batch_size,
+            flatten_top_level_arrays,
             cur_row: 0,
             bytes: Vec::with_capacity(num_fields * 2 * 8),
             stack: Vec::with_capacity(10),
@@ -346,18 +379,51 @@ impl TapeDecoder {
                 Some(l) => l,
                 None => {
                     iter.skip_whitespace();
-                    if iter.is_empty() || self.cur_row >= self.batch_size {
+                    if self.cur_row >= self.batch_size {
                         break;
                     }
 
-                    // Start of row
-                    self.cur_row += 1;
-                    self.stack.push(DecoderState::Value);
+                    match iter.peek() {
+                        Some(b'[') if self.flatten_top_level_arrays => {
+                            // Consume the `[` without writing it
+                            iter.next();
+                            self.stack.push(DecoderState::TopLevelList);
+                        }
+                        Some(_) => {
+                            // Start of row
+                            self.cur_row += 1;
+                            self.stack.push(DecoderState::Value);
+                        }
+                        None => break,
+                    }
+
+                    // There is now a top-most state to process
                     self.stack.last_mut().unwrap()
                 }
             };
 
             match state {
+                // Decoding a top-level list
+                DecoderState::TopLevelList => {
+                    iter.advance_until(|b| !json_whitespace(b) && b != b',');
+                    if self.cur_row >= self.batch_size {
+                        break;
+                    }
+
+                    match iter.peek() {
+                        Some(b']') => {
+                            // Consume the `]` without writing it
+                            iter.next();
+                            self.stack.pop();
+                        }
+                        Some(_) => {
+                            // Start of row
+                            self.cur_row += 1;
+                            self.stack.push(DecoderState::Value);
+                        }
+                        None => break,
+                    }
+                }
                 // Decoding an object
                 DecoderState::Object(start_idx) => {
                     iter.advance_until(|b| !json_whitespace(b) && b != b',');
@@ -554,16 +620,20 @@ impl TapeDecoder {
     /// True if the decoder is part way through decoding a row. If so, calling 
[`Self::finish`]
     /// would return an error.
     pub fn has_partial_row(&self) -> bool {
-        !self.stack.is_empty()
+        !matches!(self.stack.last(), None | Some(DecoderState::TopLevelList))
     }
 
     /// Finishes the current [`Tape`]
     pub fn finish(&self) -> Result<Tape<'_>, ArrowError> {
-        if let Some(b) = self.stack.last() {
-            return Err(ArrowError::JsonError(format!(
-                "Truncated record whilst reading {}",
-                b.as_str()
-            )));
+        match self.stack.last() {
+            None => {}
+            Some(DecoderState::TopLevelList) => {}
+            Some(state) => {
+                return Err(ArrowError::JsonError(format!(
+                    "Truncated record whilst reading {}",
+                    state.as_str()
+                )));
+            }
         }
 
         if self.offsets.len() >= u32::MAX as usize {
@@ -607,7 +677,7 @@ impl TapeDecoder {
 
     /// Clears this [`TapeDecoder`] in preparation to read the next batch
     pub fn clear(&mut self) {
-        assert!(self.stack.is_empty());
+        assert!(!self.has_partial_row());
 
         self.cur_row = 0;
         self.bytes.clear();
@@ -744,6 +814,15 @@ fn parse_hex(b: u8) -> Result<u8, ArrowError> {
 mod tests {
     use super::*;
 
+    /// Helper method to create a `TapeDecoder` with sensible defaults
+    fn tape_decoder() -> TapeDecoder {
+        TapeDecoder::new(TapeDecoderOptions {
+            batch_size: 16,
+            num_fields: 2,
+            flatten_top_level_arrays: false,
+        })
+    }
+
     #[test]
     fn test_sizes() {
         assert_eq!(std::mem::size_of::<DecoderState>(), 8);
@@ -767,7 +846,7 @@ mod tests {
 
         {"a": ["", "foo", ["bar", "c"]], "b": {"1": []}, "c": {"2": [1, 2, 3]} 
}
         "#;
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         decoder.decode(a.as_bytes()).unwrap();
         assert!(!decoder.has_partial_row());
         assert_eq!(decoder.num_buffered_rows(), 7);
@@ -877,21 +956,21 @@ mod tests {
     #[test]
     fn test_invalid() {
         // Test invalid
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         let err = decoder.decode(b"hello").unwrap_err().to_string();
         assert_eq!(
             err,
             "Json error: Encountered unexpected 'h' whilst parsing value"
         );
 
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         let err = decoder.decode(b"{\"hello\": }").unwrap_err().to_string();
         assert_eq!(
             err,
             "Json error: Encountered unexpected '}' whilst parsing value"
         );
 
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         let err = decoder
             .decode(b"{\"hello\": [ false, tru ]}")
             .unwrap_err()
@@ -901,7 +980,7 @@ mod tests {
             "Json error: Encountered unexpected ' ' whilst parsing literal"
         );
 
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         let err = decoder
             .decode(b"{\"hello\": \"\\ud8\"}")
             .unwrap_err()
@@ -912,7 +991,7 @@ mod tests {
         );
 
         // Missing surrogate pair
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         let err = decoder
             .decode(b"{\"hello\": \"\\ud83d\"}")
             .unwrap_err()
@@ -923,40 +1002,40 @@ mod tests {
         );
 
         // Test truncation
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         decoder.decode(b"{\"he").unwrap();
         assert!(decoder.has_partial_row());
         assert_eq!(decoder.num_buffered_rows(), 1);
         let err = decoder.finish().unwrap_err().to_string();
         assert_eq!(err, "Json error: Truncated record whilst reading string");
 
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         decoder.decode(b"{\"hello\" : ").unwrap();
         let err = decoder.finish().unwrap_err().to_string();
         assert_eq!(err, "Json error: Truncated record whilst reading value");
 
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         decoder.decode(b"{\"hello\" : [").unwrap();
         let err = decoder.finish().unwrap_err().to_string();
         assert_eq!(err, "Json error: Truncated record whilst reading list");
 
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         decoder.decode(b"{\"hello\" : tru").unwrap();
         let err = decoder.finish().unwrap_err().to_string();
         assert_eq!(err, "Json error: Truncated record whilst reading true");
 
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         decoder.decode(b"{\"hello\" : nu").unwrap();
         let err = decoder.finish().unwrap_err().to_string();
         assert_eq!(err, "Json error: Truncated record whilst reading null");
 
         // Test invalid UTF-8
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         decoder.decode(b"{\"hello\" : \"world\xFF\"}").unwrap();
         let err = decoder.finish().unwrap_err().to_string();
         assert_eq!(err, "Json error: Encountered non-UTF-8 data");
 
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         decoder.decode(b"{\"\xe2\" : \"\x96\xa1\"}").unwrap();
         let err = decoder.finish().unwrap_err().to_string();
         assert_eq!(err, "Json error: Encountered truncated UTF-8 sequence");
@@ -964,12 +1043,195 @@ mod tests {
 
     #[test]
     fn test_invalid_surrogates() {
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         let res = decoder.decode(b"{\"test\": \"\\ud800\\ud801\"}");
         assert!(res.is_err());
 
-        let mut decoder = TapeDecoder::new(16, 2);
+        let mut decoder = tape_decoder();
         let res = decoder.decode(b"{\"test\": \"\\udc00\\udc01\"}");
         assert!(res.is_err());
     }
+
+    #[test]
+    fn test_flatten_top_level_arrays() {
+        let input = r#"
+        [
+            {"hello": "world", "foo": 2, "bar": 45},
+            {"a": true, "b": false, "c": null}
+        ]
+        [
+            {"a": "b", "object": {"nested": "hello", "foo": 23}},
+            {"a": ["", "foo", ["bar", "c"]]},
+            {"hello": "world", "foo": 2, "bar": 27}
+        ]"#;
+        const TOTAL_ROWS: usize = 5;
+
+        // Check that regular decoding returns two rows
+        let mut decoder = tape_decoder();
+        decoder.decode(input.as_bytes()).unwrap();
+        assert!(!decoder.has_partial_row());
+        assert_eq!(decoder.num_buffered_rows(), 2);
+
+        let expected = TestCase::new()
+            // {"hello": "world", "foo": 2, "bar": 45}
+            .start_object(6)
+            .string("hello")
+            .string("world")
+            .string("foo")
+            .number("2")
+            .string("bar")
+            .number("45")
+            .end_object(6)
+            // {"a": true, "b": false, "c": null}
+            .start_object(6)
+            .string("a")
+            .r#true()
+            .string("b")
+            .r#false()
+            .string("c")
+            .null()
+            .end_object(6)
+            // {"a": "b", "object": {"nested": "hello", "foo": 23}}
+            .start_object(9)
+            .string("a")
+            .string("b")
+            .string("object")
+            .start_object(4)
+            .string("nested")
+            .string("hello")
+            .string("foo")
+            .number("23")
+            .end_object(4)
+            .end_object(9)
+            // {"a": ["", "foo", ["bar", "c"]]}
+            .start_object(9)
+            .string("a")
+            .start_list(6)
+            .string("")
+            .string("foo")
+            .start_list(2)
+            .string("bar")
+            .string("c")
+            .end_list(2)
+            .end_list(6)
+            .end_object(9)
+            // {"hello": "world", "foo": 2, "bar": 27}
+            .start_object(6)
+            .string("hello")
+            .string("world")
+            .string("foo")
+            .number("2")
+            .string("bar")
+            .number("27")
+            .end_object(6);
+
+        // Check that decoding with `flatten_top_level_arrays` yields rows 
correctly,
+        // and respects the configured batch size
+        for batch_size in [1, 2, 3, 4, 8] {
+            let mut decoder = TapeDecoder::new(TapeDecoderOptions {
+                batch_size,
+                num_fields: 2,
+                flatten_top_level_arrays: true,
+            });
+            decoder.decode(input.as_bytes()).unwrap();
+            assert!(!decoder.has_partial_row());
+            assert_eq!(decoder.num_buffered_rows(), 
batch_size.min(TOTAL_ROWS));
+
+            let finished = decoder.finish().unwrap();
+            assert!(!decoder.has_partial_row());
+            assert_eq!(decoder.num_buffered_rows(), 
batch_size.min(TOTAL_ROWS)); // didn't call clear() yet
+            assert_eq!(
+                finished.elements,
+                &expected.elements[..finished.elements.len()]
+            );
+            assert_eq!(
+                finished.strings,
+                &expected.strings[..finished.strings.len()]
+            );
+            assert_eq!(
+                finished.string_offsets,
+                &expected.string_offsets[..finished.string_offsets.len()]
+            );
+
+            decoder.clear();
+            assert!(!decoder.has_partial_row());
+            assert_eq!(decoder.num_buffered_rows(), 0);
+        }
+    }
+
+    /// The expected elements, strings and string offsets for a test case
+    struct TestCase {
+        elements: Vec<TapeElement>,
+        strings: String,
+        string_offsets: Vec<usize>,
+    }
+
+    impl TestCase {
+        fn new() -> Self {
+            Self {
+                elements: vec![TapeElement::Null],
+                strings: String::new(),
+                string_offsets: vec![0],
+            }
+        }
+
+        fn start_object(mut self, len: usize) -> Self {
+            let end_idx = (self.elements.len() + len + 1) as u32;
+            self.elements.push(TapeElement::StartObject(end_idx));
+            self
+        }
+
+        fn end_object(mut self, len: usize) -> Self {
+            let start_idx = (self.elements.len() - len - 1) as u32;
+            self.elements.push(TapeElement::EndObject(start_idx));
+            self
+        }
+
+        fn start_list(mut self, len: usize) -> Self {
+            let end_idx = (self.elements.len() + len + 1) as u32;
+            self.elements.push(TapeElement::StartList(end_idx));
+            self
+        }
+
+        fn end_list(mut self, len: usize) -> Self {
+            let start_idx = (self.elements.len() - len - 1) as u32;
+            self.elements.push(TapeElement::EndList(start_idx));
+            self
+        }
+
+        fn string(mut self, raw: &str) -> Self {
+            let idx = (self.string_offsets.len() - 1) as u32;
+            let start = self.strings.len();
+            let end = start + raw.len();
+            self.elements.push(TapeElement::String(idx));
+            self.strings.push_str(raw);
+            self.string_offsets.push(end);
+            self
+        }
+
+        fn number(mut self, raw: &str) -> Self {
+            let idx = (self.string_offsets.len() - 1) as u32;
+            let start = self.strings.len();
+            let end = start + raw.len();
+            self.elements.push(TapeElement::Number(idx));
+            self.strings.push_str(raw);
+            self.string_offsets.push(end);
+            self
+        }
+
+        fn r#true(mut self) -> Self {
+            self.elements.push(TapeElement::True);
+            self
+        }
+
+        fn r#false(mut self) -> Self {
+            self.elements.push(TapeElement::False);
+            self
+        }
+
+        fn null(mut self) -> Self {
+            self.elements.push(TapeElement::Null);
+            self
+        }
+    }
 }

Reply via email to