File Coverage

blib/lib/JSON/Path/Evaluator.pm
Criterion Covered Total %
statement 361 408 88.4
branch 167 216 77.3
condition 32 58 55.1
subroutine 37 39 94.8
pod 3 3 100.0
total 600 724 82.8


line stmt bran cond sub pod time code
1             package JSON::Path::Evaluator;
2             $JSON::Path::Evaluator::VERSION = '1.0.6';
3 20     20   1290058 use strict;
  20         49  
  20         836  
4 20     20   165 use warnings;
  20         53  
  20         1189  
5              
6             # ABSTRACT: A module that recursively evaluates JSONPath expressions with native support for Javascript-style filters
7              
8 20     20   122 use Carp;
  20         35  
  20         1553  
9 20     20   11647 use Carp::Assert qw(assert);
  20         29237  
  20         148  
10 20     20   6040 use JSON::MaybeXS;
  20         68618  
  20         3577  
11 20     20   10043 use JSON::Path::Constants qw(:operators :symbols);
  20         86  
  20         378  
12 20     20   171502 use JSON::Path::Tokenizer qw(tokenize);
  20         81  
  20         237  
13 20     20   5943 use List::Util qw/pairs uniq/;
  20         38  
  20         1901  
14 20     20   116 use Readonly;
  20         33  
  20         933  
15 20     20   12582 use Safe;
  20         255100  
  20         1158  
16 20     20   206 use Scalar::Util qw/looks_like_number blessed refaddr/;
  20         45  
  20         1700  
17 20     20   14458 use Storable qw/dclone/;
  20         98468  
  20         2325  
18 20     20   12709 use Try::Tiny;
  20         53889  
  20         1947  
19              
20             # VERSION
21 20     20   184 use Exporter::Shiny qw/evaluate_jsonpath/;
  20         46  
  20         229  
22             our $AUTHORITY = 'cpan:POPEFELIX';
23              
24             Readonly my $OPERATOR_IS_TRUE => 'IS_TRUE';
25             Readonly my $OPERATOR_TYPE_PATH => 1;
26             Readonly my $OPERATOR_TYPE_COMPARISON => 2;
27             Readonly my %OPERATORS => (
28             $TOKEN_ROOT => $OPERATOR_TYPE_PATH, # $
29             $TOKEN_CURRENT => $OPERATOR_TYPE_PATH, # @
30             $TOKEN_CHILD => $OPERATOR_TYPE_PATH, # . OR []
31             $TOKEN_RECURSIVE => $OPERATOR_TYPE_PATH, # ..
32             $TOKEN_ALL => $OPERATOR_TYPE_PATH, # *
33             $TOKEN_FILTER_OPEN => $OPERATOR_TYPE_PATH, # ?(
34             $TOKEN_SCRIPT_OPEN => $OPERATOR_TYPE_PATH, # (
35             $TOKEN_FILTER_SCRIPT_CLOSE => $OPERATOR_TYPE_PATH, # )
36             $TOKEN_SUBSCRIPT_OPEN => $OPERATOR_TYPE_PATH, # [
37             $TOKEN_SUBSCRIPT_CLOSE => $OPERATOR_TYPE_PATH, # ]
38             $TOKEN_UNION => $OPERATOR_TYPE_PATH, # ,
39             $TOKEN_ARRAY_SLICE => $OPERATOR_TYPE_PATH, # [ start:end:step ]
40             $TOKEN_SINGLE_EQUAL => $OPERATOR_TYPE_COMPARISON, # =
41             $TOKEN_DOUBLE_EQUAL => $OPERATOR_TYPE_COMPARISON, # ==
42             $TOKEN_TRIPLE_EQUAL => $OPERATOR_TYPE_COMPARISON, # ===
43             $TOKEN_GREATER_THAN => $OPERATOR_TYPE_COMPARISON, # >
44             $TOKEN_LESS_THAN => $OPERATOR_TYPE_COMPARISON, # <
45             $TOKEN_NOT_EQUAL => $OPERATOR_TYPE_COMPARISON, # !=
46             $TOKEN_GREATER_EQUAL => $OPERATOR_TYPE_COMPARISON, # >=
47             $TOKEN_LESS_EQUAL => $OPERATOR_TYPE_COMPARISON, # <=
48             $TOKEN_REGEX => $OPERATOR_TYPE_COMPARISON, # =~
49             );
50              
51             Readonly my $ASSERT_ENABLE => $ENV{ASSERT_ENABLE};
52              
53              
54             sub new {
55 147     147 1 339 my $class = shift;
56 147 50       887 my %args = ref $_[0] eq 'HASH' ? %{ $_[0] } : @_;
  0         0  
57 147         298 my $self = {};
58 147         367 for my $key (qw/root expression/) {
59 294 50       877 croak qq{Missing required argument '$key' in constructor} unless $args{$key};
60 294         741 $self->{$key} = $args{$key};
61             }
62 147   50     854 $self->{want_ref} = $args{want_ref} || 0;
63 147   50     526 $self->{_calling_context} = $args{_calling_context} || 0;
64              
65             my $script_engine =
66 147 100       827 $args{script_engine} ? $args{script_engine} : $self->{expression} =~ /\$_/ ? 'perl' : undef;
    100          
67 147   100     608 $self->{script_engine} = $script_engine || 'PseudoJS';
68 147         327 bless $self, $class;
69 147         501 return $self;
70             }
71              
72              
73             sub evaluate_jsonpath {
74 147     147 1 1582718 my ( $json_object, $expression, %args ) = @_;
75 147 100       584 if ( !ref $json_object ) {
76             try {
77 90     90   8421 $json_object = decode_json($json_object);
78             }
79             catch {
80 0     0   0 croak qq{Unable to decode $json_object as JSON: $_};
81             }
82 90         939 }
83              
84 147   100     2410 my $want_ref = delete $args{want_ref} || 0;
85 147   100     718 my $want_path = delete $args{want_path} || 0;
86              
87 147 100       945 my $self = __PACKAGE__->new(
88             root => $json_object,
89             expression => $expression,
90             _calling_context => wantarray ? 'ARRAY' : 'SCALAR',
91             %args
92             );
93 147         559 return $self->evaluate( $expression, want_ref => $want_ref, want_path => $want_path );
94             }
95              
96              
97             sub evaluate {
98 147     147 1 596 my ( $self, $expression, %args ) = @_;
99              
100 147         440 my $json_object = $self->{root};
101              
102 147         688 my $token_stream = [ tokenize($expression) ];
103             #use Data::Dumper qw/Dumper/;
104             #print Dumper $token_stream;
105 147 50       637 shift @{$token_stream} if $token_stream->[0] eq $TOKEN_ROOT;
  147         956  
106 147 100       453 shift @{$token_stream} if $token_stream->[0] eq $TOKEN_CHILD;
  128         4589  
107              
108 147 100       572 if ( $args{want_path} ) {
109 12         51 my %reftable = $self->_reftable_walker($json_object);
110 12         970 my @refs = $self->_evaluate( $json_object, dclone $token_stream, 1 );
111              
112 12         35 my @paths;
113 12         47 for my $ref (@refs) {
114 18 100       28 my $refaddr = ref ${$ref} ? refaddr ${$ref} : refaddr $ref;
  18         48  
  10         24  
115 18         92 push @paths, $reftable{$refaddr};
116             }
117 12         309 return @paths;
118             }
119              
120 135         545 my @ret = $self->_evaluate( $json_object, $token_stream, $args{want_ref} );
121 135         2304 return @ret;
122             }
123              
124             sub _reftable_walker {
125 82     82   149 my ( $self, $json_object, $base_path ) = @_;
126              
127 82   100     193 $base_path //= '$';
128 82   33     134 $json_object //= $self->root;
129              
130 82         177 my @entries = ( refaddr $json_object => $base_path );
131              
132 82 100       144 if ( _arraylike($json_object) ) {
    100          
133 9         16 for ( 0 .. $#{$json_object} ) {
  9         32  
134 33         97 my $path = sprintf q{%s['%d']}, $base_path, $_;
135 33 50       8243 if ( ref $json_object->[$_] ) {
136 33         85 push @entries, $self->_reftable_walker( $json_object->[$_], $path );
137             }
138             else {
139 0         0 push @entries, refaddr \( $json_object->[$_] ) => $path;
140             }
141             }
142             }
143             elsif ( _hashlike($json_object) ) {
144 67         85 for my $index ( keys %{$json_object} ) {
  67         170  
145 241         407 my $path = sprintf q{%s['%s']}, $base_path, $index;
146 241 100       395 if ( ref $json_object->{$index} ) {
147 37         189 push @entries, $self->_reftable_walker( $json_object->{$index}, $path );
148             }
149             else {
150 204         407 push @entries, refaddr \( $json_object->{$index} ) => $path;
151             }
152             }
153             }
154 82         813 return @entries;
155             }
156              
157             sub _evaluate { # This assumes that the token stream is syntactically valid
158 868     868   2219 my ( $self, $obj, $token_stream, $want_ref ) = @_;
159              
160 868 100       1959 return unless ref $obj;
161              
162 784   50     1580 $token_stream //= [];
163              
164 784         1193 while ( defined( my $token = shift @{$token_stream} ) ) {
  1539         6942  
165 1539 100       3808 next if $token eq $TOKEN_CURRENT;
166 1255 100       6808 next if $token eq $TOKEN_CHILD;
167              
168 784 100       4951 if ( $token eq $TOKEN_FILTER_OPEN ) {
    100          
169 73         370 my $filter_expression = shift @{$token_stream};
  73         167  
170              
171 73         151 my $closing_token = shift @{$token_stream};
  73         177  
172 73 50       191 assert( $closing_token eq $TOKEN_FILTER_SCRIPT_CLOSE, q{Closing token seen} ) if $ASSERT_ENABLE;
173              
174             # Find all indices matching the filter expression. This modifies $token_stream
175 73         662 my @matching_indices = $self->_process_filter( $obj, $filter_expression );
176              
177 73 100       109 if ( !@{$token_stream} ) {
  73         181  
178 39         91 my @got = map { _get( $obj, $_ ) } @matching_indices;
  45         110  
179 39 100       195 return $want_ref ? @got : map { ${$_} } @got;
  35         50  
  35         236  
180             }
181             else {
182 34         155 return map { $self->_evaluate( _get( $obj, $_ ), dclone($token_stream), $want_ref ) } @matching_indices;
  31         95  
183             }
184             }
185             elsif ( $token eq $TOKEN_RECURSIVE )
186             { # Sweet Jesus, Pooh, that's not honey! You're eating Sweet Jesus, Pooh, that's not honey! You're eating...
187 16         162 my $index = shift @{$token_stream};
  16         37  
188 16         32 my @matched;
189 16 100       37 if ( $index eq $TOKEN_FILTER_OPEN ) {
190 2         39 my $filter_expression = shift @{$token_stream};
  2         5  
191              
192 2         5 my $closing_token = shift @{$token_stream};
  2         3  
193 2 50       8 assert( $closing_token eq $TOKEN_FILTER_SCRIPT_CLOSE, q{Closing token seen} ) if $ASSERT_ENABLE;
194              
195 2         29 return $self->_filter_recursive( $obj, $filter_expression, $want_ref );
196             }
197              
198 14         107 @matched = _match_recursive( $obj, $index, $want_ref );
199 14 100       24 if ( !@{$token_stream} ) {
  14         41  
200 6         30 return @matched;
201             }
202 8         27 return map { $self->_evaluate( $_, dclone($token_stream), $want_ref ) } @matched;
  8         566  
203             }
204             else {
205 695         5929 my $index;
206 695 100       1367 if ( $token eq $TOKEN_SUBSCRIPT_OPEN ) {
207 98         509 $index = shift @{$token_stream};
  98         197  
208 98         166 my $closing_token = shift @{$token_stream};
  98         187  
209 98 50       242 assert $closing_token eq $TOKEN_SUBSCRIPT_CLOSE if $ASSERT_ENABLE;
210             }
211             else {
212 597         4670 $index = $token;
213             }
214              
215 695 100       2425 assert( !$OPERATORS{$index}, qq{"$index" is not an operator} ) if $index ne $TOKEN_ALL;
216 695 50       12709 assert( !ref $index, q{Index is a scalar} ) if $ASSERT_ENABLE;
217              
218 695         5640 my (@got) = _get( $obj, $index, create_key => $want_ref ); # This always returns a ref
219 695 100       996 if ( !@{$token_stream} ) {
  695         1377  
220 419 100       1171 return $want_ref ? @got : map { ${$_} } @got;
  353         492  
  353         1743  
221             }
222             else {
223 276         585 return map { $self->_evaluate( ${$_}, dclone($token_stream), $want_ref ) } @got;
  314         462  
  314         10491  
224             }
225             }
226             }
227             }
228              
229             sub _process_filter {
230 113     113   290 my ( $self, $obj, $filter_expression ) = @_;
231              
232 113         233 my @matching_indices;
233 113 100       395 if ( $self->{script_engine} eq 'PseudoJS' ) {
    50          
234 106         447 @matching_indices = $self->_process_pseudo_js( $obj, $filter_expression );
235             }
236             elsif ( $self->{script_engine} eq 'perl' ) {
237 7         85 @matching_indices = $self->_process_perl( $obj, $filter_expression );
238             }
239             else {
240 0         0 croak qq{Unsupported script engine "$self->{script_engine}"};
241             }
242 113         1266 return @matching_indices;
243             }
244              
245             # This _always_ has to return a ref so that when it's called from evaluate( ... , want_ref => 1)
246             # So that we can return a ref into the object (e.g. for use as an lvalue), even when the path points
247             # to a scalar (which will of course be copied).
248             #
249             # I.E.: for { foo => 'bar' }, we always want \( foo->{bar} ) so that
250             # JSON::Path->new('$.foo')->value($obj) = 'baz' works like it oughtta.
251             sub _get {
252 897     897   2662 my ( $object, $index, %args ) = @_;
253              
254 897 50 0     2095 assert( _hashlike($object) || _arraylike($object), 'Object is a hashref or an arrayref' ) if $ASSERT_ENABLE;
255              
256 897         6804 my $create_key = $args{create_key};
257              
258             # When want_ref is passed to _evaluate(), it will return a reference to whatever was matched.
259             # If what was matched is itself a ref (e.g. an arrayref), _evaluate() will return a ref of
260             # type 'REF'.
261 897 100       2282 if ( ref $object eq 'REF' ) {
262 32         41 $object = ${$object};
  32         83  
263             }
264              
265 897 100       1949 if ( $index eq $TOKEN_ALL ) {
266 29 100       199 if ( _hashlike($object) ) {
267 3         8 return map { \($_) } values %{$object};
  14         37  
  3         13  
268             }
269             else {
270 26         55 return map { \($_) } @{$object};
  76         321  
  26         65  
271             }
272             }
273             else {
274 868         4081 my @indices;
275 868 100       2727 if ( $index =~ /$TOKEN_ARRAY_SLICE/ ) {
    100          
276 9 50       102 my $length = _hashlike($object) ? scalar values %{$object} : scalar @{$object};
  0         0  
  9         25  
277 9         39 @indices = _slice( $index, $length );
278             }
279             elsif ( $index =~ /$TOKEN_UNION/ ) {
280 5         61 @indices = split /$TOKEN_UNION/, $index;
281             }
282             else {
283 854         10696 @indices = ($index);
284             }
285              
286 868 100       2087 if ( _hashlike($object) ) {
287 689 100       1291 if ($create_key) {
288 85         154 return map { \( $object->{$_} ) } @indices;
  86         374  
289             }
290             else {
291 604         836 my @ret;
292 604         1303 for my $index (@indices) {
293 605 100       2183 push @ret, \( $object->{$index} ) if exists $object->{$index};
294             }
295 604         2569 return @ret;
296             }
297             }
298             else {
299 179         368 my @numeric_indices = grep { looks_like_number($_) } @indices;
  185         839  
300 179 100       443 if ($create_key) {
301 18         35 return map { \( $object->[$_] ) } @numeric_indices;
  20         84  
302             }
303             else {
304 161         237 my @ret;
305 161         344 for my $index (@numeric_indices) {
306 152 50       632 push @ret, \( $object->[$index] ) if exists $object->[$index];
307             }
308 161         1131 return @ret;
309             }
310             }
311             }
312             }
313              
314             sub _indices {
315 40     40   66 my $object = shift;
316             return
317 27         100 _hashlike($object) ? keys %{$object}
318 40 100       71 : _arraylike($object) ? ( 0 .. $#{$object} )
  11 100       81  
319             : ();
320             }
321              
322             sub _hashlike {
323 1366     1366   2255 my $object = shift;
324 1366   66     5061 return ( ref $object eq 'HASH' || ( blessed $object && $object->can('typeof') && $object->typeof eq 'HASH' ) );
325             }
326              
327             sub _arraylike {
328 462     462   767 my $object = shift;
329 462   66     2102 return ( ref $object eq 'ARRAY' || ( blessed $object && $object->can('typeof') && $object->typeof eq 'ARRAY' ) );
330             }
331              
332             sub _get_token {
333 0     0   0 my $token_stream = shift;
334 0         0 my $token = shift @{$token_stream};
  0         0  
335 0 0       0 return unless defined $token;
336              
337 0 0       0 if ( $token eq $TOKEN_SUBSCRIPT_OPEN ) {
338 0         0 my @substream;
339             my $close_seen;
340 0         0 while ( defined( my $token = shift @{$token_stream} ) ) {
  0         0  
341 0 0       0 if ( $token eq $TOKEN_SUBSCRIPT_CLOSE ) {
342 0         0 $close_seen = 1;
343 0         0 last;
344             }
345 0         0 push @substream, $token;
346             }
347              
348 0 0       0 assert($close_seen) if $ASSERT_ENABLE;
349              
350 0 0       0 if ( grep { $_ eq $TOKEN_ARRAY_SLICE } @substream ) {
  0 0       0  
351              
352             # There are five valid cases:
353             #
354             # n:m -> n:m:1
355             # n:m:s -> n:m:s
356             # :m -> 0:m:1
357             # ::s -> 0:-1:s
358             # n: -> n:-1:1
359 0 0       0 if ( $substream[0] eq $TOKEN_ARRAY_SLICE ) {
360 0         0 unshift @substream, undef;
361             }
362              
363 20     20   58458 no warnings qw/uninitialized/;
  20         56  
  20         2581  
364 0 0       0 if ( $substream[2] eq $TOKEN_ARRAY_SLICE ) {
365 0         0 @substream = ( @substream[ ( 0, 1 ) ], undef, @substream[ ( 2 .. $#substream ) ] );
366             }
367 20     20   138 use warnings qw/uninitialized/;
  20         48  
  20         47881  
368              
369 0         0 my ( $start, $end, $step );
370 0   0     0 $start = $substream[0] // 0;
371 0   0     0 $end = $substream[2] // -1;
372 0   0     0 $step = $substream[4] // 1;
373 0         0 return { slice => [ $start, $end, $step ] };
374             }
375 0         0 elsif ( grep { $_ eq $TOKEN_UNION } @substream ) {
376 0         0 my @union = grep { $_ ne $TOKEN_UNION } @substream;
  0         0  
377 0         0 return { union => \@union };
378             }
379              
380 0         0 return $substream[0];
381             }
382 0         0 return $token;
383             }
384              
385             # See http://wiki.ecmascript.org/doku.php?id=proposals:slice_syntax
386             #
387             # in particular, for the slice [n:m], m is *one greater* than the last index to slice.
388             # This means that the slice [3:5] will return indices 3 and 4, but *not* 5.
389             sub _slice {
390 9     9   26 my ( $index, $length ) = @_;
391              
392 9         34 my ( $start, $end, $step ) = split /$TOKEN_ARRAY_SLICE/, $index, 3;
393              
394 9 50 33     168 if ( !defined($start) || $start eq '' ) {
395 0         0 $start = 0;
396             }
397 9 100 66     50 if ( !defined($end) || $end eq '' ) {
398 6         15 $end = -1;
399             }
400 9 50 33     50 if ( !defined($step) || $step eq '' ) {
401 9         16 $step = 1;
402             }
403              
404 9 100       86 $start = ( $length - 1 ) if $start == -1;
405 9 100       32 $end = $length if $end == -1;
406              
407 9         16 my @indices;
408 9 50       28 if ( $step < 0 ) {
409 0         0 @indices = grep { %_ % -$step == 0 } reverse( $start .. ( $end - 1 ) );
  0         0  
410             }
411             else {
412 9         35 @indices = grep { $_ % $step == 0 } ( $start .. ( $end - 1 ) );
  12         48  
413             }
414 9         28 return @indices;
415             }
416              
417             sub _match_recursive {
418 281     281   571 my ( $obj, $index, $want_ref ) = @_;
419              
420 281         436 my @match;
421              
422             # Fix for RT #122529.
423             #
424             # Consider the expression "$..foo..bar", evaluated with respect to the JSON "{"foo":{"bar":"baz"}}".
425             #
426             # The first term to be evaluated in the expression is "$..foo". If want_ref is passed to evaluate(),
427             # this will return a REF reference. In that case we must first dereference it to get the object that
428             # we will evaluate "..bar" with respect to.
429 281 100       687 if ( ref $obj eq 'REF' ) {
430 2         2 $obj = ${$obj};
  2         4  
431             }
432              
433 281 100       540 if ( _arraylike($obj) ) {
    100          
434 82 0 33     247 if ( looks_like_number($index) && exists $obj->[$index] ) {
435 0 0       0 push @match, $want_ref ? \( $obj->[$index] ) : $obj->[$index];
436             }
437 82         133 for ( 0 .. $#{$obj} ) {
  82         202  
438 251 100       570 next unless ref $obj->[$_];
439 115         240 push @match, _match_recursive( $obj->[$_], $index, $want_ref );
440             }
441             }
442             elsif ( _hashlike($obj) ) {
443 191 100       474 if ( exists $obj->{$index} ) {
444 16 100       123 push @match, $want_ref ? \( $obj->{$index} ) : $obj->{$index};
445             }
446 191         293 for my $val ( values %{$obj} ) {
  191         465  
447 591 100       1440 next unless ref $val;
448 152         337 push @match, _match_recursive( $val, $index, $want_ref );
449             }
450             }
451 281         713 return @match;
452             }
453              
454             sub _filter_recursive {
455 40     40   92 my ( $self, $obj, $expression, $want_ref ) = @_;
456              
457 40         65 my @ret;
458              
459             # Evaluate the filter expression for the current object
460 40         95 my @matching_indices = $self->_process_filter( $obj, $expression );
461 40         72 for my $index (@matching_indices) {
462 4         7 my ($got) = _get( $obj, $index );
463 4 100       11 push @ret, $want_ref ? $got : ${$got};
  2         6  
464             }
465              
466             # Evaluate the filter expression for any subordinate objects
467 40         79 for my $index ( _indices($obj) ) {
468 122         244 my ($got) = _get( $obj, $index );
469 122         194 $got = ${$got}; # _get will always return a reference. We want the value, so dereference it
  122         263  
470 122 100       359 next unless ref $got;
471 38         121 push @ret, $self->_filter_recursive( $got, $expression, $want_ref );
472             }
473              
474 40         123 return @ret;
475             }
476              
477             sub _process_pseudo_js {
478 106     106   211 my ( $self, $object, $expressions ) = @_;
479              
480 106         414 my @expressions_or = split /\Q||\E/, $expressions;
481 106         187 my @matching_or;
482              
483 106         211 foreach my $expression (@expressions_or) {
484 117         294 my @expressions_and = split /\Q&&\E/, $expression;
485 117         194 my %matching_and;
486              
487 117         180 foreach my $expression (@expressions_and) {
488              
489 141         334 my ( $lhs, $operator, $rhs ) = _parse_psuedojs_expression($expression);
490 141         789 $lhs =~ s/^\s+|\s+$//g;
491              
492 141         442 my (@token_stream) = tokenize($lhs);
493              
494 141 100       413 if ( _hashlike($object) ) {
    100          
495 55         238 while (my ($k, $v) = each(%$object)) {
496 158         620 my @got = $self->_evaluate( $v, [@token_stream] );
497 158         677 foreach my $got (@got) {
498 56 100       133 $matching_and{$k}++ if _compare( $operator, $got, $rhs );
499             }
500             }
501             }
502             elsif ( _arraylike($object) ) {
503 84         130 my $idx = 0;
504 84         119 for my $value ( @{$object} ) {
  84         194  
505 210         798 my ($got) = $self->_evaluate( $value, [@token_stream] );
506 210 100       547 $matching_and{$idx}++ if _compare( $operator, $got, $rhs );
507 210         565 $idx++;
508             }
509             }
510             }
511 117         485 while (my ($idx, $val) = each(%matching_and)) {
512 96 100       483 push @matching_or, $idx if ($val == @expressions_and);
513             }
514             }
515              
516 106         635 return sort(uniq(@matching_or));
517             }
518              
519             sub _parse_psuedojs_expression {
520 141     141   335 my $expression = shift;
521 141         196 my @parts;
522              
523 141         240 my ( $lhs, $operator, $rhs );
524              
525             # The operator could be '=', '!=', '==', '===', '<=', '>=' or '=~'
526 141 100       520 if ( $expression =~ /$EQUAL_SIGN/ ) {
527 58         563 my $position = index( $expression, '=' );
528 58 100       185 if ( substr( $expression, $position + 1, 1 ) eq $EQUAL_SIGN ) { # could be '==' or '==='
    100          
529 27 50       173 if ( substr( $expression, $position + 2, 1 ) eq $EQUAL_SIGN ) { # ===
530 0         0 $operator = $TOKEN_TRIPLE_EQUAL;
531             }
532             else {
533 27         200 $operator = $TOKEN_DOUBLE_EQUAL;
534             }
535             }
536             elsif ( substr( $expression, $position + 1, 1 ) eq $TILDE_SIGN ) {
537 18         195 $operator = $TOKEN_REGEX;
538             }
539             else {
540 13         126 my $preceding_char = substr( $expression, $position - 1, 1 );
541 13 100       27 if ( $preceding_char eq $GREATER_THAN_SIGN ) {
    50          
    50          
542 11         60 $operator = $TOKEN_GREATER_EQUAL;
543             }
544             elsif ( $preceding_char eq $LESS_THAN_SIGN ) {
545 0         0 $operator = $TOKEN_LESS_EQUAL;
546             }
547             elsif ( $preceding_char eq $EXCLAMATION_MARK ) {
548 0         0 $operator = $TOKEN_NOT_EQUAL;
549             }
550             else {
551 2         27 $operator = $TOKEN_SINGLE_EQUAL;
552             }
553             }
554 58         675 ( $lhs, $rhs ) = split /$operator/, $expression, 2;
555             }
556             else {
557 83         828 for ( grep { $OPERATORS{$_} eq $OPERATOR_TYPE_COMPARISON } keys %OPERATORS ) {
  1743         22470  
558 514 100       3956 next if /$EQUAL_SIGN/;
559 104 100       1271 if ( $expression =~ /$_/ ) {
560 75         361 ( $lhs, $rhs ) = split /$_/, $expression, 2;
561 75         145 $operator = $_;
562 75         169 last;
563             }
564             }
565             }
566              
567             # FIXME: RHS is assumed to be a single value. This isn't necessarily a safe assumption.
568 141 100       724 if ($operator) {
569 133   100     437 $rhs = _normalize( $rhs || '' );
570 133         270 $lhs = _normalize($lhs);
571             }
572             else {
573 8         28 $operator = $OPERATOR_IS_TRUE;
574 8         104 $lhs = $expression;
575             }
576 141         526 return ( $lhs, $operator, $rhs );
577             }
578              
579             sub _normalize {
580 266     266   413 my $string = shift;
581              
582             # NB: Stripping spaces *before* stripping quotes allows the caller to quote spaces in an index.
583             # So an index of 'foo ' will be correctly normalized as 'foo', but '"foo "' will normalize to 'foo '.
584 266         1051 $string =~ s/\s+$//; # trim trailing spaces
585 266         711 $string =~ s/^\s+//; # trim leading spaces
586 266         623 $string =~ s/^['"](.+)['"]$/$1/; # Strip quotes from index
587 266         624 return $string;
588             }
589              
590             sub _process_perl {
591 7     7   21 my ( $self, $object, $code ) = @_;
592              
593 7         115 my $cpt = Safe->new;
594 7         10257 $cpt->permit_only( ':base_core', qw/padsv padav padhv padany rv2gv/ );
595 7         1003 ${ $cpt->varglob('root') } = dclone( $self->{root} );
  7         53  
596              
597 7         167 my @matching;
598 7 100       28 if ( _hashlike($object) ) {
599 2         4 for my $index ( keys %{$object} ) {
  2         10  
600 10         26 local $_ = $object->{$index};
601 10         33 my $ret = $cpt->reval($code);
602 10 50       7308 croak qq{Error in filter: $@} if $@;
603 10 100       45 push @matching, $index if $ret;
604             }
605             }
606             else {
607 5         10 for my $index ( 0 .. $#{$object} ) {
  5         20  
608 18         44 local $_ = $object->[$index];
609 18         52 my $ret = $cpt->reval($code);
610 18 50       11353 croak qq{Error in filter: $@} if $@;
611 18 100       85 push @matching, $index if $ret;
612             }
613             }
614 7         53 return @matching;
615             }
616              
617 0           sub _compare {
618 266     266   608 my ( $operator, $lhs, $rhs ) = @_;
619              
620 20     20   205 no warnings qw/uninitialized/;
  20         55  
  20         6609  
621 266 100       757 if ( $operator eq $OPERATOR_IS_TRUE ) {
622 15 100       143 return $lhs ? 1 : 0;
623             }
624              
625 251   100     2035 my $use_numeric = looks_like_number($lhs) && looks_like_number($rhs);
626              
627 251 100 100     1170 if ( $operator eq '=' || $operator eq '==' || $operator eq '===' ) {
      66        
628 66 100       378 return $use_numeric ? ( $lhs == $rhs ) : $lhs eq $rhs;
629             }
630 185 100       355 if ( $operator eq '<' ) {
631 26 50       130 return $use_numeric ? ( $lhs < $rhs ) : $lhs lt $rhs;
632             }
633 159 100       439 if ( $operator eq '>' ) {
634 83 100       398 return $use_numeric ? ( $lhs > $rhs ) : $lhs gt $rhs;
635             }
636 76 50       144 if ( $operator eq '<=' ) {
637 0 0       0 return $use_numeric ? ( $lhs <= $rhs ) : $lhs le $rhs;
638             }
639 76 100       158 if ( $operator eq '>=' ) {
640 22 100       127 return $use_numeric ? ( $lhs >= $rhs ) : $lhs ge $rhs;
641             }
642 54 50 33     175 if ( $operator eq '!=' || $operator eq '!==' ) {
643 0 0       0 return $use_numeric ? ( $lhs != $rhs ) : $lhs ne $rhs;
644             }
645              
646 54 50       120 if ( $operator eq '=~' ) {
647 54 50       336 if ( $rhs =~ m@^/(.*)/([msian]*)$@ ) {
648 54         151 my $regex = $1;
649 54         114 my $modifiers = $2;
650 54         1414 return $lhs =~ qr/(?$modifiers)$regex/;
651             }
652             else {
653 0           warn("Regexp need to be sorrounded by //. Allowed modifiers: msian");
654 0           return 1;
655             }
656             }
657 20     20   160 use warnings qw/uninitialized/;
  20         39  
  20         2277  
658             }
659              
660             1;
661              
662             __END__