=begin pod

=head1 NAME

JSONL - Read, write, edit, and filter JSONL (JSON Lines) files

=head1 SYNOPSIS

=begin code :lang<raku>

use JSONL;

# Convenience functions
my @lines = read-jsonl('data.jsonl'.IO).list;
write-jsonl('output.jsonl'.IO, [%(:name("Alice")), %(:name("Bob"))]);
append-jsonl('output.jsonl'.IO, %(:name("Charlie")));

# Direct hash-like access on lines
for read-jsonl('data.jsonl'.IO) -> $line {
    say $line<name>;           # Hash-like access
    say $line<meta><score>;    # Nested access
    say $line.line-number;     # 1-based source line number
}

# Grep with clean syntax
my @adults = read-jsonl('data.jsonl'.IO).grep({ $_<age> > 18 }).list;

=end code

=head1 DESCRIPTION

JSONL provides a complete toolkit for working with JSONL (JSON Lines / NDJSON) files.
Supports any JSON value type per line (objects, arrays, scalars).

=head2 JSONL::Line

A single parsed JSONL line. Pairs the parsed value with its source line number.
Supports hash-like (C<$line<key>>) and array-like (C<$line[0]>) access.

=begin code :lang<raku>

my JSONL::Line $line = ...;
$line.value;                  # The parsed JSON value (Hash, Array, Str, Int, etc.)
$line.line-number;            # 1-based line number from source
$line.to-json;                # Serialize back to compact JSON string
$line.Str;                    # Same as to-json

# Hash-like access (when value is a Hash)
$line<name>;                  # AT-KEY
$line<name>:exists;           # EXISTS-KEY
$line.keys;                   # Key list
$line.values;                 # Value list
$line.kv;                     # Key-value pairs
$line.elems;                  # Number of elements

# Array-like access (when value is an Array)
$line[0];                     # AT-POS
$line[0]:exists;              # EXISTS-POS

=end code

=head2 JSONL::Reader

Lazy/streaming JSONL reader. Accepts file paths or IO handles.

=begin code :lang<raku>

# From file
my JSONL::Reader $r .= new(:path('data.jsonl'.IO));

# From handle (stdin, pipes)
my JSONL::Reader $r .= new(:handle($*IN));

# Lenient mode: skip blank lines, collect malformed as warnings
my JSONL::Reader $r .= new(:path($path), :lenient);
say $r.warnings;              # Lines that failed to parse

# Methods
$r.lines;                     # Lazy Seq of JSONL::Line
$r.list;                      # Eager List of all lines
$r.head(10);                  # First 10 lines
$r.tail(10);                  # Last 10 lines
$r.line-at(5);                # Line at 0-based index
$r.count;                     # Total line count
$r.grep({ $_<age> > 18 });   # Filter lines
$r.sample(100);               # Random sample (reservoir sampling)
$r.summary;                   # Stats: count, types, keys
$r.schema(:sample(100));      # Infer field=>type map

=end code

=head2 JSONL::Writer

Writes JSONL to files or handles. Always compact JSON, one value per line.

=begin code :lang<raku>

# To file
my JSONL::Writer $w .= new(:path('out.jsonl'.IO));
my JSONL::Writer $w .= new(:path($path), :!sorted-keys);  # Disable key sorting

# To handle
my JSONL::Writer $w .= new(:handle($fh));

# Methods
$w.write-line(%(:id(1)));     # Write single value + newline
$w.write-all(@values);        # Write all values (overwrites file)
$w.append(%(:id(2)));         # Open in append mode, write, close
$w.append-many(@values);      # Append multiple values
$w.close;                     # Close handle if we opened it

=end code

=head3 Durability with C<:flush>

By default, writes are buffered by the underlying handle and may not reach
disk immediately. For long-lived handle-mode writers (e.g. an append-only
transcript kept open across many small writes), a crash between writes can
lose more than the in-flight line if the OS/runtime buffer is holding
unflushed data. Passing C<:flush> makes the writer call C<.flush> on the
handle after every line, so a crash can lose at most the line currently
being written.

=begin code :lang<raku>

my IO::Handle $fh = 'transcript.jsonl'.IO.open(:a);
my JSONL::Writer $w .= new(:handle($fh), :flush);
$w.write-line(%(:role("user"), :text("hello")));   # durable as soon as this returns

=end code

C<:flush> defaults to C<False> to avoid the extra syscall overhead when
durability isn't required. Path-mode C<append>/C<append-many> already close
the handle after every call (which flushes implicitly), so C<:flush> mainly
matters for handle-mode writers and C<write-all>.

=head2 JSONL::Editor

In-place file editing. Reads the full file, applies the edit, writes it back.

=begin code :lang<raku>

my JSONL::Editor $e .= new;

$e.update-at($path, 0, %(:id(99)));       # Replace line at index
$e.delete-at($path, 2);                    # Remove line at index
$e.insert-at($path, 1, %(:id(50)));       # Insert at index
$e.transform($path, -> $line {            # Transform each line
    $line.value<id> > 5 ?? $line.value !! Nil  # Return Nil to delete
});

# All methods accept :lenient flag
$e.update-at($path, 0, $val, :lenient);

=end code

=head1 AUTHOR

Matt Doughty <matt@apogee.guru>

=head1 COPYRIGHT AND LICENSE

Copyright 2026 Matt Doughty

This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.

=end pod
