File Coverage

blib/lib/JQ/Lite/Util/Parsing.pm
Criterion Covered Total %
statement 545 629 86.6
branch 291 434 67.0
condition 89 160 55.6
subroutine 33 37 89.1
pod n/a
total 958 1260 76.0


line stmt bran cond sub pod time code
1             package JQ::Lite::Util;
2              
3 176     176   1347 use strict;
  176         368  
  176         7412  
4 176     176   887 use warnings;
  176         318  
  176         8613  
5              
6 176     176   1097 use JSON::PP ();
  176         378  
  176         4596  
7 176     176   876 use Scalar::Util qw(looks_like_number);
  176         349  
  176         11345  
8 176     176   1298 use Encode qw(encode is_utf8);
  176         411  
  176         8894  
9 176     176   991 use JQ::Lite::Expression ();
  176         1072  
  176         1562658  
10              
11             our $JSON_DECODER = _build_json_decoder();
12             our $FROMJSON_DECODER = _build_json_decoder();
13             our $TOJSON_ENCODER = JSON::PP->new->utf8->allow_nonref;
14              
15             sub _build_json_decoder {
16 352     352   3037 my $decoder = JSON::PP->new->utf8->allow_nonref;
17              
18 352 50       30465 if ($decoder->can('boolean_values')) {
19 352         1491 $decoder->boolean_values(JSON::PP::false, JSON::PP::true);
20             }
21              
22 352         7929 return $decoder;
23             }
24              
25             sub _encode_json {
26 199     199   473 my ($value) = @_;
27 199         880 return $TOJSON_ENCODER->encode($value);
28             }
29              
30             sub _decode_json {
31 6899     6899   14968 my ($text) = @_;
32              
33 6899 100 66     29529 if (defined $text && is_utf8($text, 1)) {
34 6         44 $text = encode('UTF-8', $text);
35             }
36              
37 6899         20989 return $JSON_DECODER->decode($text);
38             }
39              
40             sub _are_brackets_balanced {
41 22     22   48 my ($text) = @_;
42              
43 22 50 33     160 return 1 unless defined $text && length $text;
44              
45 22         124 my %pairs = (
46             '(' => ')',
47             '[' => ']',
48             '{' => '}',
49             );
50 22         109 my %closing = reverse %pairs;
51              
52 22         46 my @stack;
53             my $string;
54 22         63 my $escape = 0;
55              
56 22         160 for my $char (split //, $text) {
57 231 100       473 if (defined $string) {
58 27 50       58 if ($escape) {
59 0         0 $escape = 0;
60 0         0 next;
61             }
62              
63 27 50       63 if ($char eq '\\') {
64 0         0 $escape = 1;
65 0         0 next;
66             }
67              
68 27 100       57 if ($char eq $string) {
69 6         13 undef $string;
70             }
71              
72 27         42 next;
73             }
74              
75 204 100 66     662 if ($char eq "'" || $char eq '"') {
76 6         11 $string = $char;
77 6         10 next;
78             }
79              
80 198 100       386 if (exists $pairs{$char}) {
81 6         16 push @stack, $char;
82 6         26 next;
83             }
84              
85 192 100       422 if (exists $closing{$char}) {
86 8 100       33 return 0 unless @stack;
87 6         17 my $open = pop @stack;
88 6 50       16 return 0 unless $pairs{$open} eq $char;
89 6         14 next;
90             }
91             }
92              
93 20   33     202 return !@stack && !defined $string;
94             }
95              
96             sub _strip_wrapping_parens {
97 6060     6060   11203 my ($text) = @_;
98              
99 6060 50       12445 return '' unless defined $text;
100              
101 6060         9205 my $copy = $text;
102 6060         27875 $copy =~ s/^\s+|\s+$//g;
103              
104 6060         16857 while ($copy =~ /^\((.*)\)$/s) {
105 22         75 my $inner = $1;
106 22 100       63 last unless _are_brackets_balanced($inner);
107 20         126 $inner =~ s/^\s+|\s+$//g;
108 20         78 $copy = $inner;
109             }
110              
111 6060         14170 return $copy;
112             }
113              
114             sub _split_top_level_semicolons {
115 8     8   17 my ($text) = @_;
116              
117 8 50       33 return unless defined $text;
118              
119 8         39 my %pairs = (
120             '(' => ')',
121             '[' => ']',
122             '{' => '}',
123             );
124 8         28 my %closing = reverse %pairs;
125              
126 8         15 my @stack;
127             my $string;
128 8         12 my $escape = 0;
129 8         21 my @parts;
130 8         11 my $start = 0;
131              
132 8         21 for (my $i = 0; $i < length $text; $i++) {
133 103         160 my $char = substr($text, $i, 1);
134              
135 103 100       181 if (defined $string) {
136 1 50       2 if ($escape) {
137 0         0 $escape = 0;
138 0         0 next;
139             }
140              
141 1 50       2 if ($char eq '\\') {
142 0         0 $escape = 1;
143 0         0 next;
144             }
145              
146 1 50       2 if ($char eq $string) {
147 1         1 undef $string;
148             }
149              
150 1         2 next;
151             }
152              
153 102 100 66     278 if ($char eq "'" || $char eq '"') {
154 1         2 $string = $char;
155 1         2 next;
156             }
157              
158 101 50       179 if (exists $pairs{$char}) {
159 0         0 push @stack, $char;
160 0         0 next;
161             }
162              
163 101 50       209 if (exists $closing{$char}) {
164 0 0       0 return unless @stack;
165 0         0 my $open = pop @stack;
166 0 0       0 return unless $pairs{$open} eq $char;
167 0         0 next;
168             }
169              
170 101 100       215 next unless $char eq ';';
171              
172 9 50       22 if (!@stack) {
173 9         21 my $chunk = substr($text, $start, $i - $start);
174 9         16 push @parts, $chunk;
175 9         23 $start = $i + 1;
176             }
177             }
178              
179 8 50       30 push @parts, substr($text, $start) if $start <= length $text;
180              
181 8         43 return @parts;
182             }
183              
184             sub _split_top_level_pipes {
185 1744     1744   3842 my ($text) = @_;
186              
187 1744 50       4492 return unless defined $text;
188              
189 1744         8480 my %pairs = (
190             '(' => ')',
191             '[' => ']',
192             '{' => '}',
193             );
194 1744         8170 my %closing = reverse %pairs;
195              
196 1744         4070 my @stack;
197             my $string;
198 1744         2882 my $escape = 0;
199 1744         2763 my @parts;
200 1744         2679 my $start = 0;
201              
202 1744         2991 my $length = length $text;
203 1744         2809 my $in_try = 0;
204 1744         5689 for (my $i = 0; $i < $length; $i++) {
205 20990         33589 my $char = substr($text, $i, 1);
206              
207 20990 100       38018 if (defined $string) {
208 1528 100       2708 if ($escape) {
209 8         14 $escape = 0;
210 8         17 next;
211             }
212              
213 1520 100       2925 if ($char eq '\\') {
214 8         126 $escape = 1;
215 8         24 next;
216             }
217              
218 1512 100       2700 if ($char eq $string) {
219 336         655 undef $string;
220             }
221              
222 1512         2953 next;
223             }
224              
225 19462 100 100     63112 if ($char eq "'" || $char eq '"') {
226 336         668 $string = $char;
227 336         754 next;
228             }
229              
230 19126 100       39680 if (exists $pairs{$char}) {
231 782         1935 push @stack, $char;
232 782         1794 next;
233             }
234              
235 18344 50 100     72615 if (!$in_try && !@stack && !defined $string) {
      66        
236 15292 100       39328 if (substr($text, $i) =~ /^try\b/) {
237 14         27 $in_try = 1;
238 14         43 next;
239             }
240             }
241              
242 18330 100 100     38302 if ($in_try && !@stack && !defined $string) {
      66        
243 101 100       305 if (substr($text, $i) =~ /^catch\b/) {
244 10         21 $in_try = 0;
245 10         23 next;
246             }
247             }
248              
249 18320 100       35022 if (exists $closing{$char}) {
250 782 50       1879 return unless @stack;
251 782         1665 my $open = pop @stack;
252 782 50       2406 return unless $pairs{$open} eq $char;
253 782         1944 next;
254             }
255              
256 17538 100       45096 next unless $char eq '|';
257 482 100       1293 next if $in_try;
258 480 50       1520 if (substr($text, $i, 2) eq '||') {
259 0         0 $i++;
260 0         0 next;
261             }
262              
263 480 100       1216 if (!@stack) {
264 474         1298 my $chunk = substr($text, $start, $i - $start);
265 474         1255 push @parts, $chunk;
266 474         1404 $start = $i + 1;
267             }
268             }
269              
270 1744 50       6761 push @parts, substr($text, $start) if $start <= $length;
271              
272 1744         12023 return @parts;
273             }
274              
275             sub _split_top_level_commas {
276 1499     1499   3127 my ($text) = @_;
277              
278 1499 50       3623 return unless defined $text;
279              
280 1499         6943 my %pairs = (
281             '(' => ')',
282             '[' => ']',
283             '{' => '}',
284             );
285 1499         6535 my %closing = reverse %pairs;
286              
287 1499         2973 my @stack;
288             my $string;
289 1499         2542 my $escape = 0;
290 1499         2347 my @parts;
291 1499         2581 my $start = 0;
292              
293 1499         4688 for (my $i = 0; $i < length $text; $i++) {
294 15307         24229 my $char = substr($text, $i, 1);
295              
296 15307 100       39562 if (defined $string) {
297 1635 100       2930 if ($escape) {
298 8         13 $escape = 0;
299 8         16 next;
300             }
301              
302 1627 100       2867 if ($char eq '\\') {
303 8         11 $escape = 1;
304 8         15 next;
305             }
306              
307 1619 100       3239 if ($char eq $string) {
308 339         701 undef $string;
309             }
310              
311 1619         3152 next;
312             }
313              
314 13672 100 100     43373 if ($char eq "'" || $char eq '"') {
315 339         681 $string = $char;
316 339         747 next;
317             }
318              
319 13333 100       27879 if (exists $pairs{$char}) {
320 712         1647 push @stack, $char;
321 712         1646 next;
322             }
323              
324 12621 100       23069 if (exists $closing{$char}) {
325 712 50       1788 return unless @stack;
326 712         1482 my $open = pop @stack;
327 712 50       2122 return unless $pairs{$open} eq $char;
328 712         1658 next;
329             }
330              
331 11909 100       29885 next unless $char eq ',';
332              
333 126 100       360 if (!@stack) {
334 38         91 my $chunk = substr($text, $start, $i - $start);
335 38         73 push @parts, $chunk;
336 38         95 $start = $i + 1;
337             }
338             }
339              
340 1499 50       5434 push @parts, substr($text, $start) if $start <= length $text;
341              
342 1499         10085 return @parts;
343             }
344              
345             sub _split_top_level_operator {
346 1938     1938   4885 my ($text, $operator) = @_;
347              
348 1938 50 33     10518 return unless defined $text && defined $operator && length($operator) == 1;
      33        
349              
350 1938         6884 my %pairs = (
351             '(' => ')',
352             '[' => ']',
353             '{' => '}',
354             );
355 1938         7125 my %closing = reverse %pairs;
356              
357 1938         3643 my @stack;
358             my $string;
359 1938         3603 my $escape = 0;
360              
361 1938         5520 for (my $i = 0; $i < length $text; $i++) {
362 17109         26466 my $char = substr($text, $i, 1);
363              
364 17109 100       29875 if (defined $string) {
365 1473 100       2635 if ($escape) {
366 6         9 $escape = 0;
367 6         11 next;
368             }
369              
370 1467 100       2808 if ($char eq '\\') {
371 6         10 $escape = 1;
372 6         12 next;
373             }
374              
375 1461 100       2696 if ($char eq $string) {
376 322         634 undef $string;
377             }
378              
379 1461         2817 next;
380             }
381              
382 15636 100 100     45658 if ($char eq "'" || $char eq '"') {
383 322         634 $string = $char;
384 322         720 next;
385             }
386              
387 15314 100       29056 if (exists $pairs{$char}) {
388 717         1635 push @stack, $char;
389 717         1608 next;
390             }
391              
392 14597 100       26681 if (exists $closing{$char}) {
393 717 50       1694 return if !@stack;
394 717         1420 my $open = pop @stack;
395 717 50       2047 return if $pairs{$open} ne $char;
396 717         1703 next;
397             }
398              
399 13880 100       34910 next if $char ne $operator;
400              
401 68 100       181 if (!@stack) {
402 51 50 33     155 if ($operator eq '+' || $operator eq '-') {
403 51 50       176 my $prev = $i > 0 ? substr($text, $i - 1, 1) : '';
404 51 50       134 my $next = $i + 1 < length $text ? substr($text, $i + 1, 1) : '';
405 51 50 33     203 if ($prev =~ /[eE]/ && $next =~ /[0-9]/) {
406 0         0 next;
407             }
408 51 100       140 if ($next eq '=') {
409 2         4 next;
410             }
411             }
412              
413 49         100 my $lhs = substr($text, 0, $i);
414 49         101 my $rhs = substr($text, $i + 1);
415 49         352 return ($lhs, $rhs);
416             }
417             }
418              
419 1889         10008 return;
420             }
421              
422             sub _split_top_level_colon {
423 49     49   135 my ($text) = @_;
424              
425 49 50       160 return unless defined $text;
426              
427 49         165 my %pairs = (
428             '(' => ')',
429             '[' => ']',
430             '{' => '}',
431             );
432 49         170 my %closing = reverse %pairs;
433              
434 49         81 my @stack;
435             my $string;
436 49         77 my $escape = 0;
437              
438 49         142 for (my $i = 0; $i < length $text; $i++) {
439 313         472 my $char = substr($text, $i, 1);
440              
441 313 100       572 if (defined $string) {
442 132 50       259 if ($escape) {
443 0         0 $escape = 0;
444 0         0 next;
445             }
446              
447 132 50       270 if ($char eq '\\') {
448 0         0 $escape = 1;
449 0         0 next;
450             }
451              
452 132 100       260 if ($char eq $string) {
453 24         169 undef $string;
454             }
455              
456 132         266 next;
457             }
458              
459 181 100 66     507 if ($char eq "'" || $char eq '"') {
460 24         49 $string = $char;
461 24         79 next;
462             }
463              
464 157 50       283 if (exists $pairs{$char}) {
465 0         0 push @stack, $char;
466 0         0 next;
467             }
468              
469 157 50       289 if (exists $closing{$char}) {
470 0 0       0 return unless @stack;
471 0         0 my $open = pop @stack;
472 0 0       0 return unless $pairs{$open} eq $char;
473 0         0 next;
474             }
475              
476 157 100       278 next if $char ne ':';
477              
478 46 50       96 if (!@stack) {
479 46         159 my $lhs = substr($text, 0, $i);
480 46         93 my $rhs = substr($text, $i + 1);
481 46         275 return ($lhs, $rhs);
482             }
483             }
484              
485 3         9 return;
486             }
487              
488             sub _interpret_object_key {
489 13     13   22 my ($raw) = @_;
490              
491 13 50       28 return unless defined $raw;
492              
493 13         18 my $text = $raw;
494 13         56 $text =~ s/^\s+|\s+$//g;
495 13 50       28 return if $text eq '';
496              
497 13         21 my $decoded = eval { $FROMJSON_DECODER->decode($text) };
  13         48  
498 13 50 66     3542 if (!$@ && !ref $decoded) {
499 2         8 return $decoded;
500             }
501              
502 11 50       31 if ($text =~ /^'(.*)'$/s) {
503 0         0 my $inner = $1;
504 0         0 $inner =~ s/\\'/'/g;
505 0         0 return $inner;
506             }
507              
508 11         38 return $text;
509             }
510              
511             sub _split_top_level_semicolon {
512 0     0   0 my ($text) = @_;
513              
514 0         0 my @parts = _split_top_level_semicolons($text);
515 0 0       0 return unless @parts == 2;
516              
517 0         0 return @parts;
518             }
519              
520             sub _matches_keyword {
521 2126     2126   4401 my ($text, $pos, $keyword) = @_;
522              
523 2126 50       4305 return 0 unless defined $text;
524 2126 50       4052 return 0 if $pos < 0;
525              
526 2126         3921 my $kw_len = length $keyword;
527 2126 100       6281 return 0 if $pos + $kw_len > length $text;
528 2099 100       6951 return 0 if substr($text, $pos, $kw_len) ne $keyword;
529              
530 45 100       1519 my $before = $pos == 0 ? '' : substr($text, $pos - 1, 1);
531 45 100       152 my $after = ($pos + $kw_len) < length $text ? substr($text, $pos + $kw_len, 1) : '';
532              
533 45 50       181 return 0 if $before =~ /[A-Za-z0-9_]/;
534 45 50       127 return 0 if $after =~ /[A-Za-z0-9_]/;
535              
536 45         434 return 1;
537             }
538              
539             sub _parse_if_expression {
540 1279     1279   2484 my ($expr) = @_;
541              
542 1279 50       2872 return undef unless defined $expr;
543              
544 1279         2452 my $copy = _strip_wrapping_parens($expr);
545 1279         5476 $copy =~ s/^\s+|\s+$//g;
546 1279 100       5672 return undef unless $copy =~ /^if\b/;
547              
548 9         27 my $len = length $copy;
549 9         22 my $pos = 0;
550              
551 9 50       72 return undef unless _matches_keyword($copy, $pos, 'if');
552 9         24 $pos += 2;
553              
554 9         17 my $depth = 1;
555 9         20 my $state = 'condition';
556 9         20 my $current = '';
557 9         40 my $condition;
558             my @branches;
559 9         0 my $else_expr;
560              
561 9         16 my $in_single = 0;
562 9         18 my $in_double = 0;
563 9         18 my $escape = 0;
564              
565 9         30 while ($pos < $len) {
566 333         741 my $char = substr($copy, $pos, 1);
567              
568 333 50       813 if ($escape) {
569 0         0 $current .= $char;
570 0         0 $escape = 0;
571 0         0 $pos++;
572 0         0 next;
573             }
574              
575 333 50       697 if ($in_single) {
576 0 0       0 if ($char eq '\\') {
    0          
577 0         0 $escape = 1;
578             }
579             elsif ($char eq "'") {
580 0         0 $in_single = 0;
581             }
582 0         0 $current .= $char;
583 0         0 $pos++;
584 0         0 next;
585             }
586              
587 333 100       1047 if ($in_double) {
588 36 50       117 if ($char eq '\\') {
    100          
589 0         0 $escape = 1;
590             }
591             elsif ($char eq '"') {
592 14         22 $in_double = 0;
593             }
594 36         56 $current .= $char;
595 36         129 $pos++;
596 36         82 next;
597             }
598              
599 297 50       656 if ($char eq "'") {
600 0         0 $in_single = 1;
601 0         0 $current .= $char;
602 0         0 $pos++;
603 0         0 next;
604             }
605              
606 297 100       28919 if ($char eq '"') {
607 14         34 $in_double = 1;
608 14         58 $current .= $char;
609 14         80 $pos++;
610 14         173 next;
611             }
612              
613 283 100       562 if (_matches_keyword($copy, $pos, 'if')) {
614 1         4 $depth++;
615 1         4 $current .= 'if';
616 1         6 $pos += 2;
617 1         3 next;
618             }
619              
620 282 100 100     587 if (_matches_keyword($copy, $pos, 'then') && $depth == 1 && $state eq 'condition') {
      66        
621 11         63 $condition = $current;
622 11         194 $condition =~ s/^\s+|\s+$//g;
623 11 50 33     70 return undef unless defined $condition && length $condition;
624              
625 11         39 $current = '';
626 11         24 $state = 'then';
627 11         26 $pos += 4;
628 11         59 next;
629             }
630              
631 271 50 66     507 if (_matches_keyword($copy, $pos, 'elif') && $depth == 1 && $state eq 'then') {
      66        
632 2         10 my $then_expr = $current;
633 2         22 $then_expr =~ s/^\s+|\s+$//g;
634 2 50       10 $then_expr = '.' if !length $then_expr;
635              
636 2 50       9 return undef unless defined $condition;
637 2         24 push @branches, { condition => $condition, then => $then_expr };
638              
639 2         8 $condition = undef;
640 2         4 $current = '';
641 2         7 $state = 'condition';
642 2         4 $pos += 4;
643 2         7 next;
644             }
645              
646 269 100 100     483 if (_matches_keyword($copy, $pos, 'else') && $depth == 1 && $state eq 'then') {
      66        
647 8         27 my $then_expr = $current;
648 8         111 $then_expr =~ s/^\s+|\s+$//g;
649 8 50       34 $then_expr = '.' if !length $then_expr;
650              
651 8 50       46 return undef unless defined $condition;
652 8         76 push @branches, { condition => $condition, then => $then_expr };
653              
654 8         22 $condition = undef;
655 8         24 $current = '';
656 8         15 $state = 'else';
657 8         17 $pos += 4;
658 8         28 next;
659             }
660              
661 261 100       516 if (_matches_keyword($copy, $pos, 'end')) {
662 10 100       111 if ($depth == 1) {
663 9 100       52 if ($state eq 'then') {
    50          
    0          
664 1         5 my $then_expr = $current;
665 1         10 $then_expr =~ s/^\s+|\s+$//g;
666 1 50       7 $then_expr = '.' if !length $then_expr;
667              
668 1 50       3 return undef unless defined $condition;
669 1         8 push @branches, { condition => $condition, then => $then_expr };
670             }
671             elsif ($state eq 'else') {
672 8         20 my $else = $current;
673 8         83 $else =~ s/^\s+|\s+$//g;
674 8 50       58 $else_expr = length $else ? $else : undef;
675             }
676             elsif ($state eq 'condition') {
677 0         0 return undef;
678             }
679              
680 9         21 $depth = 0;
681 9         18 $pos += 3;
682 9         34 $current = '';
683 9         20 $state = 'done';
684 9         25 last;
685             }
686             else {
687 1         4 $depth--;
688 1         4 $current .= 'end';
689 1         2 $pos += 3;
690 1         6 next;
691             }
692             }
693              
694 251 100 66     534 if (_matches_keyword($copy, $pos, 'then') && $depth > 1) {
695 1         9 $current .= 'then';
696 1         11 $pos += 4;
697 1         5 next;
698             }
699              
700 250 50 33     505 if (_matches_keyword($copy, $pos, 'elif') && $depth > 1) {
701 0         0 $current .= 'elif';
702 0         0 $pos += 4;
703 0         0 next;
704             }
705              
706 250 100 66     460 if (_matches_keyword($copy, $pos, 'else') && $depth > 1) {
707 1         5 $current .= 'else';
708 1         3 $pos += 4;
709 1         6 next;
710             }
711              
712 249         447 $current .= $char;
713 249         763 $pos++;
714             }
715              
716 9 50       27 return undef unless @branches;
717              
718 9 50       32 if ($pos < $len) {
719 0         0 my $remaining = substr($copy, $pos);
720 0         0 $remaining =~ s/^\s+//;
721 0 0       0 return undef if $remaining =~ /\S/;
722             }
723              
724             return {
725 9         119 branches => \@branches,
726             else => $else_expr,
727             };
728             }
729              
730             sub _parse_reduce_expression {
731 1270     1270   2602 my ($expr) = @_;
732              
733 1270 50       3032 return undef unless defined $expr;
734              
735 1270         2486 my $copy = _strip_wrapping_parens($expr);
736 1270 100       6121 return undef unless $copy =~ /^reduce\s+(.+?)\s+as\s+\$(\w+)\s*\((.*)\)$/s;
737              
738 4         29 my ($generator, $var_name, $body) = ($1, $2, $3);
739 4         13 my @parts = _split_top_level_semicolons($body);
740 4 50       14 return undef unless @parts == 2;
741 4         11 my ($init_expr, $update_expr) = @parts;
742              
743 4         24 $generator =~ s/^\s+|\s+$//g;
744 4         14 $init_expr =~ s/^\s+|\s+$//g;
745 4         33 $update_expr =~ s/^\s+|\s+$//g;
746              
747             return {
748 4         46 generator => $generator,
749             var_name => $var_name,
750             init_expr => $init_expr,
751             update_expr => $update_expr,
752             };
753             }
754              
755             sub _parse_foreach_expression {
756 1283     1283   2615 my ($expr) = @_;
757              
758 1283 50       3132 return undef unless defined $expr;
759              
760 1283         3167 my $copy = _strip_wrapping_parens($expr);
761 1283 100       6488 return undef unless $copy =~ /^foreach\s+(.+?)\s+as\s+\$(\w+)\s*\((.*)\)$/s;
762              
763 4         21 my ($generator, $var_name, $body) = ($1, $2, $3);
764 4         8 my @parts = _split_top_level_semicolons($body);
765 4 50 33     16 return undef unless @parts >= 2 && @parts <= 3;
766              
767 4         32 my ($init_expr, $update_expr, $extract_expr) = @parts;
768              
769 4         7 for ($generator, $init_expr, $update_expr) {
770 12 50       15 next unless defined $_;
771 12         35 s/^\s+|\s+$//g;
772             }
773              
774 4 100       8 if (defined $extract_expr) {
775 1         3 $extract_expr =~ s/^\s+|\s+$//g;
776             }
777              
778             return {
779 4         27 generator => $generator,
780             var_name => $var_name,
781             init_expr => $init_expr,
782             update_expr => $update_expr,
783             extract_expr => $extract_expr,
784             };
785             }
786              
787             sub _resolve_variable_reference {
788 48     48   128 my ($self, $name) = @_;
789              
790 48 50 33     260 return (undef, 0) unless defined $self && ref($self) eq 'JQ::Lite';
791 48 50 33     187 return (undef, 0) unless defined $name && length $name;
792              
793 48   50     192 my $vars = $self->{_vars} || {};
794 48 100       138 return (undef, 0) unless exists $vars->{$name};
795              
796 47         197 return ($vars->{$name}, 1);
797             }
798              
799             sub _evaluate_variable_reference {
800 48     48   113 my ($self, $name, $suffix) = @_;
801              
802 48         120 my ($value, $exists) = _resolve_variable_reference($self, $name);
803 48 100       114 return () unless $exists;
804              
805 47 100 66     277 return ($value) if !defined $suffix || $suffix !~ /\S/;
806              
807 9         21 my $expr = $suffix;
808 9         44 $expr =~ s/^\s+//;
809              
810 9         66 my ($values, $ok) = _evaluate_value_expression($self, $value, $expr);
811 9 50       58 return $ok ? @$values : ();
812             }
813              
814             sub _evaluate_value_expression {
815 771     771   2222 my ($self, $context, $expr) = @_;
816              
817 771 50       2039 return ([], 0) unless defined $expr;
818              
819 771         2064 my $copy = _strip_wrapping_parens($expr);
820 771         3157 $copy =~ s/^\s+|\s+$//g;
821 771 50       2176 return ([], 0) if $copy eq '';
822              
823 771 100       1900 if (_looks_like_expression($copy)) {
824             my %builtins = (
825             floor => sub {
826 0     0   0 my ($value) = @_;
827 0         0 my $numeric = _coerce_number_strict($value, 'floor() argument');
828 0         0 return _floor($numeric);
829             },
830             ceil => sub {
831 0     0   0 my ($value) = @_;
832 0         0 my $numeric = _coerce_number_strict($value, 'ceil() argument');
833 0         0 return _ceil($numeric);
834             },
835             round => sub {
836 0     0   0 my ($value) = @_;
837 0         0 my $numeric = _coerce_number_strict($value, 'round() argument');
838 0         0 return _round($numeric);
839             },
840             tonumber => sub {
841 1     1   3 my ($value) = @_;
842 1         4 return _tonumber($value);
843             },
844 62         1157 );
845              
846             my ($ok, $value) = JQ::Lite::Expression::evaluate(
847             expr => $copy,
848             context => $context,
849             resolve_path => sub {
850 17     17   65 my ($ctx, $path) = @_;
851 17 50 33     99 return $ctx if !defined $path || $path eq '';
852 17         63 my @values = _traverse($ctx, $path);
853 17 50       68 return @values ? $values[0] : undef;
854             },
855 62         653 coerce_number => \&_coerce_number_strict,
856             builtins => \%builtins,
857             );
858              
859 55 100       981 if ($ok) {
860 6         87 return ([ $value ], 1);
861             }
862             }
863              
864 758         2088 my @pipeline_parts = _split_top_level_pipes($copy);
865 758 100       2283 if (@pipeline_parts > 1) {
866 5 50 33     69 if (defined $self && $self->can('run_query')) {
867 5         20 my $json = _encode_json($context);
868 5         1984 my @outputs = $self->run_query($json, $copy);
869 5         31 return ([ @outputs ], 1);
870             }
871             }
872              
873 753 100       2391 if ($copy =~ /^\$(\w+)(.*)$/s) {
874 32   50     129 my ($var, $suffix) = ($1, $2 // '');
875 32         72 my @values = _evaluate_variable_reference($self, $var, $suffix);
876 32         115 return (\@values, 1);
877             }
878              
879 721 100       2063 if ($copy =~ /^\[(.*)$/s) {
880 1         2 $copy = ".$copy";
881             }
882              
883 721 100       1978 if ($copy eq '.') {
884 31         105 return ([ $context ], 1);
885             }
886              
887 690 100       2317 if ($copy =~ /^\.(.*)$/s) {
888 128         415 my $path = $1;
889 128         625 $path =~ s/^\s+|\s+$//g;
890              
891 128 50 66     650 if ($path !~ /\s/ && $path !~ /[+\-*\/]/) {
892 70 50       266 return ([], 1) unless defined $context;
893 70 50       199 return ([], 1) if $path eq '';
894              
895 70         317 my @values = _traverse($context, $path);
896 70         353 return (\@values, 1);
897             }
898             }
899              
900 620         1769 my ($lhs_expr, $rhs_expr) = _split_top_level_operator($copy, '+');
901 620 100 66     2117 if (defined $lhs_expr && defined $rhs_expr) {
902 37         172 $lhs_expr =~ s/^\s+|\s+$//g;
903 37         627 $rhs_expr =~ s/^\s+|\s+$//g;
904              
905 37 50 33     182 if (length $lhs_expr && length $rhs_expr) {
906 37         175 my ($lhs_values, $lhs_ok) = _evaluate_value_expression($self, $context, $lhs_expr);
907 37         97 my $lhs;
908 37 50       131 if ($lhs_ok) {
909 37 50       127 $lhs = @$lhs_values ? $lhs_values->[0] : undef;
910             }
911             else {
912 0         0 my @outputs = $self->run_query(_encode_json($context), $lhs_expr);
913 0 0       0 $lhs = @outputs ? $outputs[0] : undef;
914             }
915              
916 37         84 my ($rhs_values, $rhs_ok) = _evaluate_value_expression($self, $context, $rhs_expr);
917 37         63 my $rhs;
918 37 50       71 if ($rhs_ok) {
919 37 50       83 $rhs = @$rhs_values ? $rhs_values->[0] : undef;
920             }
921             else {
922 0         0 my @outputs = $self->run_query(_encode_json($context), $rhs_expr);
923 0 0       0 $rhs = @outputs ? $outputs[0] : undef;
924             }
925              
926 37         85 my $combined = _apply_addition($lhs, $rhs);
927 35         217 return ([ $combined ], 1);
928             }
929             }
930              
931 583         1220 my $decoded = eval { _decode_json($copy) };
  583         1594  
932 583 100       215102 if (!$@) {
933 47         285 return ([ $decoded ], 1);
934             }
935              
936 536 50       2074 if ($copy =~ /^'(.*)'$/s) {
937 0         0 my $text = $1;
938 0         0 $text =~ s/\\'/'/g;
939 0         0 return ([ $text ], 1);
940             }
941              
942 536 100 33     9414 if ($copy !~ /\bthen\b/i
      100        
943             && $copy !~ /\belse\b/i
944             && $copy !~ /\bend\b/i
945             && $copy =~ /(?:==|!=|>=|<=|>|<|\band\b|\bor\b|\bcontains\b|\bhas\b|\bmatch\b)/)
946             {
947 13         106 my $bool = _evaluate_condition($context, $copy);
948 13 100       97 my $json_bool = $bool ? JSON::PP::true : JSON::PP::false;
949 13         124 return ([ $json_bool ], 1);
950             }
951              
952 523         3241 return ([], 0);
953             }
954              
955             sub _apply_addition {
956 46     46   101 my ($left, $right) = @_;
957              
958 46 50       119 return $right if !defined $left;
959 46 50       93 return $left if !defined $right;
960              
961 46 50       167 if (ref($left) eq 'JSON::PP::Boolean') {
962 0 0       0 $left = $left ? 1 : 0;
963             }
964              
965 46 50       157 if (ref($right) eq 'JSON::PP::Boolean') {
966 0 0       0 $right = $right ? 1 : 0;
967             }
968              
969 46 50 66     191 if (!ref $left && !ref $right) {
970 45         156 my $left_is_string = _is_string_scalar($left);
971 45         107 my $right_is_string = _is_string_scalar($right);
972              
973 45 100 66     193 if ($left_is_string || $right_is_string) {
974 13 100 66     136 die 'addition operands must both be strings' if !$left_is_string || !$right_is_string;
975 10 50       22 $left = '' unless defined $left;
976 10 50       43 $right = '' unless defined $right;
977 10         47 return "$left$right";
978             }
979              
980 32 50 33     195 if (looks_like_number($left) && looks_like_number($right)) {
981 32         153 return 0 + $left + $right;
982             }
983              
984 0         0 die 'addition operands must both be numbers or both be strings';
985             }
986              
987 1 50 33     24 if (ref $left eq 'ARRAY' && ref $right eq 'ARRAY') {
988 0         0 return [ @$left, @$right ];
989             }
990              
991 1 50       4 if (ref $left eq 'ARRAY') {
992 0         0 return [ @$left, $right ];
993             }
994              
995 1 50       4 if (ref $right eq 'ARRAY') {
996 0         0 return [ $left, @$right ];
997             }
998              
999 1 50 33     15 if (ref $left eq 'HASH' && ref $right eq 'HASH') {
1000 1         12 return { %$left, %$right };
1001             }
1002              
1003 0 0 0     0 return $right if !ref $left && ref $right eq 'HASH';
1004 0 0 0     0 return $left if ref $left eq 'HASH' && !ref $right;
1005              
1006 0         0 return undef;
1007             }
1008              
1009             sub _coerce_number_strict {
1010 30     30   66 my ($value, $label) = @_;
1011              
1012 30   50     70 $label ||= 'value';
1013              
1014 30 50       66 die "$label must be a number" unless defined $value;
1015              
1016 30 50       84 if (ref($value) eq 'JSON::PP::Boolean') {
1017 0 0       0 return $value ? 1 : 0;
1018             }
1019              
1020 30 50       72 die "$label must be a number" if ref $value;
1021 30 50       121 die "$label must be a number" unless looks_like_number($value);
1022              
1023 30         79 return 0 + $value;
1024             }
1025              
1026             sub _tonumber {
1027 3     3   6 my ($value) = @_;
1028              
1029 3 50       8 return undef unless defined $value;
1030              
1031 3 50       8 if (ref($value) eq 'JSON::PP::Boolean') {
1032 0 0       0 return $value ? 1 : 0;
1033             }
1034              
1035 3 50       6 if (ref $value) {
1036 0         0 die 'tonumber(): argument must be a string or number';
1037             }
1038              
1039 3         6 my $text = "$value";
1040 3         16 $text =~ s/^\s+|\s+$//g;
1041              
1042 3 50 33     22 die 'tonumber(): not a numeric string' unless length $text && looks_like_number($text);
1043              
1044 3         12 return 0 + $text;
1045             }
1046              
1047             sub _looks_like_expression {
1048 2123     2123   4714 my ($expr) = @_;
1049              
1050 2123 50       4874 return 0 unless defined $expr;
1051              
1052 2123 100       8068 return 1 if $expr =~ /\b(?:floor|ceil|round|tonumber)\b/;
1053 2088 100       6598 return 0 if $expr =~ /^\s*[\{\[]/;
1054 2064 100       7587 return 0 if $expr =~ /^[A-Za-z_]\w*\s*\(/;
1055 1632 100       4421 return 1 if $expr =~ /[\-*\/%]/;
1056 1589 100       8610 return 1 if $expr =~ /(?:==|!=|>=|<=|>|<|\band\b|\bor\b)/i;
1057              
1058 1546         4670 return 0;
1059             }
1060              
1061             sub _looks_like_assignment {
1062 1185     1185   2344 my ($expr) = @_;
1063              
1064 1185 50       2874 return 0 unless defined $expr;
1065 1185 100       4903 return 0 if $expr =~ /[()]/;
1066 795 50       3323 return 0 if $expr =~ /(?:==|!=|>=|<=|=>|=<)/;
1067 795         3131 return ($expr =~ /=/);
1068             }
1069              
1070             sub _parse_assignment_expression {
1071 16     16   22 my ($expr) = @_;
1072              
1073 16   50     28 $expr //= '';
1074              
1075 16         127 my ($lhs, $op, $rhs) = ($expr =~ /^(.*?)\s*([+\-*\/]?=)\s*(.*)$/);
1076              
1077 16   50     29 $lhs //= '';
1078 16   50     26 $rhs //= '';
1079 16   50     27 $op //= '=';
1080              
1081 16         64 $lhs =~ s/^\s+|\s+$//g;
1082 16         32 $rhs =~ s/^\s+|\s+$//g;
1083              
1084 16         27 $lhs =~ s/^\.//;
1085              
1086 16         26 my $value_spec = _parse_assignment_value($rhs);
1087              
1088 16         53 return ($lhs, $value_spec, $op);
1089             }
1090              
1091             sub _parse_assignment_value {
1092 16     16   23 my ($raw) = @_;
1093              
1094 16   50     27 $raw //= '';
1095 16         29 $raw =~ s/^\s+|\s+$//g;
1096              
1097 16 100       31 if ($raw =~ /^\.(.+)$/) {
1098 1         6 return { type => 'path', value => $1 };
1099             }
1100              
1101 15         17 my $decoded = eval { _decode_json($raw) };
  15         114  
1102 15 100       1961 if (!$@) {
1103 11         42 return { type => 'literal', value => $decoded };
1104             }
1105              
1106 4 100       18 if ($raw =~ /^'(.*)'$/) {
1107 2         6 my $text = $1;
1108 2         4 $text =~ s/\\'/'/g;
1109 2         8 return { type => 'literal', value => $text };
1110             }
1111              
1112 2         8 return { type => 'expression', value => $raw };
1113             }
1114              
1115             1;