File Coverage

blib/lib/JSON/Path/Evaluator.pm
Criterion Covered Total %
statement 347 401 86.5
branch 148 212 69.8
condition 24 51 47.0
subroutine 40 42 95.2
pod 3 3 100.0
total 562 709 79.2


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