File Coverage

blib/lib/JQ/Lite/Parser.pm
Criterion Covered Total %
statement 130 146 89.0
branch 57 78 73.0
condition 19 33 57.5
subroutine 8 8 100.0
pod 0 1 0.0
total 214 266 80.4


line stmt bran cond sub pod time code
1             package JQ::Lite::Parser;
2              
3 176     176   1400 use strict;
  176         408  
  176         9013  
4 176     176   1480 use warnings;
  176         613  
  176         12946  
5              
6 176     176   1101 use JQ::Lite::Util ();
  176         353  
  176         3247  
7 176     176   839 use JSON::PP ();
  176         367  
  176         372288  
8              
9             sub parse_query {
10 936     936 0 2495 my ($query) = @_;
11              
12 936 50       2501 return () unless defined $query;
13 936 50       4011 return () if $query =~ /^\s*\.\s*$/;
14              
15 936         3404 my @parts = JQ::Lite::Util::_split_top_level_pipes($query);
16             @parts = map {
17 936         2528 my $part = $_;
  1393         2368  
18 1393         9934 $part =~ s/^\s+|\s+$//g;
19 1393         4462 $part;
20             } @parts;
21              
22             @parts = map {
23 936 100       1833 if ($_ eq '.[]') {
  1393 100       6315  
24 14         50 '.[]';
25             }
26             elsif ($_ =~ /^\.(.+)$/) {
27 549         1815 my $rest = $1;
28 549 100 100     9392 if ($rest =~ /,/) {
    100 100        
    100 66        
29 5         11 $_; # preserve leading dot when sequence filters are present
30             }
31             elsif ($rest =~ /^\s*\[/) {
32 4         14 $_; # preserve leading dot for array indexing and bracket access
33             }
34             elsif ($rest =~ /^\s*[+\-*\/%]/
35             || $rest =~ /[+\-*\/%]/
36             || $rest =~ /(?:==|!=|>=|<=|>|<|\band\b|\bor\b)/i
37             || $rest =~ /\b(?:floor|ceil|round|tonumber)\b/)
38             {
39 49         148 $_;
40             }
41             else {
42 491         1141 my $trimmed = $rest;
43 491         2057 $trimmed =~ s/^\s+|\s+$//g;
44 491 100       1625 if ($trimmed =~ /^"(?:[^"\\]|\\.)*"$/s) {
45 2         5 my $decoded = eval { JQ::Lite::Util::_decode_json($trimmed) };
  2         6  
46 2 50 33     357 return $decoded if defined $decoded && !$@;
47             }
48 489         1473 $rest;
49             }
50             }
51             else {
52 830         2223 $_;
53             }
54             } @parts;
55              
56 934         1776 @parts = map { _lower_object_shorthand($_) } @parts;
  1391         3767  
57              
58 934         1658 my @expanded;
59 934         2047 for my $part (@parts) {
60 1391 50       2973 next unless defined $part;
61              
62 1391         2305 my $trimmed = $part;
63 1391         6097 $trimmed =~ s/^\s+|\s+$//g;
64              
65 1391 100       4476 if ($trimmed =~ /^\(.*\)$/s) {
66 4         17 my $inner = JQ::Lite::Util::_strip_wrapping_parens($trimmed);
67 4 100 33     31 if (defined $inner && length $inner && $inner ne $trimmed) {
      66        
68 3         18 my @inner_parts = parse_query($inner);
69 3 50       7 if (@inner_parts) {
70 3         5 push @expanded, @inner_parts;
71 3         6 next;
72             }
73             }
74             }
75              
76 1388         3353 push @expanded, $trimmed;
77             }
78              
79 934         3798 return @expanded;
80             }
81              
82             sub _lower_object_shorthand {
83 1424     1424   3398 my ($text) = @_;
84              
85 1424 50       3450 return $text unless defined $text;
86 1424 100       5468 return $text if index($text, '{') == -1;
87              
88 28         60 my $result = '';
89 28         101 my $len = length $text;
90 28         48 my $i = 0;
91 28         102 my $string;
92 28         48 my $escape = 0;
93              
94 28         107 while ($i < $len) {
95 266         397 my $char = substr($text, $i, 1);
96              
97 266 100       434 if (defined $string) {
98 32         46 $result .= $char;
99 32 50       86 if ($escape) {
    50          
    100          
100 0         0 $escape = 0;
101             }
102             elsif ($char eq '\\') {
103 0         0 $escape = 1;
104             }
105             elsif ($char eq $string) {
106 4         7 undef $string;
107             }
108 32         42 $i++;
109 32         60 next;
110             }
111              
112 234 100 66     713 if ($char eq "'" || $char eq '"') {
113 4         10 $string = $char;
114 4         8 $result .= $char;
115 4         7 $i++;
116 4         9 next;
117             }
118              
119 230 100       498 if ($char eq '{') {
120 28         176 my ($body, $consumed) = _extract_object_body($text, $i);
121 28 50       88 if (defined $body) {
122 28         106 my $lowered = _lower_object_constructor($body);
123 28         141 $result .= '{' . $lowered . '}';
124 28         58 $i += $consumed;
125 28         89 next;
126             }
127             }
128              
129 202         293 $result .= $char;
130 202         313 $i++;
131             }
132              
133 28         171 return $result;
134             }
135              
136             sub _extract_object_body {
137 28     28   76 my ($text, $start) = @_;
138              
139 28         125 my $len = length $text;
140 28         44 my $depth = 0;
141 28         142 my $string;
142 28         55 my $escape = 0;
143              
144 28         101 for (my $i = $start; $i < $len; $i++) {
145 566         833 my $char = substr($text, $i, 1);
146              
147 566 100       955 if (defined $string) {
148 200 50       335 if ($escape) {
149 0         0 $escape = 0;
150 0         0 next;
151             }
152              
153 200 50       341 if ($char eq '\\') {
154 0         0 $escape = 1;
155 0         0 next;
156             }
157              
158 200 100       359 if ($char eq $string) {
159 37         63 undef $string;
160             }
161              
162 200         398 next;
163             }
164              
165 366 100 66     1269 if ($char eq "'" || $char eq '"') {
166 37         90 $string = $char;
167 37         83 next;
168             }
169              
170 329 100       656 if ($char eq '{') {
171 32         54 $depth++;
172 32         105 next;
173             }
174              
175 297 100       678 if ($char eq '}') {
176 32         98 $depth--;
177 32 100       132 if ($depth == 0) {
178 28         106 my $body = substr($text, $start + 1, $i - $start - 1);
179 28         137 return ($body, $i - $start + 1);
180             }
181 4         10 next;
182             }
183             }
184              
185 0         0 return (undef, 1);
186             }
187              
188             sub _lower_object_constructor {
189 28     28   65 my ($inner) = @_;
190              
191 28 50       72 return $inner unless defined $inner;
192              
193 28         112 my @parts = JQ::Lite::Util::_split_top_level_commas($inner);
194 28 50       91 return $inner unless @parts;
195              
196 28         73 my @transformed;
197 28         78 for my $part (@parts) {
198 37 50       127 next unless defined $part;
199              
200 37         76 my $trimmed = $part;
201 37         293 $trimmed =~ s/^\s+|\s+$//g;
202 37 100       100 next if $trimmed eq '';
203              
204 36         163 my ($lhs, $rhs) = JQ::Lite::Util::_split_top_level_colon($part);
205              
206 36 100 66     164 if (defined $lhs && defined $rhs) {
207 33         75 my $key = $lhs;
208 33         155 $key =~ s/^\s+|\s+$//g;
209              
210 33         254 my $value = _lower_object_shorthand($rhs);
211 33         222 $value =~ s/^\s+|\s+$//g;
212              
213 33         93 push @transformed, "$key: $value";
214 33         99 next;
215             }
216              
217 3 50 33     32 if (!defined $lhs && $trimmed =~ /^[A-Za-z_][A-Za-z0-9_]*$/) {
218 3         12 push @transformed, "$trimmed: .$trimmed";
219 3         26 next;
220             }
221              
222 0 0 0     0 if (defined $lhs && !defined $rhs) {
223 0         0 my $key = $lhs;
224 0         0 $key =~ s/^\s+|\s+$//g;
225 0 0       0 next if $key eq '';
226 0         0 push @transformed, "$key: .$key";
227 0         0 next;
228             }
229              
230 0         0 my $lowered = _lower_object_shorthand($trimmed);
231 0         0 $lowered =~ s/^\s+|\s+$//g;
232 0 0       0 push @transformed, $lowered if length $lowered;
233             }
234              
235 28         140 return join(', ', @transformed);
236             }
237              
238             1;