File Coverage

blib/lib/JSON/Lines.pm
Criterion Covered Total %
statement 244 273 89.3
branch 87 130 66.9
condition 26 39 66.6
subroutine 29 31 93.5
pod 21 21 100.0
total 407 494 82.3


line stmt bran cond sub pod time code
1             package JSON::Lines;
2 16     16   2045298 use 5.006; use strict; use warnings; our $VERSION = '1.11';
  16     16   110  
  16     16   102  
  16         110  
  16         580  
  16         122  
  16         28  
  16         1583  
3 16     16   19037 use Cpanel::JSON::XS; use base 'Import::Export';
  16     16   128576  
  16         1549  
  16         143  
  16         33  
  16         16338  
4             our ($JSON, $LINES, $STRING, %EX);
5             BEGIN {
6 16     16   361638 $JSON = Cpanel::JSON::XS->new;
7 16         96 $STRING = qr{ " (?> (?: [^"\\]++ | \\. )*+ ) " }x;
8 16         1588 $LINES = qr{ ( [\[\{] (?> (?: (?> [^\[\]\{\}"]++ ) | $STRING | (??{ $LINES }) )*+ ) [\]\}] ) }x;
9 16         67194 %EX = (
10             jsonl => [qw/all/]
11             );
12             }
13             sub jsonl {
14 4 50   4 1 191292 my %args = (scalar @_ == 1 ? %{$_[0]} : @_);
  0         0  
15 4         21 my $self = __PACKAGE__->new(%args);
16             return $args{file}
17             ? $self->encode_file($args{file}, $args{data})
18             : $self->encode($args{data})
19 4 50       16 if ($args{encode});
    100          
20             return $args{file}
21             ? $self->decode_file($args{file})
22             : $self->decode($args{data})
23 2 50       12 if ($args{decode});
    50          
24             }
25             sub new {
26 40 50   40 1 2864886 my ($pkg, %args) = (shift, scalar @_ == 1 ? %{$_[0]} : @_);
  0         0  
27 40         200 my $self = bless { headers => [], _buffer => '' }, $pkg;
28 40   66     451 exists $args{$_} && $JSON->$_($args{$_}) for qw/pretty canonical utf8/;
29 40         361 $self->{$_} = $args{$_} for qw/parse_headers error_cb success_cb/;
30 40         120 $self;
31             }
32             sub pretty {
33 9     9 1 3545 my ($self, $val) = @_;
34 9 100       69 return $JSON->get_indent if @_ == 1;
35 5         107 $JSON->pretty($val);
36 5         19 return $self;
37             }
38             sub canonical {
39 7     7 1 1008 my ($self, $val) = @_;
40 7 100       76 return $JSON->get_canonical if @_ == 1;
41 4         18 $JSON->canonical($val);
42 4         12 return $self;
43             }
44             sub utf8 {
45 6     6 1 19 my ($self, $val) = @_;
46 6 100       62 return $JSON->get_utf8 if @_ == 1;
47 3         15 $JSON->utf8($val);
48 3         10 return $self;
49             }
50             sub parse_headers {
51 5     5 1 11 my ($self, $val) = @_;
52 5 100       27 return $self->{parse_headers} if @_ == 1;
53 2         5 $self->{parse_headers} = $val;
54 2         6 return $self;
55             }
56             sub error_cb {
57 5     5 1 13 my ($self, $val) = @_;
58 5 100       28 return $self->{error_cb} if @_ == 1;
59 2         5 $self->{error_cb} = $val;
60 2         6 return $self;
61             }
62             sub success_cb {
63 5     5 1 13 my ($self, $val) = @_;
64 5 100       98 return $self->{success_cb} if @_ == 1;
65 2         17 $self->{success_cb} = $val;
66 2         5 return $self;
67             }
68             sub encode {
69 18 100   18 1 4007 my ($self, @data) = (shift, scalar @_ == 1 ? @{$_[0]} : @_);
  9         29  
70             @data = $self->_parse_headers(@data)
71 18 100       75 if ($self->{parse_headers});
72 18         53 my $stream;
73 18         54 for (@data) {
74 66         107 my $json = eval { $JSON->encode($_) };
  66         454  
75 66 50       137 if ($@) {
76 0 0       0 if ($self->{error_cb}) {
77 0         0 $self->{error_cb}->('encode', $@, $_);
78             } else {
79 0         0 die $@;
80             }
81             } else {
82 66 50       216 $self->{success_cb}->('encode', $json, $_) if $self->{success_cb};
83 66 100       344 $stream .= $json . ($json =~ m/\n$/ ? "" : "\n");
84             }
85             }
86 18         194 return $self->{stream} = $stream;
87             }
88             sub encode_file {
89 5     5 1 164 my ($self, $file) = (shift, shift);
90 5         1005 open my $fh, '>', $file;
91 5         45 print $fh $self->encode(@_);
92 5         291 close $fh;
93 5         50 return $file;
94             }
95             sub decode {
96 160     160 1 58795 my ($self, $string) = @_;
97 160 100 66     889 if (defined $self->{_buffer} && length $self->{_buffer}) {
98 2         4 $string = $self->{_buffer} . $string;
99 2         4 $self->{_buffer} = '';
100             }
101 160         222 my @lines;
102 160         259 my $pos = 0;
103 160         281 my $len = length($string);
104 160         403 while ($pos < $len) {
105 362 100       1590 if ($string =~ m/\G[\s\n]+/gc) {
106 138         225 $pos = pos($string);
107 138         352 next;
108             }
109 224         557 my $remaining = substr($string, $pos);
110 224         621 my ($obj, $chars_consumed, $regex_matched) = $self->_decode_one($remaining);
111 224 100       632 if (defined $obj) {
    50          
112 219         351 push @lines, $obj;
113 219         322 $pos += $chars_consumed;
114             } elsif ($regex_matched) {
115 0 0       0 $pos += $chars_consumed if $chars_consumed > 0;
116 0 0       0 $pos++ if $chars_consumed == 0;
117             } else {
118 5 100       14 if ($remaining =~ /^[\[\{]/) {
119 4         8 $self->{_buffer} = $remaining;
120 4         8 last;
121             }
122 1 50       5 if ($string =~ m/\G[^\[\{]+/gc) {
123 1         2 $pos = pos($string);
124             } else {
125 0         0 $pos++;
126             }
127             }
128 220         1100 pos($string) = $pos;
129             }
130             @lines = $self->_deparse_headers(@lines)
131 160 100       434 if ($self->{parse_headers});
132 160 100       711 return wantarray ? @lines : \@lines;
133             }
134             sub remaining {
135 4     4 1 3234 my ($self) = @_;
136 4   50     20 return $self->{_buffer} // '';
137             }
138             sub clear_buffer {
139 17     17 1 14528 my ($self) = @_;
140 17         29 $self->{_buffer} = '';
141 17         27 return $self;
142             }
143             sub decode_file {
144 2     2 1 1760 my ($self, $file) = (shift, shift);
145 2         71 open my $fh, '<', $file;
146 2         4 my $content = do { local $/; <$fh> };
  2         19  
  2         94  
147 2         22 close $fh;
148 2         9 return $self->decode($content);
149             }
150             sub add_line {
151 2     2 1 2169 my ($self, $line, $fh) = @_;
152 2 50       8 if (defined $fh) {
153 0         0 print $fh $self->encode([$line]);
154             } else {
155 2         4 my $stream = $self->{stream};
156 2         8 my $add = $self->encode([$line]);
157 2         7 $self->{stream} = $stream . $add;
158 2         13 $self->{stream};
159             }
160             }
161             sub clear_stream {
162 0     0 1 0 $_[0]->{stream} = '';
163             }
164             sub get_lines {
165 1     1 1 1220 my ($self, $fh, $lines) = @_;
166 1         2 my @lines;
167 1         3 for (1 .. $lines) {
168 3         4 my $line = $self->get_line($fh);
169 3         4 push @lines, $line;
170 3 100       16 last if eof($fh);
171             }
172 1 50       5 return wantarray ? @lines : \@lines;
173             }
174             sub get_line {
175 17     17 1 12523 my ($self, $fh) = @_;
176 17   100     56 $self->{_line_buffer} //= [];
177 17 100       20 if (@{$self->{_line_buffer}}) {
  17         39  
178 3         6 return shift @{$self->{_line_buffer}};
  3         9  
179             }
180 14         24 my $line = '';
181 14   100     1757 $line .= <$fh> while ($line !~ m/^$LINES/ && !eof($fh));
182 14 100 66     67 return undef if $line eq '' && eof($fh);
183 12         58 my @objects = $self->decode($line);
184 12 50       29 return undef unless @objects;
185 12         19 my $first = shift @objects;
186 12         14 push @{$self->{_line_buffer}}, @objects;
  12         23  
187 12         59 return $first;
188             }
189             sub get_line_at {
190 16     16 1 24461 my ($self, $fh, $index, $seek) = @_;
191 16         48 my $fh_id = fileno($fh);
192 16 100       100 my ($target_line, $target_offset) = $index =~ /:/
193             ? split(/:/, $index)
194             : ($index, 0);
195 16   100     69 $self->{_line_pos} //= {};
196 16   100     54 $self->{_parsed_lines} //= {};
197 16 100       43 if ($seek) {
198 9         91 seek $fh, 0, 0;
199 9         33 $self->{_line_pos}{$fh_id} = 0;
200 9         38 $self->{_parsed_lines}{$fh_id} = {};
201             }
202 16   50     52 $self->{_line_pos}{$fh_id} //= 0;
203 16   50     42 $self->{_parsed_lines}{$fh_id} //= {};
204 16 100       54 if ($target_line < $self->{_line_pos}{$fh_id}) {
205 2         23 seek $fh, 0, 0;
206 2         6 $self->{_line_pos}{$fh_id} = 0;
207 2         12 $self->{_parsed_lines}{$fh_id} = {};
208             }
209 16   100     115 while ($self->{_line_pos}{$fh_id} < $target_line && !eof($fh)) {
210 19         55 <$fh>;
211 19         155 $self->{_line_pos}{$fh_id}++;
212             }
213 16 100       176 return undef if eof($fh);
214 15 50       55 if (exists $self->{_parsed_lines}{$fh_id}{$target_line}) {
215 0         0 my $line_objects = $self->{_parsed_lines}{$fh_id}{$target_line};
216 0 0 0     0 return undef unless $line_objects && @$line_objects > $target_offset;
217 0         0 return $line_objects->[$target_offset];
218             }
219 15         42 my $line = '';
220 15         32 my $start_line = $self->{_line_pos}{$fh_id};
221 15   66     1548 while ($line !~ m/^$LINES/ && !eof($fh)) {
222 29         152 $line .= <$fh>;
223 29         2605 $self->{_line_pos}{$fh_id}++;
224             }
225 15 50       1148 return undef unless $line =~ m/^$LINES/;
226 15         69 my @objects = $self->decode($line);
227 15         69 $self->{_parsed_lines}{$fh_id}{$start_line} = \@objects;
228 15 50       81 return undef unless @objects > $target_offset;
229 15         74 return $objects[$target_offset];
230             }
231             sub get_subset {
232 3     3 1 6929 my ($self, $fh, $offset, $length, $out_file) = @_;
233 3         26 my $in_fh = 1;
234 3 100       39 if (ref $fh ne 'GLOB') {
235 1         3 my $file = $fh;
236 1 50       63 open my $ffh, '<', $file or die "cannot open file: $!";
237 1         4 $fh = $ffh;
238 1         4 $in_fh = 0;
239             }
240 3         20 seek $fh, 0, 0;
241 3         14 my @all_objects;
242 3         5 my $line = '';
243 3         84 while (!eof($fh)) {
244 29   33     45 do { $line .= <$fh> } while ($line !~ m/^$LINES/ && !eof($fh));
  29         2045  
245 29 50       1816 if ($line =~ m/^$LINES/) {
246 29         105 push @all_objects, $self->decode($line);
247 29         176 $line = "";
248             }
249             }
250 3 100       10 if (!$in_fh) {
251 1         16 close $fh;
252             }
253 3         6 my $end = $length;
254 3 50       10 $end = $#all_objects if $end > $#all_objects;
255 3         19 my @subset = @all_objects[$offset .. $end];
256 3 50       10 if ($out_file) {
257 0 0       0 open my $cfh, '>', $out_file or die "cannot open file for writing: $!";
258 0         0 print $cfh $self->encode(\@subset);
259 0         0 close $cfh;
260 0         0 return 1;
261             }
262 3         42 return \@subset;
263             }
264             sub group_lines {
265 7     7 1 27861 my ($self, $fh, $key) = @_;
266 7 50       94 die "Key must be provided for grouping" unless defined $key;
267 7         66 seek $fh, 0, 0;
268 7         16 my (%groups, $line_num, $line);
269 7         19 ($line_num, $line) = (0, '');
270 7         154 while (!eof($fh)) {
271 40         106 my $start_line = $line_num;
272 40   66     3397 while ($line !~ m/^$LINES/ && !eof($fh)) {
273 152         695 $line .= <$fh>;
274 152         12674 $line_num++;
275             }
276 40 50       3432 last unless $line =~ m/^$LINES/;
277 40         177 my @objects = $self->decode($line);
278 40         89 $line = '';
279 40         62 my $obj_offset = 0;
280 40         90 for my $data (@objects) {
281             my $value = ref $key eq 'CODE'
282 19         36 ? do { local $_ = $data; $key->($data) }
  19         61  
283 45 100       123 : $data->{$key};
284 45 100       175 my $index = @objects > 1 ? "$start_line:$obj_offset" : $start_line;
285 45         69 push @{ $groups{$value} }, $index;
  45         166  
286 45         466 $obj_offset++;
287             }
288             }
289 7         107 return \%groups;
290             }
291             sub _decode_one {
292 224     224   406 my ($self, $string) = @_;
293 224 100       720 return (undef, 0, 0) unless $string =~ /^[\[\{]/;
294 223 100       18186 if ($string =~ m/^($LINES)/) {
295 219         658 my $json_str = $1;
296 219         334 my $chars_consumed = length($json_str);
297 219         392 my $struct = eval { $JSON->decode($json_str) };
  219         2074  
298 219 50       546 if ($@) {
299 0         0 return (undef, $chars_consumed, 1);
300             }
301 219 100       523 $self->{success_cb}->('decode', $struct, $json_str) if $self->{success_cb};
302 219         1522 return ($struct, $chars_consumed, 1);
303             }
304 4         13 return (undef, 0, 0);
305             }
306             sub _decode_line {
307 0     0   0 my ($self, $line) = @_;
308 0         0 my $struct = eval { $JSON->decode($line) };
  0         0  
309 0 0       0 if ($@) {
310 0 0       0 if ($self->{error_cb}) {
311 0         0 return $self->{error_cb}->('decode', $@, $line);
312             } else {
313 0         0 die $@;
314             }
315             }
316 0 0       0 return $self->{success_cb}->('decode', $struct, $line) if $self->{success_cb};
317 0         0 return $struct;
318             }
319             sub _parse_headers {
320 2     2   6 my ($self, @data) = @_;
321 2         4 my @headers = @{ $self->{headers} };
  2         6  
322 2 50       6 unless (@headers) {
323 2 50       9 if (ref $data[0] eq 'ARRAY') {
324 0         0 @headers = @{ shift @data };
  0         0  
325             } else {
326 2         5 my %key_map;
327 2         5 for (@data) {
328 5         16 %key_map = (%key_map, %{$_});
  5         27  
329             }
330 2         15 @headers = sort keys %key_map;
331             }
332 2         7 $self->{headers} = \@headers;
333             }
334 2         5 my @body;
335 2         6 for my $d (@data) {
336             push @body, (ref $d eq 'ARRAY')
337             ? $d
338             : [
339             map {
340 5 50       26 $d->{$_}
  15         43  
341             } @headers
342             ];
343             }
344             return (
345 2         11 \@headers,
346             @body
347             );
348             }
349             sub _deparse_headers {
350 1     1   4 my ($self, @data) = @_;
351 1 50       5 return @data unless ref $data[0] eq 'ARRAY';
352 1         1 my @headers = @{ shift @data };
  1         4  
353 1         8 my @body;
354 1         4 for my $d (@data) {
355 2         4 my $i = 0;
356             push @body, (ref $d eq 'HASH')
357             ? $d
358             : {
359             map {
360 2 50       8 $_ => $d->[$i++]
  6         23  
361             } @headers
362             };
363             }
364 1         7 return @body;
365             }
366             1;
367              
368             __END__