File Coverage

blib/lib/Jmespath/TreeInterpreter.pm
Criterion Covered Total %
statement 245 252 97.2
branch 84 94 89.3
condition 27 33 81.8
subroutine 45 47 95.7
pod 2 25 8.0
total 403 451 89.3


line stmt bran cond sub pod time code
1             package Jmespath::TreeInterpreter;
2 2     2   7 use parent 'Jmespath::Visitor';
  2         1  
  2         12  
3 2     2   109 use strict;
  2         2  
  2         37  
4 2     2   5 use warnings;
  2         2  
  2         43  
5 2     2   6 use Try::Tiny;
  2         2  
  2         87  
6 2     2   6 use List::Util qw(unpairs);
  2         2  
  2         85  
7 2     2   10 use Scalar::Util qw(looks_like_number);
  2         3  
  2         65  
8 2     2   6 use JSON;
  2         3  
  2         7  
9 2     2   730 use Jmespath::Expression;
  2         3  
  2         44  
10 2     2   679 use Jmespath::Functions qw(:all);
  2         2  
  2         474  
11 2     2   712 use Jmespath::AttributeException;
  2         3  
  2         52  
12 2     2   642 use Jmespath::IndexException;
  2         4  
  2         53  
13 2     2   616 use Jmespath::UnknownFunctionException;
  2         4  
  2         3571  
14              
15              
16             my $COMPARATOR_FUNC = { 'le' => 'le',
17             'ne' => 'ne',
18             'lt' => 'lt',
19             'lte' => 'lte',
20             'eq' => 'eq',
21             'gt' => 'gt',
22             'gte' => 'gte' };
23              
24             my $MAP_TYPE = 'HASH';
25              
26             my $OPTIONS_DEFAULT = { hash_cls => undef,
27             custom_functions => undef };
28              
29             sub new {
30 760     760 0 631 my ($class, $options) = @_;
31 760         1636 my $self = $class->SUPER::new($options);
32 760 50       1108 if ( not defined $options) { $options = $OPTIONS_DEFAULT; }
  760         619  
33             $self->{_hash_cls} = $options->{ hash_cls }
34 760 50       1180 if defined $options->{hash_cls};
35 0         0 $self->{_functions} = eval { 'use ' . $options->{custom_functions} }
36 760 50       1148 if defined $options->{custom_functions};
37              
38 760         1068 return $self;
39             }
40              
41             sub visit {
42 5359     5359 0 4170 my ($self, $node, $args) = @_;
43 5359         4348 my $node_type = $node->{type};
44 5359         3355 my $result;
45             try {
46 5359     5359   84627 my $method = \&{'visit_' . $node->{type}};
  5359         10068  
47 5359         6186 $result = &$method( $self, $node, $args );
48             } catch {
49 47     47   18882 $_->throw;
50 5359         16895 };
51 5312         35763 return $result;
52             }
53              
54             sub default_visit {
55 0     0 0 0 my ($self, $node, @args) = @_;
56 0         0 Jmespath::NotImplementedException->new($node->{type})->throw;
57 0         0 return;
58             }
59              
60             =item visit_subexpression(node, value)
61              
62             Drills down into a subexpression in the AST graph.
63              
64             =cut
65              
66             sub visit_subexpression {
67 172     172 1 158 my ($self, $node, $value) = @_;
68 172         131 my $result = $value;
69 172         152 foreach my $node (@{$node->{children}}) {
  172         240  
70 364         455 $result = $self->visit($node, $result);
71             }
72 172         264 return $result;
73             }
74              
75             =item visit_field(node, value)
76              
77             Returns the value of a field in the JSON data.
78              
79             value : the JSON data
80             node : the AST node being evaluated.
81              
82             =cut
83              
84             sub visit_field {
85 2024     2024 1 1676 my ($self, $node, $value) = @_;
86 2024         1262 my $result;
87             try {
88 2024     2024   32403 $result = $value->{$node->{value}};
89             } catch {
90             # when the field cannot be looked up, then the spec defines the
91             # return value as undef.
92 19     19   143 return;
93 2024         5690 };
94 2024         14281 return $result;
95             }
96              
97             sub visit_comparator {
98 466     466 0 399 my ($self, $node, $value) = @_;
99              
100 466         277 my $comparator_func = \&{'jp_' . $node->{value}};
  466         583  
101 466 50       625 if ( not defined &$comparator_func ) {
102 0         0 Jmespath::UnknownFunctionException
103             ->new({ message => 'unknown-function: Unknown function: ' . $comparator_func })
104             ->throw;
105             }
106              
107 466         749 return &$comparator_func( $self->visit( @{$node->{children}}[0], $value ),
108 466         351 $self->visit( @{$node->{children}}[1], $value ) );
  466         736  
109             }
110              
111             sub visit_current {
112 66     66 0 59 my ( $self, $node, $value ) = @_;
113 66         100 return $value;
114             }
115              
116             sub visit_expref {
117 28     28 0 29 my ( $self, $node, $value ) = @_;
118 28         73 return Jmespath::Expression->new($node->{children}[0], $self);
119             }
120              
121             sub visit_function_expression {
122 208     208 0 195 my ($self, $node, $value) = @_;
123 208         149 my $function = \&{'jp_' . $node->{value}};
  208         314  
124 208 100       324 if ( not defined &$function ) {
125 1         16 Jmespath::UnknownFunctionException
126             ->new({ message => 'unknown-function: Unknown function: ' . $function })
127             ->throw;
128             }
129              
130 207         170 my $resolved_args = [];
131 207         139 foreach my $child ( @{$node->{ children}} ) {
  207         269  
132 301         379 my $current = $self->visit($child, $value);
133 301         202 push @{$resolved_args}, $current;
  301         456  
134             }
135              
136 207         460 return &$function(@$resolved_args);
137             }
138              
139             sub visit_filter_projection {
140 82     82 0 84 my ($self, $node, $value) = @_;
141 82         73 my $base = $self->visit( @{$node->{children}}[0], $value);
  82         155  
142 82 100       174 return if ref($base) ne 'ARRAY';
143 74 50       104 return if scalar @$base == 0;
144              
145 74         55 my $comparator_node = @{ $node->{children} }[2];
  74         84  
146 74         71 my $collected = [];
147             # use Data::Dumper;
148 74         90 foreach my $element (@$base) {
149             # print Dumper $comparator_node;
150             # print Dumper $element;
151 447         2012 my $cnode_result = $self->visit($comparator_node, $element);
152             # print "cnode_result: $cnode_result\n";
153 447 100       642 if ( $self->_is_true($cnode_result) ) {
154 206         1271 my $current = $self->visit(@{$node->{children}}[1], $element);
  206         339  
155 206 100       292 if (defined $current) {
156 203         122 push @{$collected}, $current;
  203         369  
157             }
158             }
159             }
160 74         406 return $collected;
161             }
162              
163             sub visit_flatten {
164 93     93 0 86 my ($self, $node, $value) = @_;
165 93         77 my $base = $self->visit(@{$node->{'children'}}[0], $value);
  93         166  
166              
167 93 100       205 return if ref($base) ne 'ARRAY';
168              
169 81         68 my $merged_list = [];
170 81         104 foreach my $element (@$base) {
171 186 100       213 if (ref($element) eq 'ARRAY') {
172 87         121 push @$merged_list, @$element;
173             }
174             else {
175 99         121 push @$merged_list, $element;
176             }
177             }
178 81         139 return $merged_list;
179             }
180              
181             sub visit_identity {
182 624     624 0 525 my ($self, $node, $value) = @_;
183 624 100       792 return if not defined $value;
184             # SHEER NEGATIVE ENERGY HACKERY - FORCE NUMBERS TO BE NUMBERS
185             # THANK YOU JSON.PM
186 615 50       1032 $value = 1 * $value if $value =~ /^[-][0-9]+$/;
187 615         964 return $value;
188             }
189              
190             sub visit_index {
191 222     222 0 185 my ($self, $node, $value) = @_;
192 222 100       383 return if ref($value) ne 'ARRAY';
193 169         114 my $result;
194             try {
195 169     169   2738 $result = $value->[ $node->{value} ];
196             } catch {
197 0     0   0 Jmespath::IndexException->new({ message => 'Invalid index' })->throw;
198 169         549 };
199 169         1293 return $result;
200             }
201              
202             sub visit_index_expression {
203 213     213 0 185 my ($self, $node, $value) = @_;
204 213         139 my $result = $value;
205 213         161 foreach my $node (@{$node->{children}}) {
  213         275  
206 473         625 $result = $self->visit($node, $result);
207             }
208 212         301 return $result;
209             }
210              
211              
212             # Rules:
213             #
214              
215             sub visit_slice {
216 38     38 0 34 my ($self, $node, $value) = @_;
217              
218             # Rule 08: If the element being sliced is an array and yields no
219             # results, the result MUST be an empty array.
220 38         31 my $selected = [];
221 38         32 my ($start, $stop);
222              
223             # Rule 05: If the given step is omitted, it it assumed to be 1.
224 38 100       63 my $step = defined $node->{children}->[2] ? $node->{children}->[2] : 1;
225              
226             # Rule 07: If the element being sliced is not an array, the result is null.
227 38 100       74 return if ref($value) ne 'ARRAY';
228              
229             # Rule 06: If the given step is 0, an error MUST be raised.
230 36 100       58 if ($step == 0) {
231 1         10 Jmespath::ValueException->new(message => 'Invalid slice expression')->throw;
232             }
233 35 50       24 if (scalar @{$node->{children}} > 3) {
  35         67  
234 0         0 Jmespath::ValueException->new(message => 'Invalid slice expression')->throw;
235             }
236              
237             # Rule 02: If no start position is given, it is assumed to be 0 if
238             # the given step is greater than 0 or the end of the array
239             # if the given step is less than 0.
240 35 100 100     144 if (not defined $node->{children}->[0] and $step > 0) {
    100 66        
    100          
241 13         13 $start = 0;
242             }
243             elsif (not defined $node->{children}->[0] and $step < 0) {
244 4         4 $start = scalar(@$value);
245             }
246             elsif ( $node->{children}->[0] < 0) {
247 1         2 $start = scalar(@$value) + $node->{children}->[0];
248             }
249             else {
250 17         16 $start = $node->{children}->[0];
251             }
252              
253             # Rule 01: If a negative start position is given, it is calculated as the
254             # total length of the array plus the given start position.
255             # if ($start < 0) {
256             # $start = scalar(@$value) + $start;
257             # }
258              
259             # Rule 04: If no stop position is given, it is assumed to be the
260             # length of the array if the given step is greater than 0
261             # or 0 if the given step is less than 0.
262 35 100 100     186 if (not defined $node->{children}->[1] and $step > 0) {
    100 66        
    100 100        
    100 66        
263 11         7 $stop = scalar(@$value);
264             }
265             elsif (not defined $node->{children}->[1] and $step < 0) {
266 3         3 $stop = -1;
267             }
268             # Rule 03: If a negative stop position is given, it is calculated as
269             # the total length of the array plus the given stop
270             # position.
271             elsif ($node->{children}->[1] < 0 and $step < 0) {
272 2         4 $stop = scalar(@$value) + $node->{children}->[1];
273             }
274             elsif ($node->{children}->[1] < 0 and $step > 0) {
275 2         3 $stop = scalar(@$value) + $node->{children}->[1];
276             }
277             else {
278 17         14 $stop = $node->{children}->[1];
279             }
280              
281 35 100       42 if ($step > 0) {
282 27         48 for ( my $idx = $start; $idx < $stop; $idx += $step ) {
283 169         101 push @$selected, @{$value}[$idx];
  169         140  
284 169 100       318 last if $idx == scalar(@$value) - 1
285             }
286             }
287             else {
288 8         15 for ( my $idx = $start; $idx > $stop; $idx += $step ) {
289 53         44 push @$selected, @{$value}[$idx];
  53         59  
290 53 100       81 last if $idx == 0;
291             }
292             }
293 35         61 return $selected;
294             }
295              
296             sub visit_key_val_pair {
297 68     68 0 65 my ($self, $node, $value) = @_;
298 68         57 return $self->visit(@{$node->{children}}[0], $value);
  68         116  
299             }
300              
301             sub visit_literal {
302 559     559 0 505 my ($self, $node, $value) = @_;
303 559         843 return $node->{value};
304             }
305              
306             sub visit_multi_select_hash {
307 42     42 0 41 my ($self, $node, $value) = @_;
308 42 100       71 return if not defined $value;
309 38         35 my %merged;
310 38         30 foreach my $child (@{$node->{children}}) {
  38         56  
311 68         89 my $result = $self->visit($child, $value);
312 68 50       117 return if not defined $child->{value};
313 68         216 %merged = (%merged,( $child->{value} , $result ));
314             }
315 38         64 return \%merged;
316             }
317              
318             sub visit_multi_select_list {
319 46     46 0 41 my ($self, $node, $value) = @_;
320 46 100       88 return if not defined $value;
321 42 50       21 return if scalar @{$node->{children}} == 0;
  42         85  
322              
323 42         40 my $collected = [];
324 42         27 foreach my $child ( @{$node->{children}}) {
  42         51  
325 76         101 my $result = $self->visit($child, $value);
326 76 100       132 my $to_collect = defined $result ? $self->visit($child, $value) : undef;
327 76         125 push @$collected, $to_collect;
328             }
329 42         58 return $collected;
330             }
331              
332             sub visit_or_expression {
333 64     64 0 59 my ($self, $node, $value) = @_;
334 64         54 my $matched = $self->visit( @{$node->{children}}[0], $value );
  64         151  
335 64 100       113 if ( $self->_is_false($matched)) {
336 34         179 $matched = $self->visit(@{$node->{children}}[1], $value);
  34         63  
337             }
338 64         200 return $matched;
339             }
340              
341             sub visit_and_expression {
342 36     36 0 36 my ($self, $node, $value) = @_;
343 36         31 my $matched = $self->visit(@{$node->{children}}[0], $value);
  36         62  
344             # return if the left side eval is found to be false
345 36 100       61 return $matched if $self->_is_false($matched);
346             # if this isn't true then the whole evaluation is false
347 24         143 $matched = $self->visit(@{$node->{children}}[1], $value);
  24         46  
348 24         38 return $matched;
349             }
350              
351             sub visit_not_expression {
352 24     24 0 24 my ($self, $node, $value) = @_;
353 24         21 my $original_result = $self->visit(@{$node->{children}}[0], $value);
  24         41  
354 24 100       29 return JSON::true if $self->_is_false($original_result) == 1;
355 10         38 return JSON::false;
356             }
357              
358             sub visit_pipe {
359 21     21 0 20 my ($self, $node, $value) = @_;
360 21         22 my $result = $value;
361 21         15 foreach my $node ( @{$node->{children}}) {
  21         31  
362 42         57 $result = $self->visit($node, $result);
363             }
364 21         33 return $result;
365             }
366              
367             sub visit_projection {
368 200     200 0 188 my ($self, $node, $value) = @_;
369 200         169 my $base = $self->visit(@{$node->{children}}[0], $value);
  200         356  
370 199 100       375 return if ref($base) ne 'ARRAY';
371              
372 169         157 my $collected = [];
373 169         181 foreach my $element (@$base) {
374 720         479 my $current = $self->visit(@{$node->{children}}[1], $element);
  720         1109  
375 720 100       1444 push (@$collected, $current) if defined $current;
376             }
377 169         307 return $collected;
378             }
379              
380             sub visit_value_projection {
381 63     63 0 59 my ($self, $node, $value) = @_;
382 63         58 my $base = $self->visit(@{$node->{children}}[0], $value);
  63         112  
383 63         53 my @basekeys;
384             try {
385 63     63   1112 @basekeys = map { $base->{ $_ } } sort keys %$base ;
  103         161  
386             } catch {
387 14     14   96 return;
388 63         208 };
389 63 100       417 return if scalar @basekeys == 0;
390 46         48 my $collected = [];
391 46         51 foreach my $element (@basekeys) {
392 103         89 my $current = $self->visit(@{$node->{children}}[1], $element);
  103         145  
393 103 100       206 push( @$collected, $current ) if defined $current;
394             }
395 46         84 return $collected;
396             }
397              
398             sub _is_false {
399 571     571   457 my ($self, $value) = @_;
400 571 100       772 return 1 if not defined $value;
401             # return 1 if JSON::is_bool($value) and $value == JSON::false;
402             # return 0 if JSON::is_bool($value) and $value == JSON::true;
403 557 100 100     1235 return 1 if ref($value) eq 'JSON::PP::Boolean' and $value == JSON::false;
404 284 100 66     2036 return 0 if ref($value) eq 'JSON::PP::Boolean' and $value == JSON::true;
405 41 100 100     103 return 1 if ref($value) eq 'ARRAY' and scalar @$value == 0;
406 31 100 100     59 return 1 if ref($value) eq 'HASH' and scalar keys %$value == 0;
407 29 50 33     45 return 1 if ref($value) eq 'SCALAR' and $value eq '';
408 29 100       42 return 1 if $value eq '';
409 27         43 return 0;
410             }
411              
412             sub _is_true {
413 447     447   539 return ! shift->_is_false(shift);
414             }
415              
416             1;
417              
418             __END__
419              
420             =head1 NAME
421              
422             Jmespath::TreeInterpreter
423