File Coverage

blib/lib/JQ/Lite/Util/Transform.pm
Criterion Covered Total %
statement 1074 1210 88.7
branch 702 982 71.4
condition 146 248 58.8
subroutine 100 101 99.0
pod n/a
total 2022 2541 79.5


line stmt bran cond sub pod time code
1             package JQ::Lite::Util;
2              
3 176     176   1657 use strict;
  176         646  
  176         7506  
4 176     176   953 use warnings;
  176         358  
  176         10664  
5              
6 176     176   1139 use JSON::PP ();
  176         394  
  176         4565  
7 176     176   883 use List::Util qw(sum min max);
  176         326  
  176         14748  
8 176     176   1117 use Scalar::Util qw(looks_like_number);
  176         391  
  176         8315  
9 176     176   979 use MIME::Base64 qw(encode_base64 decode_base64);
  176         318  
  176         8982  
10 176     176   1040 use Encode qw(encode is_utf8);
  176         322  
  176         7359  
11 176     176   945 use B ();
  176         317  
  176         3346851  
12              
13             our ($JSON_DECODER, $FROMJSON_DECODER, $TOJSON_ENCODER);
14              
15             sub _apply_tostring {
16 7     7   35 my ($value) = @_;
17              
18 7 100       21 if (!defined $value) {
19 1         6 return 'null';
20             }
21              
22 6 100       26 if (JSON::PP::is_bool($value)) {
23 1 50       67 return $value ? 'true' : 'false';
24             }
25              
26 5 100       47 if (!ref $value) {
27 3         15 return "$value";
28             }
29              
30 2 50 66     18 if (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
31 2         10 return $TOJSON_ENCODER->encode($value);
32             }
33              
34 0         0 return $TOJSON_ENCODER->encode($value);
35             }
36              
37             sub _apply_tojson {
38 21     21   32 my ($value) = @_;
39              
40 21         72 return $TOJSON_ENCODER->encode($value);
41             }
42              
43             sub _apply_fromjson {
44 15     15   20 my ($value) = @_;
45              
46 15 100       20 return undef if !defined $value;
47              
48 14 100       22 if (ref $value eq 'ARRAY') {
49 3         5 return [ map { _apply_fromjson($_) } @$value ];
  10         17  
50             }
51              
52 11 50       15 return $value if ref $value;
53              
54 11         13 my $text = "$value";
55 11         13 my $decoded = eval { $FROMJSON_DECODER->decode($text) };
  11         21  
56              
57 11 100       1366 return $@ ? $value : $decoded;
58             }
59              
60             sub _apply_numeric_function {
61 51     51   127 my ($value, $callback) = @_;
62              
63 51 100       98 return undef if !defined $value;
64              
65 48 100       123 if (JSON::PP::is_bool($value)) {
66 9 100       482 my $numeric = $value ? 1 : 0;
67 9         92 return $callback->($numeric);
68             }
69              
70 39 100       240 if (!ref $value) {
71 27 100       95 return looks_like_number($value) ? $callback->($value) : $value;
72             }
73              
74 12 50       35 if (ref $value eq 'ARRAY') {
75 12         26 return [ map { _apply_numeric_function($_, $callback) } @$value ];
  36         68  
76             }
77              
78 0         0 return $value;
79             }
80              
81             sub _apply_clamp {
82 20     20   44 my ($value, $min, $max) = @_;
83              
84 20 100       50 return undef if !defined $value;
85              
86 19 100       50 if (JSON::PP::is_bool($value)) {
87 1 50       50 my $numeric = $value ? 1 : 0;
88 1         15 return _clamp_scalar($numeric, $min, $max);
89             }
90              
91 18 100       109 if (!ref $value) {
92 14         36 return _clamp_scalar($value, $min, $max);
93             }
94              
95 4 50       16 if (ref $value eq 'ARRAY') {
96 4         9 return [ map { _apply_clamp($_, $min, $max) } @$value ];
  11         43  
97             }
98              
99 0         0 return $value;
100             }
101              
102             sub _normalize_numeric_bound {
103 19     19   36 my ($value) = @_;
104              
105 19 100       41 return undef if !defined $value;
106              
107 18 50       46 if (JSON::PP::is_bool($value)) {
108 0 0       0 return $value ? 1 : 0;
109             }
110              
111 18 50       183 return looks_like_number($value) ? 0 + $value : undef;
112             }
113              
114             sub _clamp_scalar {
115 15     15   33 my ($value, $min, $max) = @_;
116              
117 15 100       44 return $value unless looks_like_number($value);
118              
119 14         25 my $numeric = 0 + $value;
120 14 100 100     54 $numeric = $min if defined $min && $numeric < $min;
121 14 100 100     51 $numeric = $max if defined $max && $numeric > $max;
122              
123 14         56 return $numeric;
124             }
125              
126             sub _apply_to_number {
127 16     16   39 my ($value) = @_;
128              
129 16 100       40 return undef if !defined $value;
130              
131 15 100       42 if (JSON::PP::is_bool($value)) {
132 2 100       67 return $value ? 1 : 0;
133             }
134              
135 13 100       96 if (!ref $value) {
136 7 100       75 return looks_like_number($value) ? 0 + $value : $value;
137             }
138              
139 6 100       21 if (ref $value eq 'ARRAY') {
140 4         10 return [ map { _apply_to_number($_) } @$value ];
  9         31  
141             }
142              
143 2         12 return $value;
144             }
145              
146             sub _extract_numeric_values {
147 34     34   93 my ($values) = @_;
148              
149 34 50       101 return () unless ref $values eq 'ARRAY';
150              
151             return map {
152 96 100       755 JSON::PP::is_bool($_) ? ($_ ? 1 : 0) : 0 + $_;
    100          
153             } grep {
154 34 100 66     107 defined $_ && (JSON::PP::is_bool($_) || (!ref $_ && looks_like_number($_)));
  123   100     1202  
155             } @$values;
156             }
157              
158             sub _normalize_percentile {
159 16     16   37 my ($value) = @_;
160              
161 16 50       38 return undef if !defined $value;
162              
163 16 50       51 if (JSON::PP::is_bool($value)) {
164 0 0       0 $value = $value ? 1 : 0;
165             }
166              
167 16 50       123 return undef if ref $value;
168 16 100       68 return undef unless looks_like_number($value);
169              
170 15         33 my $fraction = 0 + $value;
171              
172 15 100       38 return undef if $fraction != $fraction; # NaN
173 14 100       39 return undef if ($fraction * 0) != ($fraction * 0); # infinity
174              
175 12 100       27 if ($fraction > 1) {
176 9 100       30 $fraction /= 100 if $fraction <= 100;
177             }
178              
179 12 100       32 $fraction = 0 if $fraction < 0;
180 12 100       25 $fraction = 1 if $fraction > 1;
181              
182 12         36 return $fraction;
183             }
184              
185             sub _percentile_value {
186 12     12   30 my ($numbers, $fraction) = @_;
187              
188 12 50       38 return undef unless ref $numbers eq 'ARRAY';
189 12 50       28 return undef unless @$numbers;
190              
191 12 50       59 $fraction = 0 if $fraction < 0;
192 12 50       42 $fraction = 1 if $fraction > 1;
193              
194 12 100       32 return $numbers->[0] if @$numbers == 1;
195              
196 11         30 my $rank = $fraction * (@$numbers - 1);
197 11         23 my $lower_index = int($rank);
198 11 100       30 my $upper_index = $lower_index == @$numbers - 1 ? $lower_index : $lower_index + 1;
199 11         29 my $weight = $rank - $lower_index;
200              
201 11 100       36 return $numbers->[$lower_index] if $upper_index == $lower_index;
202              
203 9         19 my $lower = $numbers->[$lower_index];
204 9         18 my $upper = $numbers->[$upper_index];
205              
206 9         46 return $lower + ($upper - $lower) * $weight;
207             }
208              
209             sub _apply_merge_objects {
210 3     3   5 my ($value) = @_;
211              
212 3 100       10 if (ref $value eq 'ARRAY') {
213 2         3 my %merged;
214 2         4 my $saw_object = 0;
215              
216 2         5 for my $element (@$value) {
217 8 100       21 next unless ref $element eq 'HASH';
218 3         16 %merged = (%merged, %$element);
219 3         7 $saw_object = 1;
220             }
221              
222 2 100       31 return $saw_object ? \%merged : {};
223             }
224              
225 1 50       4 if (ref $value eq 'HASH') {
226 1         5 return { %$value };
227             }
228              
229 0         0 return $value;
230             }
231              
232             sub _to_entries {
233 4     4   8 my ($value) = @_;
234              
235 4 100       12 if (ref $value eq 'HASH') {
236 2         17 return [ map { { key => $_, value => $value->{$_} } } sort keys %$value ];
  4         26  
237             }
238              
239 2 50       9 if (ref $value eq 'ARRAY') {
240 2         7 return [ map { { key => $_, value => $value->[$_] } } 0 .. $#$value ];
  4         19  
241             }
242              
243 0         0 return $value;
244             }
245              
246             sub _is_string_scalar {
247 211     211   422 my ($value) = @_;
248              
249 211 50       459 return 0 if !defined $value;
250 211 50       420 return 0 if ref $value;
251              
252 211         939 my $sv = B::svref_2object(\$value);
253 211         1249 my $flags = $sv->FLAGS;
254              
255 211 100       932 return $flags & B::SVp_POK() ? 1 : 0;
256             }
257              
258             sub _from_entries {
259 11     11   27 my ($value) = @_;
260              
261 11 100       55 die 'from_entries(): argument must be an array' unless ref $value eq 'ARRAY';
262              
263 10         14 my %result;
264             my @numeric_keys;
265 10         16 my $saw_non_numeric_key = 0;
266 10         22 for my $entry (@$value) {
267 15         16 my ($key, $val);
268              
269 15 100       40 if (ref $entry eq 'HASH') {
    100          
270 11 50       23 die 'from_entries(): entry is missing key' if !exists $entry->{key};
271 11 100       43 die 'from_entries(): entry is missing value' if !exists $entry->{value};
272 10         19 ($key, $val) = ($entry->{key}, $entry->{value});
273             }
274             elsif (ref $entry eq 'ARRAY') {
275 3 50       8 die 'from_entries(): entry must have a key and value' if @$entry < 2;
276 3         5 ($key, $val) = @{$entry}[0, 1];
  3         8  
277             }
278             else {
279 1         16 die 'from_entries(): entry must be an object or [key, value] tuple';
280             }
281              
282 13 100 66     43 if (!defined $key || ref $key) {
283 1         14 die 'from_entries(): key must be a string';
284             }
285              
286 12         13 $key = "$key";
287              
288 12 50       23 die 'from_entries(): key must be a string' if !_is_string_scalar($key);
289              
290 12         24 $result{$key} = $val;
291              
292 12 100       38 if ($key =~ /^(?:0|[1-9]\d*)$/) {
293 6         31 push @numeric_keys, 0 + $key;
294             }
295             else {
296 6         12 $saw_non_numeric_key = 1;
297             }
298             }
299              
300 7 100 100     22 if (@numeric_keys && !$saw_non_numeric_key) {
301 3         4 my %seen;
302 3         4 my $max_index = -1;
303 3         5 for my $index (@numeric_keys) {
304 5 50       13 next if $seen{$index}++;
305 5 50       12 $max_index = $index if $index > $max_index;
306             }
307              
308 3 100 66     19 if ($max_index + 1 == scalar(keys %result) && $max_index + 1 == scalar(@numeric_keys)) {
309 2         8 my @array = map { $result{$_} } 0 .. $max_index;
  4         14  
310 2         10 return \@array;
311             }
312             }
313              
314 5         23 return \%result;
315             }
316              
317             sub _apply_with_entries {
318 1     1   3 my ($self, $value, $filter) = @_;
319              
320 1 50 33     33 return $value unless ref $value eq 'HASH' || ref $value eq 'ARRAY';
321              
322 1         10 my $entries = _to_entries($value);
323 1 50       4 return $value unless ref $entries eq 'ARRAY';
324              
325 1         2 my @transformed;
326 1         3 for my $entry (@$entries) {
327 2         7 my @results = $self->run_query(_encode_json($entry), $filter);
328 2         6 for my $result (@results) {
329 1         6 my $normalized = _normalize_entry($result);
330 1 50       7 push @transformed, $normalized if $normalized;
331             }
332             }
333              
334 1         6 return _from_entries(\@transformed);
335             }
336              
337             sub _apply_map_values {
338 8     8   24 my ($self, $value, $filter) = @_;
339              
340 8 50       19 return $value if !defined $value;
341              
342 8 100       27 if (ref $value eq 'HASH') {
343 5         8 my %result;
344 5         37 for my $key (keys %$value) {
345 8         20 my $original = $value->{$key};
346 8         29 my @outputs = $self->run_query(_encode_json($original), $filter);
347 8 100       52 next unless @outputs;
348 5         21 $result{$key} = $outputs[0];
349             }
350 5         26 return \%result;
351             }
352              
353 3 50       13 if (ref $value eq 'ARRAY') {
354 3         7 my @result;
355 3         9 for my $original (@$value) {
356 8 100 66     39 if (ref $original eq 'HASH' || ref $original eq 'ARRAY') {
357 2         11 push @result, _apply_map_values($self, $original, $filter);
358 2         6 next;
359             }
360              
361 6         21 my @outputs = $self->run_query(_encode_json($original), $filter);
362 6 100       20 push @result, $outputs[0] if @outputs;
363             }
364 3         14 return \@result;
365             }
366              
367 0         0 return $value;
368             }
369              
370             sub _apply_walk {
371 13     13   24 my ($self, $value, $filter) = @_;
372              
373 13 100       25 if (ref $value eq 'HASH') {
    100          
374 2         3 my %copy;
375 2         5 for my $key (keys %$value) {
376 4         9 $copy{$key} = _apply_walk($self, $value->{$key}, $filter);
377             }
378 2         4 $value = \%copy;
379             }
380             elsif (ref $value eq 'ARRAY') {
381 3         5 my @copy = map { _apply_walk($self, $_, $filter) } @$value;
  6         7  
382 3         4 $value = \@copy;
383             }
384              
385 13         34 my @results = $self->run_query(_encode_json($value), $filter);
386 13 50       42 return @results ? $results[0] : undef;
387             }
388              
389             sub _apply_recurse {
390 2     2   7 my ($self, $value, $filter) = @_;
391              
392 2         38 my @stack = ($value);
393 2         6 my @outputs;
394              
395 2         6 while (@stack) {
396 9         18 my $current = pop @stack;
397 9         20 push @outputs, $current;
398              
399 9 50       21 next unless defined $current;
400              
401 9         15 my @children;
402 9 100       27 if (defined $filter) {
    100          
    100          
403 4         16 my $json = _encode_json($current);
404 4         1032 @children = $self->run_query($json, $filter);
405             }
406             elsif (ref $current eq 'ARRAY') {
407 1         4 @children = @$current;
408             }
409             elsif (ref $current eq 'HASH') {
410 1         6 @children = map { $current->{$_} } sort keys %$current;
  2         7  
411             }
412              
413 9 100       27 next unless @children;
414              
415 4         11 for my $child (reverse @children) {
416 7         20 push @stack, $child;
417             }
418             }
419              
420 2         49 return @outputs;
421             }
422              
423             sub _apply_delpaths {
424 10     10   28 my ($self, $value, $filter) = @_;
425              
426 10 50       46 return $value if !defined $value;
427 10 50 33     69 return $value if !ref $value || JSON::PP::is_bool($value);
428              
429 10   50     98 $filter //= '';
430 10         44 $filter =~ s/^\s+|\s+$//g;
431 10 50       29 return $value if $filter eq '';
432              
433 10         21 my @paths;
434 10         16 my $decoded_paths = eval { _decode_json($filter) };
  10         52  
435 10 50 33     2272 if (!$@ && defined $decoded_paths) {
436 10 100       44 if (ref $decoded_paths eq 'ARRAY') {
437 9 100       18 if (grep { ref($_) ne 'ARRAY' } @$decoded_paths) {
  12         46  
438 2         37 die 'delpaths(): paths must be an array of path arrays';
439             }
440              
441 7         12 push @paths, map { _validate_path_array($_, 'delpaths') } @$decoded_paths;
  10         32  
442             }
443             else {
444 1         108 die 'delpaths(): paths must be an array of path arrays';
445             }
446             }
447              
448 5 50       11 if (!@paths) {
449 0         0 my @outputs = $self->run_query(_encode_json($value), $filter);
450 0         0 for my $output (@outputs) {
451 0 0       0 next unless defined $output;
452              
453 0 0       0 if (ref $output eq 'ARRAY') {
454 0 0       0 if (grep { ref($_) ne 'ARRAY' } @$output) {
  0         0  
455 0         0 die 'delpaths(): paths must be an array of path arrays';
456             }
457              
458 0         0 push @paths, map { _validate_path_array($_, 'delpaths') } @$output;
  0         0  
459             }
460             else {
461 0         0 die 'delpaths(): paths must be an array of path arrays';
462             }
463             }
464             }
465              
466 5 50       26 return $value unless @paths;
467              
468 5 50       9 if (grep { ref $_ eq 'ARRAY' && !@$_ } @paths) {
  8 100       30  
469 1         6 return undef;
470             }
471              
472 4         12 my $clone = _deep_clone($value);
473 4         1993 my @ordered = _sort_paths_for_deletion(@paths);
474              
475 4         42 for my $path (@ordered) {
476 7 50       19 next unless ref $path eq 'ARRAY';
477 7 50       13 next unless @$path;
478 7         19 _delete_path_inplace($clone, [@$path]);
479             }
480              
481 4         23 return $clone;
482             }
483              
484             sub _sort_paths_for_deletion {
485 4     4   12 my (@paths) = @_;
486              
487             return sort {
488 4         18 my $depth_cmp = @$b <=> @$a;
  3         8  
489 3 100       12 return $depth_cmp if $depth_cmp;
490              
491 2         26 my $prefix_cmp = _path_prefix_key($a) cmp _path_prefix_key($b);
492 2 50       5 return $prefix_cmp if $prefix_cmp;
493              
494 2         8 return _compare_path_segments($b->[-1], $a->[-1]);
495             } @paths;
496             }
497              
498             sub _path_prefix_key {
499 4     4   8 my ($path) = @_;
500              
501 4 50 33     23 return '' if !$path || @$path < 2;
502              
503 0         0 my @segments = @$path[0 .. $#$path - 1];
504 0         0 return join "\x1f", map { _path_segment_key($_) } @segments;
  0         0  
505             }
506              
507             sub _path_segment_key {
508 0     0   0 my ($segment) = @_;
509              
510 0 0       0 return 'undef' if !defined $segment;
511              
512 0 0       0 if (JSON::PP::is_bool($segment)) {
513 0 0       0 return $segment ? 'bool:true' : 'bool:false';
514             }
515              
516 0 0       0 return ref $segment ? 'ref:' . ref($segment) : "scalar:$segment";
517             }
518              
519             sub _compare_path_segments {
520 2     2   6 my ($left, $right) = @_;
521              
522 2 50 33     24 if (_is_numeric_segment($left) && _is_numeric_segment($right)) {
523 2         5 return _numeric_segment_value($left) <=> _numeric_segment_value($right);
524             }
525              
526 0         0 return _path_segment_key($left) cmp _path_segment_key($right);
527             }
528              
529             sub _numeric_segment_value {
530 4     4   23 my ($segment) = @_;
531              
532 4 50       7 if (JSON::PP::is_bool($segment)) {
533 4 100       52 return $segment ? 1 : 0;
534             }
535              
536 0         0 return int($segment);
537             }
538              
539             sub _deep_clone {
540 15     15   25 my ($value) = @_;
541              
542 15 50       26 return $value if !defined $value;
543 15 100 66     71 return $value if !ref $value || JSON::PP::is_bool($value);
544              
545 5         41 my $json = _encode_json($value);
546 5         960 return _decode_json($json);
547             }
548              
549             sub _delete_path_inplace {
550 7     7   46 my ($value, $path) = @_;
551              
552 7 50 66     26 return unless ref $value eq 'HASH' || ref $value eq 'ARRAY';
553 7 50       13 return unless ref $path eq 'ARRAY';
554 7 50       11 return unless @$path;
555              
556 7         14 my @segments = @$path;
557 7         10 my $last = pop @segments;
558              
559 7         9 my $cursor = $value;
560 7         11 for my $segment (@segments) {
561 1 50       3 if (ref $cursor eq 'HASH') {
562 1         5 my $key = _coerce_hash_key($segment);
563 1 50       2 return unless defined $key;
564 1 50       3 return unless exists $cursor->{$key};
565 1         15 $cursor = $cursor->{$key};
566 1         3 next;
567             }
568              
569 0 0       0 if (ref $cursor eq 'ARRAY') {
570 0         0 my $index = _normalize_array_index($segment, scalar @$cursor);
571 0 0       0 return if !defined $index;
572 0         0 $cursor = $cursor->[$index];
573 0         0 next;
574             }
575              
576 0         0 return;
577             }
578              
579 7 100       15 if (ref $cursor eq 'HASH') {
580 3         21 my $key = _coerce_hash_key($last);
581 3 50       20 return unless defined $key;
582 3         5 delete $cursor->{$key};
583 3         6 return;
584             }
585              
586 4 50       10 if (ref $cursor eq 'ARRAY') {
587 4         10 my $index = _normalize_array_index($last, scalar @$cursor);
588 4 50       9 return if !defined $index;
589 4         21 splice @$cursor, $index, 1;
590             }
591             }
592              
593             sub _normalize_array_index {
594 4     4   14 my ($value, $length) = @_;
595              
596 4 50       8 return if !defined $value;
597              
598 4 100       11 if (JSON::PP::is_bool($value)) {
599 2 100       18 $value = $value ? 1 : 0;
600             }
601              
602 4 50       59 return if ref $value;
603              
604 4 50       28 return if $value !~ /^-?\d+$/;
605              
606 4         8 my $index = int($value);
607 4 50       9 $index += $length if $index < 0;
608              
609 4 50 33     15 return if $index < 0 || $index >= $length;
610              
611 4         8 return $index;
612             }
613              
614             sub _normalize_entry {
615 1     1   4 my ($entry) = @_;
616              
617 1 50       5 if (ref $entry eq 'HASH') {
618 1 50       4 return unless exists $entry->{key};
619 1         7 return { key => $entry->{key}, value => $entry->{value} };
620             }
621              
622 0 0       0 if (ref $entry eq 'ARRAY') {
623 0 0       0 return unless @$entry >= 2;
624 0         0 return { key => $entry->[0], value => $entry->[1] };
625             }
626              
627 0         0 return;
628             }
629              
630             sub _apply_coalesce {
631 8     8   27 my ($self, $value, $lhs_expr, $rhs_expr) = @_;
632              
633 8         24 my @lhs_values = _evaluate_coalesce_operand($self, $value, $lhs_expr);
634 8         19 for my $candidate (@lhs_values) {
635 6 100       29 return $candidate if defined $candidate;
636             }
637              
638 5         16 my @rhs_values = _evaluate_coalesce_operand($self, $value, $rhs_expr);
639 5         11 for my $candidate (@rhs_values) {
640 5 50       31 return $candidate if defined $candidate;
641             }
642              
643 0         0 return undef;
644             }
645              
646             sub _evaluate_coalesce_operand {
647 13     13   34 my ($self, $context, $expr) = @_;
648              
649 13 50       34 return () unless defined $expr;
650              
651 13         54 my $copy = $expr;
652 13         67 $copy =~ s/^\s+|\s+$//g;
653 13 50       100 return () if $copy eq '';
654              
655 13         45 while ($copy =~ /^\((.*)\)$/) {
656 0         0 $copy = $1;
657 0         0 $copy =~ s/^\s+|\s+$//g;
658             }
659              
660 13 100       38 if ($copy =~ /^(.*?)\s*\/\/\s*(.+)$/) {
661 1         6 my ($lhs, $rhs) = ($1, $2);
662 1         6 my $result = _apply_coalesce($self, $context, $lhs, $rhs);
663 1         5 return ($result);
664             }
665              
666 12 50       29 if ($copy eq '.') {
667 0         0 return ($context);
668             }
669              
670 12         24 my $decoded = eval { _decode_json($copy) };
  12         38  
671 12 100       3959 if (!$@) {
672 2         8 return ($decoded);
673             }
674              
675 10 50       38 if ($copy =~ /^'(.*)'$/) {
676 0         0 my $text = $1;
677 0         0 $text =~ s/\\'/'/g;
678 0         0 return ($text);
679             }
680              
681 10 50       59 return () unless defined $context;
682              
683 10         40 my $path = $copy;
684 10         42 $path =~ s/^\.//;
685              
686 10         33 return _traverse($context, $path);
687             }
688              
689             sub _traverse {
690 752     752   1911 my ($data, $query) = @_;
691 752         2631 my @steps = split /\./, $query;
692 752         1899 my @stack = ($data);
693              
694 752         1562 for my $step (@steps) {
695 780         2113 my $optional = ($step =~ s/\?$//);
696 780         1395 my @next_stack;
697              
698 780         1417 for my $item (@stack) {
699 789 100       2437 next if !defined $item;
700              
701             # direct index access: [index]
702 788 100       5107 if ($step =~ /^\[(\d+)\]$/) {
    100          
    100          
    100          
703 4         14 my $index = $1;
704 4 50 33     45 if (ref $item eq 'ARRAY' && defined $item->[$index]) {
705 4         15 push @next_stack, $item->[$index];
706             }
707             }
708             # array expansion without key: []
709             elsif ($step eq '[]') {
710 2 50       13 if (ref $item eq 'ARRAY') {
    0          
711 2         7 push @next_stack, @$item;
712             }
713             elsif (ref $item eq 'HASH') {
714 0         0 push @next_stack, values %$item;
715             }
716             }
717             # index access: key[index]
718             elsif ($step =~ /^(.*?)\[(\d+)\]$/) {
719 19         102 my ($key, $index) = ($1, $2);
720 19 50 33     143 if (ref $item eq 'HASH' && exists $item->{$key}) {
721 19         67 my $val = $item->{$key};
722 19 50 33     567 push @next_stack, $val->[$index]
723             if ref $val eq 'ARRAY' && defined $val->[$index];
724             }
725             }
726             # array expansion: key[]
727             elsif ($step =~ /^(.*?)\[\]$/) {
728 51         192 my $key = $1;
729 51 100 66     557 if (ref $item eq 'HASH' && exists $item->{$key}) {
    50          
730 49         122 my $val = $item->{$key};
731 49 100       161 if (ref $val eq 'ARRAY') {
    50          
732 48         174 push @next_stack, @$val;
733             }
734             elsif (ref $val eq 'HASH') {
735 1         5 push @next_stack, values %$val;
736             }
737             }
738             elsif (ref $item eq 'ARRAY') {
739 0         0 for my $sub (@$item) {
740 0 0 0     0 if (ref $sub eq 'HASH' && exists $sub->{$key}) {
741 0         0 my $val = $sub->{$key};
742 0 0       0 if (ref $val eq 'ARRAY') {
    0          
743 0         0 push @next_stack, @$val;
744             }
745             elsif (ref $val eq 'HASH') {
746 0         0 push @next_stack, values %$val;
747             }
748             }
749             }
750             }
751             }
752             # standard access: key
753             else {
754 712 100 100     4214 if (ref $item eq 'HASH' && exists $item->{$step}) {
    50          
755 681         2009 push @next_stack, $item->{$step};
756             }
757             elsif (ref $item eq 'ARRAY') {
758 0         0 for my $sub (@$item) {
759 0 0 0     0 if (ref $sub eq 'HASH' && exists $sub->{$step}) {
760 0         0 push @next_stack, $sub->{$step};
761             }
762             }
763             }
764             }
765             }
766              
767             # allow empty results if optional
768 780         1896 @stack = @next_stack;
769 780 100 100     2634 last if !@stack && !$optional;
770             }
771              
772 752         2671 return @stack;
773             }
774              
775             sub _evaluate_condition {
776 70     70   204 my ($item, $cond) = @_;
777              
778             # support for numeric expressions like: select(.a + 5 > 10)
779 70 100       494 if ($cond =~ /^\s*(\.\w+)\s*([\+\-\*\/%])\s*(-?\d+(?:\.\d+)?)\s*(==|!=|>=|<=|>|<)\s*(-?\d+(?:\.\d+)?)\s*$/) {
780 8         59 my ($path, $op1, $rhs1, $cmp, $rhs2) = ($1, $2, $3, $4, $5);
781 8         38 my @values = _traverse($item, substr($path, 1));
782 8         23 my $lhs = $values[0];
783            
784 8 50 33     63 return 0 unless defined $lhs && $lhs =~ /^-?\d+(?:\.\d+)?$/;
785            
786 8         23 my $expr = _apply_numeric_operator($lhs, $op1, $rhs1);
787 8         34 return _compare_numeric_values($expr, $cmp, $rhs2);
788             }
789              
790             # support for multiple conditions: split and evaluate recursively
791 62 50       308 if ($cond =~ /\s+and\s+/i) {
792 0         0 my @conds = split /\s+and\s+/i, $cond;
793 0         0 for my $c (@conds) {
794 0 0       0 return 0 unless _evaluate_condition($item, $c);
795             }
796 0         0 return 1;
797             }
798 62 50       221 if ($cond =~ /\s+or\s+/i) {
799 0         0 my @conds = split /\s+or\s+/i, $cond;
800 0         0 for my $c (@conds) {
801 0 0       0 return 1 if _evaluate_condition($item, $c);
802             }
803 0         0 return 0;
804             }
805              
806             # support for the contains operator: select(.tags contains "perl")
807 62 100       209 if ($cond =~ /^\s*\.(.+?)\s+contains\s+"(.*?)"\s*$/) {
808 3         23 my ($path, $want) = ($1, $2);
809 3         9 my @vals = _traverse($item, $path);
810              
811 3         11 for my $val (@vals) {
812 3 100 33     11 if (ref $val eq 'ARRAY') {
    50          
813 2 100       5 return 1 if grep { $_ eq $want } @$val;
  6         15  
814             }
815             elsif (!ref $val && index($val, $want) >= 0) {
816 1         4 return 1;
817             }
818             }
819 1         6 return 0;
820             }
821              
822             # support for the has operator: select(.meta has "key")
823 59 100       219 if ($cond =~ /^\s*\.(.+?)\s+has\s+"(.*?)"\s*$/) {
824 2         6 my ($path, $key) = ($1, $2);
825 2         5 my @vals = _traverse($item, $path);
826              
827 2         2 for my $val (@vals) {
828 2 100 66     8 if (ref $val eq 'HASH' && exists $val->{$key}) {
829 1         4 return 1;
830             }
831             }
832 1         3 return 0;
833             }
834              
835             # support for the match operator (with optional 'i' flag)
836 57 100       270 if ($cond =~ /^\s*\.(.+?)\s+match\s+"(.*?)"(i?)\s*$/) {
837 16         88 my ($path, $pattern, $ignore_case) = ($1, $2, $3);
838 16         51 my ($re, $error) = _build_regex($pattern, $ignore_case);
839 16 100       40 if ($error) {
840 1         10 $error =~ s/[\r\n]+$//;
841 1         24 die "match(): invalid regular expression - $error";
842             }
843              
844 15         43 my @vals = _traverse($item, $path);
845 15         31 for my $val (@vals) {
846 15 50       36 next if ref $val;
847 15 100       127 return 1 if $val =~ $re;
848             }
849 12         77 return 0;
850             }
851              
852             # support for the =~ operator: select(. =~ "pattern")
853 41 100       2554 if ($cond =~ /^\s*\.(.+?)\s*=~\s*"(.*?)"(i?)\s*$/) {
854 2         8 my ($path, $pattern, $ignore_case) = ($1, $2, $3);
855 2         7 my ($re, $error) = _build_regex($pattern, $ignore_case);
856 2 100       8 if ($error) {
857 1         6 $error =~ s/[\r\n]+$//;
858 1         13 die "=~: invalid regular expression - $error";
859             }
860              
861 1         6 my @vals = _traverse($item, $path);
862 1         3 for my $val (@vals) {
863 0 0       0 next if ref $val;
864 0 0       0 return 1 if $val =~ $re;
865             }
866              
867 1         7 return 0;
868             }
869            
870             # pattern for a single condition
871 39 100       287 if ($cond =~ /^\s*\.(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+?)\s*$/) {
872 34         187 my ($path, $op, $value_raw) = ($1, $2, $3);
873              
874 34         73 my $value;
875 34 100       308 if ($value_raw =~ /^"(.*)"$/) {
    50          
    50          
    100          
876 2         4 $value = $1;
877             } elsif ($value_raw eq 'true') {
878 0         0 $value = JSON::PP::true;
879             } elsif ($value_raw eq 'false') {
880 0         0 $value = JSON::PP::false;
881             } elsif ($value_raw =~ /^-?\d+(?:\.\d+)?$/) {
882 29         78 $value = 0 + $value_raw;
883             } else {
884 3         7 $value = $value_raw;
885             }
886              
887 34         146 my @values = _traverse($item, $path);
888 34 100       104 return 0 unless @values;
889              
890 31         59 for my $field_val (@values) {
891 31 50       83 next unless defined $field_val;
892              
893 31   66     316 my $is_number = (!ref($field_val) && $field_val =~ /^-?\d+(?:\.\d+)?$/)
894             && (!ref($value) && $value =~ /^-?\d+(?:\.\d+)?$/);
895              
896 31 50       134 if ($op eq '==') {
    100          
    50          
897 0 0       0 return 1 if $is_number ? ($field_val == $value) : ($field_val eq $value);
    0          
898             } elsif ($op eq '!=') {
899 2 50       28 return 1 if $is_number ? ($field_val != $value) : ($field_val ne $value);
    100          
900             } elsif ($is_number) {
901             # perform numeric comparisons only when applicable
902 29 100       83 if ($op eq '>') {
    50          
    0          
    0          
903 22 100       103 return 1 if $field_val > $value;
904             } elsif ($op eq '>=') {
905 7 100       52 return 1 if $field_val >= $value;
906             } elsif ($op eq '<') {
907 0 0       0 return 1 if $field_val < $value;
908             } elsif ($op eq '<=') {
909 0 0       0 return 1 if $field_val <= $value;
910             }
911             }
912             }
913             }
914              
915 22         72 return 0;
916             }
917              
918             sub _apply_numeric_operator {
919 8     8   24 my ($lhs, $operator, $rhs) = @_;
920              
921 8 100       30 return $lhs + $rhs if $operator eq '+';
922 4 50       13 return $lhs - $rhs if $operator eq '-';
923 4 50       12 return $lhs * $rhs if $operator eq '*';
924 4 50 33     46 return undef if ($operator eq '/' || $operator eq '%') && $rhs == 0;
      33        
925 0 0       0 return $lhs / $rhs if $operator eq '/';
926 0 0       0 return $lhs % $rhs if $operator eq '%';
927              
928 0         0 return undef;
929             }
930              
931             sub _compare_numeric_values {
932 8     8   20 my ($lhs, $operator, $rhs) = @_;
933              
934 8 100 66     41 return 0 unless defined $lhs && defined $rhs;
935              
936 4 50       9 return $lhs == $rhs if $operator eq '==';
937 4 50       12 return $lhs != $rhs if $operator eq '!=';
938 4 50       9 return $lhs >= $rhs if $operator eq '>=';
939 4 50       8 return $lhs <= $rhs if $operator eq '<=';
940 4 50       31 return $lhs > $rhs if $operator eq '>';
941 0 0       0 return $lhs < $rhs if $operator eq '<';
942              
943 0         0 return 0;
944             }
945              
946             sub _smart_cmp {
947             return sub {
948 50     50   147 my ($a, $b) = @_;
949              
950 50         271 my $num_a = ($a =~ /^-?\d+(?:\.\d+)?$/);
951 50         158 my $num_b = ($b =~ /^-?\d+(?:\.\d+)?$/);
952              
953 50 100 66     215 if ($num_a && $num_b) {
954 36         185 return $a <=> $b;
955             } else {
956 14         47 return "$a" cmp "$b"; # explicitly perform string comparison
957             }
958 26     26   253 };
959             }
960              
961             sub _extreme_by {
962 6     6   19 my ($array_ref, $key_path, $use_entire_item, $mode) = @_;
963              
964 6 50       49 return undef unless ref $array_ref eq 'ARRAY';
965 6 50       27 return undef unless @$array_ref;
966              
967 6         22 my $cmp = _smart_cmp();
968 6         15 my ($best_item, $best_key);
969              
970 6         17 for my $element (@$array_ref) {
971 20         45 my $candidate = _extract_extreme_key($element, $key_path, $use_entire_item);
972 20 100       51 next unless defined $candidate;
973              
974 17 100       33 if (!defined $best_item) {
975 5         32 ($best_item, $best_key) = ($element, $candidate);
976 5         13 next;
977             }
978              
979 12         54 my $comparison = $cmp->($candidate, $best_key);
980 12 100 100     96 if (($mode eq 'max' && $comparison > 0)
      100        
      100        
981             || ($mode eq 'min' && $comparison < 0)) {
982 5         16 ($best_item, $best_key) = ($element, $candidate);
983             }
984             }
985              
986 6 100       67 return defined $best_item ? $best_item : undef;
987             }
988              
989             sub _extract_extreme_key {
990 20     20   46 my ($element, $key_path, $use_entire_item) = @_;
991              
992 20 100       55 my @values = $use_entire_item ? ($element) : _traverse($element, $key_path);
993 20 100       75 return undef unless @values;
994              
995 17         60 my $value = $values[0];
996 17         37 return _value_to_comparable($value);
997             }
998              
999             sub _value_to_comparable {
1000 17     17   34 my ($value) = @_;
1001              
1002 17 50       35 return undef unless defined $value;
1003              
1004 17 50       57 if (JSON::PP::is_bool($value)) {
1005 0 0       0 return $value ? 1 : 0;
1006             }
1007              
1008 17 50       125 if (!ref $value) {
1009 17         46 return $value;
1010             }
1011              
1012 0 0 0     0 if (ref($value) eq 'HASH' || ref($value) eq 'ARRAY') {
1013 0         0 return _encode_json($value);
1014             }
1015              
1016 0         0 return undef;
1017             }
1018              
1019             sub _normalize_path_argument {
1020 25     25   78 my ($raw_path) = @_;
1021              
1022 25 50       53 $raw_path = '' unless defined $raw_path;
1023 25         94 $raw_path =~ s/^\s+|\s+$//g;
1024 25         59 $raw_path =~ s/^['"](.*)['"]$/$1/;
1025              
1026 25   66     93 my $use_entire_item = ($raw_path eq '' || $raw_path eq '.');
1027 25         40 my $key_path = $raw_path;
1028 25 100       91 $key_path =~ s/^\.// unless $use_entire_item;
1029              
1030 25         101 return ($key_path, $use_entire_item);
1031             }
1032              
1033             sub _project_numeric_values {
1034 19     19   25 my ($element, $key_path, $use_entire_item) = @_;
1035              
1036 19 100       43 my @values = $use_entire_item
1037             ? ($element)
1038             : _traverse($element, $key_path);
1039              
1040 19         20 my @numbers;
1041 19         30 for my $value (@values) {
1042 19 50       24 next unless defined $value;
1043              
1044 19 100       30 if (JSON::PP::is_bool($value)) {
1045 3 100       58 push @numbers, $value ? 1 : 0;
1046 3         20 next;
1047             }
1048              
1049 16 50       82 next if ref $value;
1050 16 100       33 next unless looks_like_number($value);
1051              
1052 12         18 push @numbers, 0 + $value;
1053             }
1054              
1055 19         30 return @numbers;
1056             }
1057              
1058             sub _uniq {
1059 1     1   3 my %seen;
1060 1         3 return grep { !$seen{_key($_)}++ } @_;
  4         10  
1061             }
1062              
1063             # generate a unique key for hash, array, or scalar values
1064             sub _key {
1065 81     81   152 my ($val) = @_;
1066 81 100       217 if (ref $val eq 'HASH') {
    100          
1067 6         18 return join(",", sort map { "$_=" . _key($val->{$_}) } keys %$val);
  12         28  
1068             } elsif (ref $val eq 'ARRAY') {
1069 6         12 return join(",", map { _key($_) } @$val);
  12         24  
1070             } else {
1071 69         213 return "$val";
1072             }
1073             }
1074              
1075             sub _group_by {
1076 5     5   7 my ($array_ref, $path) = @_;
1077 5 100       21 die 'group_by(): input must be an array' unless ref $array_ref eq 'ARRAY';
1078              
1079 4         6 my ($key_path, $use_entire_item) = _normalize_path_argument($path);
1080              
1081 4         6 my @entries;
1082 4         5 my $index = 0;
1083 4         5 for my $item (@$array_ref) {
1084 12         12 my $key_value;
1085 12 50       13 if ($use_entire_item) {
1086 0         0 $key_value = $item;
1087             } else {
1088 12         22 my @values = _traverse($item, $key_path);
1089 12 50       43 $key_value = @values ? $values[0] : undef;
1090             }
1091              
1092 12 100       21 my $signature = defined $key_value ? _key($key_value) : "\0__JQ_LITE_UNDEF__";
1093 12         34 push @entries, {
1094             item => $item,
1095             signature => $signature,
1096             index => $index++,
1097             };
1098             }
1099              
1100 4         8 my $cmp = _smart_cmp();
1101             my @sorted = sort {
1102 4         12 my $order = $cmp->($a->{signature}, $b->{signature});
  12         18  
1103 12 100       22 $order = $a->{index} <=> $b->{index} if $order == 0;
1104             $order;
1105             } @entries;
1106              
1107 4         10 my @groups;
1108             my $current_signature;
1109 4         4 for my $entry (@sorted) {
1110 12 100 100     26 if (!defined $current_signature || $entry->{signature} ne $current_signature) {
1111 8         24 push @groups, [];
1112 8         14 $current_signature = $entry->{signature};
1113             }
1114 12         11 push @{ $groups[-1] }, $entry->{item};
  12         19  
1115             }
1116              
1117 4         30 return \@groups;
1118             }
1119              
1120             sub _flatten_all {
1121 8     8   13 my ($value) = @_;
1122              
1123 8 50       22 return $value unless ref $value eq 'ARRAY';
1124              
1125 8         13 my @flattened;
1126 8         14 for my $item (@$value) {
1127 17 100       27 if (ref $item eq 'ARRAY') {
1128 6         15 my $flattened = _flatten_all($item);
1129 6 50       30 if (ref $flattened eq 'ARRAY') {
1130 6         13 push @flattened, @$flattened;
1131             } else {
1132 0         0 push @flattened, $flattened;
1133             }
1134             } else {
1135 11         22 push @flattened, $item;
1136             }
1137             }
1138              
1139 8         44 return \@flattened;
1140             }
1141              
1142             sub _flatten_depth {
1143 22     22   53 my ($value, $depth) = @_;
1144              
1145 22 50       64 return $value unless ref $value eq 'ARRAY';
1146 22 100       46 return $value if $depth <= 0;
1147              
1148 8         9 my @flattened;
1149 8         15 for my $item (@$value) {
1150 22 100       40 if (ref $item eq 'ARRAY') {
1151 15         44 my $flattened = _flatten_depth($item, $depth - 1);
1152 15 50       27 if (ref $flattened eq 'ARRAY') {
1153 15         32 push @flattened, @$flattened;
1154             } else {
1155 0         0 push @flattened, $flattened;
1156             }
1157             } else {
1158 7         12 push @flattened, $item;
1159             }
1160             }
1161              
1162 8         32 return \@flattened;
1163             }
1164              
1165             sub _apply_string_predicate {
1166 37     37   94 my ($value, $needle, $mode) = @_;
1167              
1168 37 100       124 if (ref $value eq 'ARRAY') {
1169 6         19 return [ map { _apply_string_predicate($_, $needle, $mode) } @$value ];
  19         89  
1170             }
1171              
1172 31 100       84 return JSON::PP::false if !_is_string_scalar($needle);
1173              
1174 29         90 return _string_predicate_result($value, $needle, $mode);
1175             }
1176              
1177             sub _string_predicate_result {
1178 29     29   71 my ($value, $needle, $mode) = @_;
1179              
1180 29 100       69 return JSON::PP::false if !defined $value;
1181 28 100       61 return JSON::PP::false if ref $value;
1182 27 100       57 return JSON::PP::false if !_is_string_scalar($value);
1183              
1184 24   50     94 $needle //= '';
1185 24         42 my $len = length $needle;
1186              
1187 24 100       66 if ($mode eq 'start') {
1188 11 100 100     110 return JSON::PP::true if $len == 0 || index($value, $needle) == 0;
1189 5         23 return JSON::PP::false;
1190             }
1191              
1192 13 50       36 if ($mode eq 'end') {
1193 13 100       44 return JSON::PP::true if $len == 0;
1194 9 50       25 return JSON::PP::false if length($value) < $len;
1195 9 100       75 return JSON::PP::true if substr($value, -$len) eq $needle;
1196 5         19 return JSON::PP::false;
1197             }
1198              
1199 0         0 return JSON::PP::false;
1200             }
1201              
1202             sub _apply_test {
1203 16     16   32 my ($value, $pattern, $flags) = @_;
1204              
1205 16         33 my ($regex, $error) = _build_regex($pattern, $flags);
1206 16 100       31 if ($error) {
1207 3         17 $error =~ s/[\r\n]+$//;
1208 3         47 die "test(): invalid regular expression - $error";
1209             }
1210              
1211 13         29 return _test_against_regex($value, $regex);
1212             }
1213              
1214             sub _apply_match {
1215 10     10   25 my ($value, $pattern, $flags) = @_;
1216              
1217 10         28 my ($regex, $error) = _build_regex($pattern, $flags);
1218 10 100       22 if ($error) {
1219 3         17 $error =~ s/[\r\n]+$//;
1220 3         49 die "match(): invalid regular expression - $error";
1221             }
1222              
1223 7         16 return _match_against_regex($value, $regex);
1224             }
1225              
1226             sub _test_against_regex {
1227 26     26   36 my ($value, $regex) = @_;
1228              
1229 26 100       44 if (ref $value eq 'ARRAY') {
1230 6         11 return [ map { _test_against_regex($_, $regex) } @$value ];
  13         36  
1231             }
1232              
1233 20 100       32 return JSON::PP::false if !defined $value;
1234              
1235 19 100       42 if (JSON::PP::is_bool($value)) {
1236 2 100       19 $value = $value ? 'true' : 'false';
1237             }
1238              
1239 19 100       103 return JSON::PP::false if ref $value;
1240              
1241 18 100       98 return $value =~ $regex ? JSON::PP::true : JSON::PP::false;
1242             }
1243              
1244             sub _match_against_regex {
1245 7     7   12 my ($value, $regex) = @_;
1246              
1247 7 50       36 if (ref $value eq 'ARRAY') {
1248 0         0 return [ map { _match_against_regex($_, $regex) } @$value ];
  0         0  
1249             }
1250              
1251 7 50       14 return undef if !defined $value;
1252              
1253 7 50       19 if (JSON::PP::is_bool($value)) {
1254 0 0       0 $value = $value ? 'true' : 'false';
1255             }
1256              
1257 7 50       54 return undef if ref $value;
1258              
1259 7         9 my $text = "$value";
1260 7 100       53 return undef unless $text =~ $regex;
1261              
1262 4         12 my $offset = $-[0];
1263 4         13 my $length = $+[0] - $-[0];
1264 4         9 my $string = substr($text, $offset, $length);
1265              
1266 4         5 my @captures;
1267 4         9 my $capture_count = $#-;
1268 4         12 for my $index (1 .. $capture_count) {
1269 2 50 33     11 if (defined $-[$index] && $-[$index] >= 0) {
1270 2         7 my $capture_offset = $-[$index];
1271 2         6 my $capture_length = $+[$index] - $-[$index];
1272 2         5 my $capture_string = substr($text, $capture_offset, $capture_length);
1273 2         9 push @captures, {
1274             offset => $capture_offset,
1275             length => $capture_length,
1276             string => $capture_string,
1277             };
1278             } else {
1279 0         0 push @captures, {
1280             offset => undef,
1281             length => undef,
1282             string => undef,
1283             };
1284             }
1285             }
1286              
1287             return {
1288 4         33 offset => $offset,
1289             length => $length,
1290             string => $string,
1291             captures => \@captures,
1292             };
1293             }
1294              
1295             sub _build_regex {
1296 44     44   81 my ($pattern, $flags) = @_;
1297              
1298 44 50       106 $pattern = '' unless defined $pattern;
1299 44 50       89 $flags = '' unless defined $flags;
1300              
1301 44         78 my %allowed = map { $_ => 1 } qw(i m s x);
  176         425  
1302 44         83 my $modifiers = '';
1303 44         118 for my $flag (split //, $flags) {
1304 12 100       40 return (undef, "unknown regex flag '$flag'") unless $allowed{$flag};
1305 10 50       25 next if index($modifiers, $flag) >= 0;
1306 10         20 $modifiers .= $flag;
1307             }
1308              
1309 42         73 my $escaped = $pattern;
1310 42         86 $escaped =~ s/'/\\'/g;
1311              
1312 42         3971 my $regex = eval "qr'$escaped'$modifiers";
1313 42 100       223 if ($@) {
1314 6         27 return (undef, $@);
1315             }
1316              
1317 36         142 return ($regex, undef);
1318             }
1319              
1320             sub _parse_string_argument {
1321 99     99   306 my ($raw) = @_;
1322              
1323 99 50       309 return '' if !defined $raw;
1324              
1325 99         184 my $parsed = eval { _decode_json($raw) };
  99         302  
1326 99 50       12656 if (!$@) {
1327 99 50       243 $parsed = '' if !defined $parsed;
1328 99         284 return $parsed;
1329             }
1330              
1331 0         0 $raw =~ s/^\s+|\s+$//g;
1332 0         0 $raw =~ s/^['"]//;
1333 0         0 $raw =~ s/['"]$//;
1334 0         0 return $raw;
1335             }
1336              
1337             sub _parse_literal_argument {
1338 28     28   163 my ($raw) = @_;
1339              
1340 28 50       82 return undef if !defined $raw;
1341              
1342 28         60 my $parsed = eval { _decode_json($raw) };
  28         101  
1343 28 50       8217 return $parsed if !$@;
1344              
1345 0         0 $raw =~ s/^\s+|\s+$//g;
1346 0         0 $raw =~ s/^['"]//;
1347 0         0 $raw =~ s/['"]$//;
1348 0         0 return $raw;
1349             }
1350              
1351             sub _apply_csv {
1352 3     3   9 my ($value) = @_;
1353              
1354 3 100       13 if (ref $value eq 'ARRAY') {
1355 2         5 my @fields = map { _format_csv_field($_) } @$value;
  6         19  
1356 2         14 return join(',', @fields);
1357             }
1358              
1359 1         5 return _format_csv_field($value);
1360             }
1361              
1362             sub _apply_tsv {
1363 3     3   15 my ($value) = @_;
1364              
1365 3 100       6 if (ref $value eq 'ARRAY') {
1366 2         2 my @fields = map { _format_tsv_field($_) } @$value;
  6         18  
1367 2         8 return join("\t", @fields);
1368             }
1369              
1370 1         4 return _format_tsv_field($value);
1371             }
1372              
1373             sub _apply_base64 {
1374 6     6   12 my ($value) = @_;
1375              
1376 6         5 my $text;
1377              
1378 6 100 66     24 if (!defined $value) {
    50          
    100          
    50          
1379 1         2 $text = 'null';
1380             }
1381             elsif (JSON::PP::is_bool($value)) {
1382 0 0       0 $text = $value ? 'true' : 'false';
1383             }
1384             elsif (!ref $value) {
1385 2         13 $text = "$value";
1386             }
1387             elsif (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
1388 3         33 $text = _encode_json($value);
1389             }
1390             else {
1391 0         0 $text = "$value";
1392             }
1393              
1394 6         324 return encode_base64($text, '');
1395             }
1396              
1397             sub _apply_base64d {
1398 7     7   15 my ($value) = @_;
1399              
1400 7         438 my $text;
1401              
1402 7 50 0     31 if (!defined $value) {
    50          
    50          
    0          
1403 0         0 $text = '';
1404             }
1405             elsif (JSON::PP::is_bool($value)) {
1406 0 0       0 $text = $value ? 'true' : 'false';
1407             }
1408             elsif (!ref $value) {
1409 7 100       60 die '@base64d(): input must be base64 text'
1410             if !_is_string_scalar($value);
1411 6         11 $text = "$value";
1412             }
1413             elsif (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
1414 0         0 $text = _encode_json($value);
1415             }
1416             else {
1417 0         0 $text = "$value";
1418             }
1419              
1420 6         14 $text =~ s/\s+//g;
1421              
1422 6 100       52 die '@base64d(): input must be base64 text'
1423             if length($text) % 4 != 0;
1424              
1425 4 100       62 die '@base64d(): input must be base64 text'
1426             if $text !~ /^[A-Za-z0-9+\/]*={0,2}$/;
1427              
1428 3 50 66     9 die '@base64d(): input must be base64 text'
1429             if $text =~ /=/ && $text !~ /=+$/;
1430              
1431 3         10 my $decoded = decode_base64($text);
1432 3         8 my $reencoded = encode_base64($decoded, '');
1433              
1434 3 50       5 die '@base64d(): input must be base64 text'
1435             if $reencoded ne $text;
1436              
1437 3         9 return $decoded;
1438             }
1439              
1440             sub _apply_uri {
1441 8     8   19 my ($value) = @_;
1442              
1443 8         16 my $text;
1444              
1445 8 100 33     73 if (!defined $value) {
    50          
    100          
    50          
1446 1         2 $text = 'null';
1447             }
1448             elsif (JSON::PP::is_bool($value)) {
1449 0 0       0 $text = $value ? 'true' : 'false';
1450             }
1451             elsif (!ref $value) {
1452 5         44 $text = "$value";
1453             }
1454             elsif (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
1455 2         48 $text = _encode_json($value);
1456             }
1457             else {
1458 0         0 $text = "$value";
1459             }
1460              
1461 8 100       335 my $encoded = is_utf8($text, 1)
1462             ? encode('UTF-8', $text)
1463             : $text;
1464 8         195 $encoded =~ s/([^A-Za-z0-9\-._~])/sprintf('%%%02X', ord($1))/ge;
  76         282  
1465 8         34 return $encoded;
1466             }
1467              
1468             sub _format_csv_field {
1469 7     7   40 my ($value) = @_;
1470              
1471 7 50       19 return '' if !defined $value;
1472              
1473 7 50       23 if (JSON::PP::is_bool($value)) {
1474 0 0       0 return $value ? 'true' : 'false';
1475             }
1476              
1477 7 50 33     81 if (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
1478 0         0 my $encoded = _encode_json($value);
1479 0         0 return _quote_csv_text($encoded);
1480             }
1481              
1482 7 50       16 if (ref $value) {
1483 0         0 my $stringified = "$value";
1484 0         0 return _quote_csv_text($stringified);
1485             }
1486              
1487 7 100       18 if (_is_unquoted_csv_number($value)) {
1488 1         6 return "$value";
1489             }
1490              
1491 6         15 my $text = "$value";
1492 6         14 return _quote_csv_text($text);
1493             }
1494              
1495             sub _format_tsv_field {
1496 7     7   8 my ($value) = @_;
1497              
1498 7 50       13 return '' if !defined $value;
1499              
1500 7 100       14 if (JSON::PP::is_bool($value)) {
1501 1 50       33 return $value ? 'true' : 'false';
1502             }
1503              
1504 6 100 66     35 if (ref $value eq 'ARRAY' || ref $value eq 'HASH') {
1505 1         3 my $encoded = _encode_json($value);
1506 1         95 return _escape_tsv_text($encoded);
1507             }
1508              
1509 5 50       7 if (ref $value) {
1510 0         0 my $stringified = "$value";
1511 0         0 return _escape_tsv_text($stringified);
1512             }
1513              
1514 5         6 my $text = "$value";
1515 5         6 return _escape_tsv_text($text);
1516             }
1517              
1518             sub _quote_csv_text {
1519 6     6   12 my ($text) = @_;
1520              
1521 6 50       15 $text = '' unless defined $text;
1522 6         18 $text =~ s/"/""/g;
1523 6         58 return '"' . $text . '"';
1524             }
1525              
1526             sub _escape_tsv_text {
1527 6     6   7 my ($text) = @_;
1528              
1529 6 50       13 $text = '' unless defined $text;
1530 6         8 $text =~ s/\\/\\\\/g;
1531 6         9 $text =~ s/\t/\\t/g;
1532 6         7 $text =~ s/\r/\\r/g;
1533 6         6 $text =~ s/\n/\\n/g;
1534 6         11 return $text;
1535             }
1536              
1537             sub _is_unquoted_csv_number {
1538 7     7   15 my ($value) = @_;
1539              
1540 7 50       15 return 0 if !defined $value;
1541 7 50       16 return 0 if ref $value;
1542              
1543 7         32 my $sv = B::svref_2object(\$value);
1544 7         37 my $flags = $sv->FLAGS;
1545              
1546 7 100       44 return ($flags & (B::SVp_IOK() | B::SVp_NOK())) ? 1 : 0;
1547             }
1548              
1549             sub _apply_split {
1550 29     29   56 my ($value, $separator) = @_;
1551              
1552 29 100       66 if (ref $value eq 'ARRAY') {
1553 4         5 my @parts;
1554              
1555 4         11 for my $element (@$value) {
1556 11 100       19 if (JSON::PP::is_bool($element)) {
1557 5 100       54 my $stringified = $element ? 'true' : 'false';
1558 5         51 my $result = _apply_split($stringified, $separator);
1559 5 50       11 push @parts, ref($result) eq 'ARRAY' ? @$result : $result;
1560 5         9 next;
1561             }
1562              
1563 6         30 my $result = _apply_split($element, $separator);
1564 6 100       16 push @parts, ref($result) eq 'ARRAY' ? @$result : $result;
1565             }
1566              
1567 4         10 return \@parts;
1568             }
1569              
1570 25 100       50 return undef if !defined $value;
1571 22 100       49 if (JSON::PP::is_bool($value)) {
    100          
1572 1 50       9 $value = $value ? 'true' : 'false';
1573             }
1574             elsif (ref $value) {
1575 1         5 return $value;
1576             }
1577              
1578 21 50       147 $separator = '' unless defined $separator;
1579              
1580 21 100       34 if ($separator eq '') {
1581 3         18 return [ split(//, $value) ];
1582             }
1583              
1584 18         47 my $pattern = quotemeta $separator;
1585 18         232 my @parts = split /$pattern/, $value, -1;
1586 18         92 return [ @parts ];
1587             }
1588              
1589             sub _apply_explode {
1590 13     13   31 my ($value) = @_;
1591              
1592 13 100       36 if (ref $value eq 'ARRAY') {
1593 2         8 return [ map { _apply_explode($_) } @$value ];
  8         54  
1594             }
1595              
1596 11 100       45 return undef if !defined $value;
1597              
1598 9 100       33 if (JSON::PP::is_bool($value)) {
1599 1 50       22 $value = $value ? 'true' : 'false';
1600             }
1601              
1602 9 100       87 return $value if ref $value;
1603              
1604 7         33 my @chars = split(//u, "$value");
1605 7         15 return [ map { ord($_) } @chars ];
  24         86  
1606             }
1607              
1608             sub _apply_implode {
1609 10     10   21 my ($value) = @_;
1610              
1611 10 100       47 return undef if !defined $value;
1612              
1613 9 100       26 if (ref $value eq 'ARRAY') {
1614 8         16 my $has_nested = grep { ref $_ } @$value;
  24         51  
1615              
1616 8 100       23 if ($has_nested) {
1617 2         23 return [ map { _apply_implode($_) } @$value ];
  6         18  
1618             }
1619              
1620 6         11 my $string = '';
1621 6         15 for my $code (@$value) {
1622 18 50       40 next unless defined $code;
1623 18 50       80 next unless looks_like_number($code);
1624 18         45 $string .= chr(int($code));
1625             }
1626 6         28 return $string;
1627             }
1628              
1629 1         9 return $value;
1630             }
1631              
1632             sub _apply_substr {
1633 16     16   33 my ($value, @args) = @_;
1634              
1635 16 100       25 if (ref $value eq 'ARRAY') {
1636 2         3 return [ map { _apply_substr($_, @args) } @$value ];
  4         7  
1637             }
1638              
1639 14 100       25 return undef if !defined $value;
1640 13 100       31 if (JSON::PP::is_bool($value)) {
    50          
1641 3 100       41 $value = $value ? 'true' : 'false';
1642             }
1643             elsif (ref $value) {
1644 0         0 return $value;
1645             }
1646              
1647 13         85 my ($start, $length) = @args;
1648 13 100 100     61 if (defined $start && !looks_like_number($start)) {
1649 1         44 die 'substr(): start index must be numeric';
1650             }
1651 12 100       20 $start = 0 unless defined $start;
1652 12         15 $start = int($start);
1653              
1654 12 100       17 if (defined $length) {
1655 8 100       15 if (!looks_like_number($length)) {
1656 1         43 die 'substr(): length must be numeric';
1657             }
1658 7         6 $length = int($length);
1659 7         23 return substr($value, $start, $length);
1660             }
1661              
1662 4         13 return substr($value, $start);
1663             }
1664              
1665             sub _apply_slice {
1666 9     9   21 my ($value, @args) = @_;
1667              
1668 9 50       13 return undef if !defined $value;
1669              
1670 9 100       18 if (ref $value eq 'ARRAY') {
1671 8         8 my $array = $value;
1672 8         11 my $size = @$array;
1673              
1674 8 50       13 return [] if $size == 0;
1675              
1676 8 50       14 my $raw_start = @args ? $args[0] : 0;
1677 8         10 my $start = 0;
1678              
1679 8 100 66     34 if (defined $raw_start && !looks_like_number($raw_start)) {
1680 1         23 die 'slice(): start must be numeric';
1681             }
1682              
1683 7 50 33     19 if (defined $raw_start && looks_like_number($raw_start)) {
1684 7         9 $start = int($raw_start);
1685             }
1686              
1687 7 100       10 $start += $size if $start < 0;
1688 7 100       11 $start = 0 if $start < 0;
1689 7 100       22 return [] if $start >= $size;
1690              
1691 6         6 my $length;
1692 6 100 66     29 if (@args > 1 && defined $args[1] && !looks_like_number($args[1])) {
      100        
1693 1         21 die 'slice(): length must be numeric';
1694             }
1695              
1696 5 50 66     17 if (@args > 1 && defined $args[1] && looks_like_number($args[1])) {
      66        
1697 4         7 $length = int($args[1]);
1698             }
1699              
1700 5         4 my $end;
1701 5 100       8 if (defined $length) {
1702 4 100       7 return [] if $length <= 0;
1703 3         14 $end = $start + $length;
1704             }
1705             else {
1706 1         2 $end = $size;
1707             }
1708              
1709 4 50       5 $end = $size if $end > $size;
1710              
1711 4 50       7 return [] if $end <= $start;
1712              
1713 4         17 return [ @$array[$start .. $end - 1] ];
1714             }
1715              
1716 1         3 return $value;
1717             }
1718              
1719             sub _apply_replace {
1720 17     17   50 my ($value, $search, $replacement) = @_;
1721              
1722 17 100       49 if (ref $value eq 'ARRAY') {
1723 2         6 return [ map { _apply_replace($_, $search, $replacement) } @$value ];
  6         18  
1724             }
1725              
1726 15 100       64 return $value if !defined $value;
1727 14 100       44 return $value if ref $value;
1728              
1729 13 100       74 return $value if looks_like_number($value);
1730              
1731 11 50       36 $search = defined $search ? "$search" : '';
1732 11 50       29 $replacement = defined $replacement ? "$replacement" : '';
1733              
1734 11 100       59 return $value if $search eq '';
1735              
1736 10         27 my $pattern = quotemeta $search;
1737 10         221 (my $copy = "$value") =~ s/$pattern/$replacement/g;
1738 10         62 return $copy;
1739             }
1740              
1741             sub _apply_pick {
1742 5     5   11 my ($value, $keys) = @_;
1743              
1744 5 50       16 return $value unless @$keys;
1745              
1746 5 100       11 if (ref $value eq 'HASH') {
1747 3         3 my %subset;
1748 3         10 for my $key (@$keys) {
1749 6 50       10 next unless defined $key;
1750 6 100       16 next unless exists $value->{$key};
1751 5         12 $subset{$key} = $value->{$key};
1752             }
1753 3         9 return \%subset;
1754             }
1755              
1756 2 100       6 if (ref $value eq 'ARRAY') {
1757 1         3 return [ map { _apply_pick($_, $keys) } @$value ];
  2         19  
1758             }
1759              
1760 1         3 return $value;
1761             }
1762              
1763             sub _parse_arguments {
1764 118     118   347 my ($raw) = @_;
1765              
1766 118 50       260 return () unless defined $raw;
1767              
1768 118         214 my $parsed = eval { _decode_json("[$raw]") };
  118         457  
1769 118 50 33     23101 if (!$@ && ref $parsed eq 'ARRAY') {
1770 118         460 return @$parsed;
1771             }
1772              
1773 0         0 my @parts = split /,/, $raw;
1774             return map {
1775 0         0 my $part = $_;
  0         0  
1776 0         0 $part =~ s/^\s+|\s+$//g;
1777 0         0 $part;
1778             } @parts;
1779             }
1780              
1781             sub _split_semicolon_arguments {
1782 37     37   106 my ($raw, $expected) = @_;
1783              
1784 37   50     81 $raw //= '';
1785              
1786 37         45 my @segments;
1787 37         72 my $current = '';
1788 37         41 my $depth = 0;
1789 37         41 my $in_single = 0;
1790 37         48 my $in_double = 0;
1791 37         39 my $escape = 0;
1792              
1793 37         240 for my $char (split //, $raw) {
1794 458 100       571 if ($escape) {
1795 1         1 $current .= $char;
1796 1         9 $escape = 0;
1797 1         3 next;
1798             }
1799              
1800 457 100 66     650 if ($char eq '\\' && $in_double) {
1801 1         2 $current .= $char;
1802 1         2 $escape = 1;
1803 1         3 next;
1804             }
1805              
1806 456 100 66     721 if ($char eq '"' && !$in_single) {
1807 110         138 $in_double = !$in_double;
1808 110         131 $current .= $char;
1809 110         132 next;
1810             }
1811              
1812 346 50 33     483 if ($char eq "'" && !$in_double) {
1813 0         0 $in_single = !$in_single;
1814 0         0 $current .= $char;
1815 0         0 next;
1816             }
1817              
1818 346 100 66     695 if (!$in_single && !$in_double) {
1819 113 100 66     365 if ($char =~ /[\[\{\(]/) {
    100 66        
    100          
1820 10         12 $depth++;
1821             }
1822             elsif ($char =~ /[\]\}\)]/ && $depth > 0) {
1823 10         13 $depth--;
1824             }
1825             elsif ($char eq ';' && $depth == 0) {
1826 19         32 my $segment = $current;
1827 19         83 $segment =~ s/^\s+|\s+$//g;
1828 19 50       68 push @segments, length $segment ? $segment : undef;
1829 19         29 $current = '';
1830 19         28 next;
1831             }
1832             }
1833              
1834 327         408 $current .= $char;
1835             }
1836              
1837 37         80 my $final = $current;
1838 37         153 $final =~ s/^\s+|\s+$//g;
1839 37 50       88 push @segments, length $final ? $final : undef;
1840              
1841 37 50       64 if (defined $expected) {
1842 37         47 $expected = int($expected);
1843 37 100       78 if ($expected > @segments) {
1844 18         38 push @segments, (undef) x ($expected - @segments);
1845             }
1846             }
1847              
1848 37         147 return @segments;
1849             }
1850              
1851             sub _parse_range_arguments {
1852 9     9   15 my ($raw) = @_;
1853              
1854 9 50       13 return () unless defined $raw;
1855              
1856 9         21 $raw =~ s/^\s+|\s+$//g;
1857 9 50       16 return () if $raw eq '';
1858              
1859 9         10 my @segments;
1860 9         9 my $current = '';
1861 9         10 my $in_single = 0;
1862 9         8 my $in_double = 0;
1863 9         11 my $escape = 0;
1864              
1865 9         30 for my $char (split //, $raw) {
1866 61 50       68 if ($escape) {
1867 0         0 $current .= $char;
1868 0         0 $escape = 0;
1869 0         0 next;
1870             }
1871              
1872 61 50 33     73 if ($char eq '\\' && $in_double) {
1873 0         0 $current .= $char;
1874 0         0 $escape = 1;
1875 0         0 next;
1876             }
1877              
1878 61 100 66     76 if ($char eq '"' && !$in_single) {
1879 6         9 $in_double = !$in_double;
1880 6         6 $current .= $char;
1881 6         6 next;
1882             }
1883              
1884 55 50 33     64 if ($char eq "'" && !$in_double) {
1885 0         0 $in_single = !$in_single;
1886 0         0 $current .= $char;
1887 0         0 next;
1888             }
1889              
1890 55 50 66     83 if ($char eq ';' && !$in_single && !$in_double) {
      66        
1891 11         26 push @segments, $current;
1892 11         12 $current = '';
1893 11         13 next;
1894             }
1895              
1896 44         50 $current .= $char;
1897             }
1898              
1899 9         17 push @segments, $current;
1900              
1901 9         9 my @args;
1902 9         10 for my $segment (@segments) {
1903 20 50       27 next unless defined $segment;
1904 20         18 my $clean = $segment;
1905 20         48 $clean =~ s/^\s+|\s+$//g;
1906 20 50       32 next if $clean eq '';
1907              
1908 20         32 my @values = _parse_arguments($clean);
1909 20 50       30 my $value = @values ? $values[0] : undef;
1910 20         30 push @args, $value;
1911             }
1912              
1913 9         23 return @args;
1914             }
1915              
1916             sub _apply_range {
1917 9     9   14 my ($value, $args_ref) = @_;
1918              
1919 9         14 my $sequence = _build_range_sequence($args_ref);
1920 4         10 return @$sequence;
1921             }
1922              
1923             sub _build_range_sequence {
1924 9     9   10 my ($args_ref) = @_;
1925              
1926 9         11 my @args = @$args_ref;
1927 9 50       14 die 'range(): bounds must be numeric' unless @args;
1928              
1929 9 50       14 @args = @args[0 .. 2] if @args > 3;
1930              
1931 9         10 my ($start, $end, $step);
1932              
1933 9 100       18 if (@args == 1) {
    100          
1934 2         3 $start = 0;
1935 2         5 $end = _coerce_range_number($args[0]);
1936 2         4 $step = 1;
1937             }
1938             elsif (@args == 2) {
1939 3         7 $start = _coerce_range_number($args[0]);
1940 3         5 $end = _coerce_range_number($args[1]);
1941 3         3 $step = 1;
1942             }
1943             else {
1944 4         8 $start = _coerce_range_number($args[0]);
1945 4         7 $end = _coerce_range_number($args[1]);
1946 4         5 $step = _coerce_range_number($args[2]);
1947             }
1948              
1949 9 100 100     63 die 'range(): bounds must be numeric' unless defined $start && defined $end;
1950 6 100       35 die 'range(): step must be numeric' if !defined $step;
1951 4 50       6 return [] if $step == 0;
1952              
1953 4 100       6 if ($step > 0) {
1954 3 50       4 return [] if $start >= $end;
1955 3         4 my @sequence;
1956 3         5 for (my $current = $start; $current < $end; $current += $step) {
1957 11         21 push @sequence, 0 + $current;
1958             }
1959 3         6 return \@sequence;
1960             }
1961              
1962             # negative step
1963 1 50       4 return [] if $start <= $end;
1964              
1965 1         2 my @sequence;
1966 1         2 for (my $current = $start; $current > $end; $current += $step) {
1967 3         6 push @sequence, 0 + $current;
1968             }
1969              
1970 1         2 return \@sequence;
1971             }
1972              
1973             sub _coerce_range_number {
1974 20     20   23 my ($value) = @_;
1975              
1976 20 50       24 return undef if !defined $value;
1977 20 100       27 return undef if ref $value;
1978 18 100       27 return undef if _is_string_scalar($value);
1979              
1980 15 50       35 return looks_like_number($value) ? 0 + $value : undef;
1981             }
1982              
1983             sub _apply_contains {
1984 17     17   53 my ($value, $needle) = @_;
1985              
1986 17 100       83 return _deep_contains($value, $needle, 'legacy') ? JSON::PP::true : JSON::PP::false;
1987             }
1988              
1989             sub _apply_contains_subset {
1990 11     11   44 my ($value, $needle) = @_;
1991              
1992 11 100       39 return _deep_contains($value, $needle, 'subset') ? JSON::PP::true : JSON::PP::false;
1993             }
1994              
1995             sub _apply_inside {
1996 7     7   19 my ($value, $container) = @_;
1997              
1998 7         20 return _apply_contains($container, $value);
1999             }
2000              
2001             sub _apply_indices {
2002 9     9   25 my ($value, $needle) = @_;
2003              
2004 9 100       33 if (ref $value eq 'ARRAY') {
2005 6         12 my @matches;
2006 6         22 for my $i (0 .. $#$value) {
2007 23 100       102 push @matches, $i if _values_equal($value->[$i], $needle);
2008             }
2009 6         40 return \@matches;
2010             }
2011              
2012 3 50       12 return [] if !defined $value;
2013              
2014 3 50 33     11 if (!ref $value || JSON::PP::is_bool($value)) {
2015 3 100       28 return [] unless defined $needle;
2016              
2017 2         7 my $haystack = "$value";
2018 2         5 my $fragment = "$needle";
2019              
2020 2         5 my @positions;
2021 2 100       6 if ($fragment eq '') {
2022 1         5 @positions = (0 .. length($haystack));
2023             }
2024             else {
2025 1         2 my $pos = -1;
2026 1         2 while (1) {
2027 3         8 $pos = index($haystack, $fragment, $pos + 1);
2028 3 100       8 last if $pos == -1;
2029 2         4 push @positions, $pos;
2030             }
2031             }
2032              
2033 2         10 return \@positions;
2034             }
2035              
2036 0         0 return [];
2037             }
2038              
2039             sub _apply_has {
2040 7     7   11 my ($value, $needle) = @_;
2041              
2042 7 50       18 return JSON::PP::false if !defined $needle;
2043              
2044 7 100       16 if (ref $value eq 'HASH') {
2045 2 100       13 return exists $value->{$needle} ? JSON::PP::true : JSON::PP::false;
2046             }
2047              
2048 5 50       10 if (ref $value eq 'ARRAY') {
2049 5 50       9 return JSON::PP::false if ref $needle;
2050              
2051 5         36 my $sv = B::svref_2object(\$needle);
2052 5         34 my $flags = $sv->FLAGS;
2053 5 100       14 return JSON::PP::false unless ($flags & (B::SVp_IOK() | B::SVp_NOK()));
2054              
2055 3         6 my $index = int($needle);
2056 3 100 66     22 return ($index >= 0 && $index < @$value)
2057             ? JSON::PP::true
2058             : JSON::PP::false;
2059             }
2060              
2061 0         0 return JSON::PP::false;
2062             }
2063              
2064             sub _values_equal {
2065 62     62   165 my ($left, $right) = @_;
2066              
2067 62 50 66     193 return 1 if !defined $left && !defined $right;
2068 60 100 66     234 return 0 if !defined $left || !defined $right;
2069              
2070 58 100 66     168 if (JSON::PP::is_bool($left) && JSON::PP::is_bool($right)) {
2071 6         274 return (!!$left) == (!!$right);
2072             }
2073              
2074 52 50 66     465 if (!ref $left && !ref $right) {
2075 44 100 66     267 if (looks_like_number($left) && looks_like_number($right)) {
2076 11         80 return $left == $right;
2077             }
2078 33         253 return "$left" eq "$right";
2079             }
2080              
2081 8 50 33     43 if (ref $left eq 'ARRAY' && ref $right eq 'ARRAY') {
2082 0 0       0 return 0 if @$left != @$right;
2083 0         0 for (my $i = 0; $i < @$left; $i++) {
2084 0 0       0 return 0 unless _values_equal($left->[$i], $right->[$i]);
2085             }
2086 0         0 return 1;
2087             }
2088              
2089 8 50 33     88 if (ref $left eq 'HASH' && ref $right eq 'HASH') {
2090 8 100       50 return 0 if keys(%$left) != keys(%$right);
2091 7         73 for my $key (keys %$left) {
2092 9 100 66     53 return 0 unless exists $right->{$key} && _values_equal($left->{$key}, $right->{$key});
2093             }
2094 5         41 return 1;
2095             }
2096              
2097 0         0 return 0;
2098             }
2099              
2100             sub _deep_contains {
2101 63     63   153 my ($value, $needle, $mode) = @_;
2102              
2103 63   50     156 $mode ||= 'legacy';
2104              
2105 63 100 100     241 return 1 if !defined $value && !defined $needle;
2106 61 100       181 return 0 if !defined $value;
2107              
2108 59 100       164 if (ref $value eq 'ARRAY') {
2109 14         53 return _array_contains($value, $needle, $mode);
2110             }
2111              
2112 45 100       114 if (ref $value eq 'HASH') {
2113 13         97 return _hash_contains($value, $needle, $mode);
2114             }
2115              
2116 32         88 return _scalar_contains($value, $needle);
2117             }
2118              
2119             sub _array_contains {
2120 14     14   38 my ($haystack, $needle, $mode) = @_;
2121              
2122 14 100 66     69 if ($mode eq 'subset' && ref $needle eq 'ARRAY') {
2123 8         15 my @used;
2124 8         23 NEEDLE: for my $expected (@$needle) {
2125 12         37 for my $i (0 .. $#$haystack) {
2126 29 100       69 next if $used[$i];
2127 26 100       61 if (_deep_contains($haystack->[$i], $expected, $mode)) {
2128 9         16 $used[$i] = 1;
2129 9         28 next NEEDLE;
2130             }
2131             }
2132 3         41 return 0;
2133             }
2134 5         35 return 1;
2135             }
2136              
2137 6         20 for my $item (@$haystack) {
2138 11 100       36 return 1 if _values_equal($item, $needle);
2139             }
2140              
2141 2         42 return 0;
2142             }
2143              
2144             sub _hash_contains {
2145 13     13   38 my ($value, $needle, $mode) = @_;
2146              
2147 13 100       42 if (ref $needle eq 'HASH') {
2148 12         64 for my $key (keys %$needle) {
2149 13 50       56 return 0 unless exists $value->{$key};
2150              
2151 13 100       98 if ($mode eq 'legacy') {
2152 4 100       28 return 0 unless _values_equal($value->{$key}, $needle->{$key});
2153             }
2154             else {
2155 9 100       25 return 0 unless _deep_contains($value->{$key}, $needle->{$key}, $mode);
2156             }
2157             }
2158 6         40 return 1;
2159             }
2160              
2161 1 50       15 return exists $value->{$needle} ? 1 : 0;
2162             }
2163              
2164             sub _scalar_contains {
2165 32     32   76 my ($value, $needle) = @_;
2166              
2167 32 50       71 return 0 if !defined $value;
2168 32 100       144 return 0 if !defined $needle;
2169              
2170 30 50 33     93 if (!ref $value || JSON::PP::is_bool($value)) {
2171 30         66 my $haystack = "$value";
2172 30         73 my $fragment = "$needle";
2173 30 100       187 return index($haystack, $fragment) >= 0 ? 1 : 0;
2174             }
2175              
2176 0         0 return 0;
2177             }
2178              
2179             sub _ceil {
2180 9     9   20 my ($number) = @_;
2181              
2182 9 100       37 return $number if int($number) == $number;
2183 6 100       33 return $number > 0 ? int($number) + 1 : int($number);
2184             }
2185              
2186             sub _floor {
2187 10     10   19 my ($number) = @_;
2188              
2189 10 100       44 return $number if int($number) == $number;
2190 7 100       38 return $number > 0 ? int($number) : int($number) - 1;
2191             }
2192              
2193             sub _round {
2194 14     14   18 my ($number) = @_;
2195              
2196 14 100       33 return $number if int($number) == $number;
2197 10 100       34 return $number >= 0 ? int($number + 0.5) : int($number - 0.5);
2198             }
2199              
2200             sub _group_count {
2201 4     4   9 my ($array_ref, $path) = @_;
2202 4 50       14 return {} unless ref $array_ref eq 'ARRAY';
2203              
2204 4         15 my ($key_path, $use_entire_item) = _normalize_path_argument($path);
2205              
2206 4         8 my %counts;
2207 4         11 for my $item (@$array_ref) {
2208 18         28 my $key_value;
2209 18 100       36 if ($use_entire_item) {
2210 3         6 $key_value = $item;
2211             }
2212             else {
2213 15         37 my @values = _traverse($item, $key_path);
2214 15 100       51 $key_value = @values ? $values[0] : undef;
2215             }
2216              
2217 18 100       49 my $key = defined $key_value ? _key($key_value) : 'null';
2218 18         95 $counts{$key}++;
2219             }
2220              
2221 4         25 return \%counts;
2222             }
2223              
2224             1;