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.431';
3 16     16   558221 use strict;
  16         57  
  16         494  
4 16     16   87 use warnings;
  16         28  
  16         364  
5 16     16   260 use 5.008;
  16         53  
6              
7             # ABSTRACT: A module that recursively evaluates JSONPath expressions with native support for Javascript-style filters
8              
9 16     16   96 use Carp;
  16         51  
  16         1008  
10 16     16   8096 use Carp::Assert qw(assert);
  16         19560  
  16         94  
11 16     16   3815 use Exporter::Tiny ();
  16         9995  
  16         269  
12 16     16   1033 use JSON::MaybeXS;
  16         11935  
  16         885  
13 16     16   6432 use JSON::Path::Constants qw(:operators :symbols);
  16         43  
  16         5641  
14 16     16   7348 use JSON::Path::Tokenizer qw(tokenize);
  16         42  
  16         900  
15 16     16   122 use List::Util qw/pairs/;
  16         32  
  16         916  
16 16     16   88 use Readonly;
  16         30  
  16         513  
17 16     16   8689 use Safe;
  16         478338  
  16         1079  
18 16     16   150 use Scalar::Util qw/looks_like_number blessed refaddr/;
  16         33  
  16         1235  
19 16     16   10319 use Storable qw/dclone/;
  16         45726  
  16         1077  
20 16     16   7425 use Sys::Hostname qw/hostname/;
  16         14977  
  16         968  
21 16     16   7854 use Try::Tiny;
  16         29682  
  16         1054  
22              
23             # VERSION
24 16     16   160 use base q(Exporter);
  16         37  
  16         31842  
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 179 my $class = shift;
59 97 50       433 my %args = ref $_[0] eq 'HASH' ? %{ $_[0] } : @_;
  0         0  
60 97         207 my $self = {};
61 97         217 for my $key (qw/root expression/) {
62 194 50       473 croak qq{Missing required argument '$key' in constructor} unless $args{$key};
63 194         412 $self->{$key} = $args{$key};
64             }
65 97   50     412 $self->{want_ref} = $args{want_ref} || 0;
66 97   50     312 $self->{_calling_context} = $args{_calling_context} || 0;
67              
68             my $script_engine =
69 97 100       367 $args{script_engine} ? $args{script_engine} : $self->{expression} =~ /\$_/ ? 'perl' : undef;
    100          
70 97   100     327 $self->{script_engine} = $script_engine || 'PseudoJS';
71 97         216 bless $self, $class;
72 97         248 return $self;
73             }
74              
75              
76             sub evaluate_jsonpath {
77 97     97 1 272127 my ( $json_object, $expression, %args ) = @_;
78              
79 97 100       303 if ( !ref $json_object ) {
80             try {
81 40     40   4133 $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         275 }
87              
88 97   100     975 my $want_ref = delete $args{want_ref} || 0;
89 97   100     335 my $want_path = delete $args{want_path} || 0;
90              
91 97 100       477 my $self = __PACKAGE__->new(
92             root => $json_object,
93             expression => $expression,
94             _calling_context => wantarray ? 'ARRAY' : 'SCALAR',
95             %args
96             );
97 97         256 return $self->evaluate( $expression, want_ref => $want_ref, want_path => $want_path );
98             }
99              
100              
101             sub evaluate {
102 97     97 1 283 my ( $self, $expression, %args ) = @_;
103              
104 97         238 my $json_object = $self->{root};
105              
106 97         301 my $token_stream = [ tokenize($expression) ];
107 97 50       314 shift @{$token_stream} if $token_stream->[0] eq $TOKEN_ROOT;
  97         507  
108 97 100       247 shift @{$token_stream} if $token_stream->[0] eq $TOKEN_CHILD;
  78         398  
109              
110 97 100       335 if ( $args{want_path} ) {
111 11         40 my %reftable = $self->_reftable_walker($json_object);
112 11         775 my @refs = $self->_evaluate( $json_object, dclone $token_stream, 1 );
113              
114 11         33 my @paths;
115 11         26 for my $ref (@refs) {
116 17 100       41 my $refaddr = ref ${$ref} ? refaddr ${$ref} : refaddr $ref;
  17         55  
  9         25  
117 17         49 push @paths, $reftable{$refaddr};
118             }
119 11         181 return @paths;
120             }
121              
122 86         263 my @ret = $self->_evaluate( $json_object, $token_stream, $args{want_ref} );
123 86         1014 return @ret;
124             }
125              
126             sub _reftable_walker {
127 77     77   140 my ( $self, $json_object, $base_path ) = @_;
128              
129 77 100       153 $base_path = defined $base_path ? $base_path : '$';
130 77 50       136 $json_object = defined $json_object ? $json_object : $self->root;
131              
132 77         191 my @entries = ( refaddr $json_object => $base_path );
133              
134 77 100       148 if ( _arraylike($json_object) ) {
    100          
135 8         23 for ( 0 .. $#{$json_object} ) {
  8         32  
136 30         83 my $path = sprintf q{%s['%d']}, $base_path, $_;
137 30 50       80 if ( ref $json_object->[$_] ) {
138 30         66 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         87 for my $index ( keys %{$json_object} ) {
  63         177  
147 234         537 my $path = sprintf q{%s['%s']}, $base_path, $index;
148 234 100       421 if ( ref $json_object->{$index} ) {
149 36         106 push @entries, $self->_reftable_walker( $json_object->{$index}, $path );
150             }
151             else {
152 198         438 push @entries, refaddr \( $json_object->{$index} ) => $path;
153             }
154             }
155             }
156 77         636 return @entries;
157             }
158              
159             sub _evaluate { # This assumes that the token stream is syntactically valid
160 438     438   1278 my ( $self, $obj, $token_stream, $want_ref ) = @_;
161              
162 438 100       969 return unless ref $obj;
163              
164 354 50       677 $token_stream = defined $token_stream ? $token_stream : [];
165              
166 354         491 while ( defined( my $token = shift @{$token_stream} ) ) {
  600         2336  
167 600 100       1413 next if $token eq $TOKEN_CURRENT;
168 539 100       2726 next if $token eq $TOKEN_CHILD;
169              
170 354 100       1643 if ( $token eq $TOKEN_FILTER_OPEN ) {
    100          
171 13         74 my $filter_expression = shift @{$token_stream};
  13         30  
172              
173 13         25 my $closing_token = shift @{$token_stream};
  13         26  
174 13 50       50 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         130 my @matching_indices = $self->_process_filter( $obj, $filter_expression );
178              
179 13 100       25 if ( !@{$token_stream} ) {
  13         50  
180 11         25 my @got = map { _get( $obj, $_ ) } @matching_indices;
  16         58  
181 11 100       56 return $want_ref ? @got : map { ${$_} } @got;
  7         13  
  7         30  
182             }
183             else {
184 2         6 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         156 my $index = shift @{$token_stream};
  16         30  
190 16         33 my @matched;
191 16 100       34 if ( $index eq $TOKEN_FILTER_OPEN ) {
192 2         12 my $filter_expression = shift @{$token_stream};
  2         5  
193              
194 2         4 my $closing_token = shift @{$token_stream};
  2         4  
195 2 50       7 assert( $closing_token eq $TOKEN_FILTER_SCRIPT_CLOSE, q{Closing token seen} ) if $ASSERT_ENABLE;
196              
197 2         24 return $self->_filter_recursive( $obj, $filter_expression, $want_ref );
198             }
199              
200 14         100 @matched = _match_recursive( $obj, $index, $want_ref );
201 14 100       21 if ( !@{$token_stream} ) {
  14         42  
202 6         50 return @matched;
203             }
204 8         19 return map { $self->_evaluate( $_, dclone($token_stream), $want_ref ) } @matched;
  8         347  
205             }
206             else {
207 325         2548 my $index;
208 325 100       629 if ( $token eq $TOKEN_SUBSCRIPT_OPEN ) {
209 63         257 $index = shift @{$token_stream};
  63         104  
210 63         88 my $closing_token = shift @{$token_stream};
  63         103  
211 63 50       123 assert $closing_token eq $TOKEN_SUBSCRIPT_CLOSE if $ASSERT_ENABLE;
212             }
213             else {
214 262         1108 $index = $token;
215             }
216              
217 325 100       964 assert( !$OPERATORS{$index}, qq{"$index" is not an operator} ) if $index ne $TOKEN_ALL;
218 325 50       4676 assert( !ref $index, q{Index is a scalar} ) if $ASSERT_ENABLE;
219              
220 325         2325 my (@got) = _get( $obj, $index, create_key => $want_ref ); # This always returns a ref
221 325 100       446 if ( !@{$token_stream} ) {
  325         602  
222 176 100       539 return $want_ref ? @got : map { ${$_} } @got;
  112         180  
  112         483  
223             }
224             else {
225 149         272 return map { $self->_evaluate( ${$_}, dclone($token_stream), $want_ref ) } @got;
  184         236  
  184         4115  
226             }
227             }
228             }
229             }
230              
231             sub _process_filter {
232 53     53   107 my ( $self, $obj, $filter_expression ) = @_;
233              
234 53         73 my @matching_indices;
235 53 100       131 if ( $self->{script_engine} eq 'PseudoJS' ) {
    50          
236 47         111 @matching_indices = $self->_process_pseudo_js( $obj, $filter_expression );
237             }
238             elsif ( $self->{script_engine} eq 'perl' ) {
239 6         35 @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         780 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   1076 my ( $object, $index, %args ) = @_;
255              
256 471 50 0     956 assert( _hashlike($object) || _arraylike($object), 'Object is a hashref or an arrayref' ) if $ASSERT_ENABLE;
257              
258 471         3278 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       1104 if ( ref $object eq 'REF' ) {
264 5         10 $object = ${$object};
  5         11  
265             }
266              
267 471 100       852 if ( $index eq $TOKEN_ALL ) {
268 15 100       75 if ( _hashlike($object) ) {
269 1         2 return map { \($_) } values %{$object};
  9         17  
  1         6  
270             }
271             else {
272 14         26 return map { \($_) } @{$object};
  52         100  
  14         26  
273             }
274             }
275             else {
276 456         1977 my @indices;
277 456 100       921 if ( $index =~ /$TOKEN_ARRAY_SLICE/ ) {
    100          
278 9 50       93 my $length = _hashlike($object) ? scalar values %{$object} : scalar @{$object};
  0         0  
  9         21  
279 9         33 @indices = _slice( $index, $length );
280             }
281             elsif ( $index =~ /$TOKEN_UNION/ ) {
282 5         57 @indices = split /$TOKEN_UNION/, $index;
283             }
284             else {
285 442         4458 @indices = ($index);
286             }
287              
288 456 100       877 if ( _hashlike($object) ) {
289 342 100       575 if ($create_key) {
290 84         143 return map { \( $object->{$_} ) } @indices;
  85         358  
291             }
292             else {
293 258         324 my @ret;
294 258         446 for my $index (@indices) {
295 259 100       727 push @ret, \( $object->{$index} ) if exists $object->{$index};
296             }
297 258         814 return @ret;
298             }
299             }
300             else {
301 114         211 my @numeric_indices = grep { looks_like_number($_) } @indices;
  120         451  
302 114 100       236 if ($create_key) {
303 18         33 return map { \( $object->[$_] ) } @numeric_indices;
  20         81  
304             }
305             else {
306 96         129 my @ret;
307 96         160 for my $index (@numeric_indices) {
308 87 50       259 push @ret, \( $object->[$index] ) if exists $object->[$index];
309             }
310 96         321 return @ret;
311             }
312             }
313             }
314             }
315              
316             sub _indices {
317 40     40   67 my $object = shift;
318             return
319 27         85 _hashlike($object) ? keys %{$object}
320 40 100       63 : _arraylike($object) ? ( 0 .. $#{$object} )
  11 100       31  
321             : ();
322             }
323              
324             sub _hashlike {
325 841     841   1336 my $object = shift;
326 841   66     2840 return ( ref $object eq 'HASH' || ( blessed $object && $object->can('typeof') && $object->typeof eq 'HASH' ) );
327             }
328              
329             sub _arraylike {
330 391     391   517 my $object = shift;
331 391   66     1506 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   152 no warnings qw/uninitialized/;
  16         52  
  16         1180  
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   99 use warnings qw/uninitialized/;
  16         37  
  16         23626  
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   27 my ( $index, $length ) = @_;
393              
394 9         56 my ( $start, $end, $step ) = split /$TOKEN_ARRAY_SLICE/, $index, 3;
395              
396 9 50 33     144 if ( !defined($start) || $start eq '' ) {
397 0         0 $start = 0;
398             }
399 9 100 66     42 if ( !defined($end) || $end eq '' ) {
400 6         13 $end = -1;
401             }
402 9 50 33     39 if ( !defined($step) || $step eq '' ) {
403 9         15 $step = 1;
404             }
405              
406 9 100       33 $start = ( $length - 1 ) if $start == -1;
407 9 100       23 $end = $length if $end == -1;
408              
409 9         16 my @indices;
410 9 50       31 if ( $step < 0 ) {
411 0         0 @indices = grep { %_ % -$step == 0 } reverse( $start .. ( $end - 1 ) );
  0         0  
412             }
413             else {
414 9         28 @indices = grep { $_ % $step == 0 } ( $start .. ( $end - 1 ) );
  12         45  
415             }
416 9         26 return @indices;
417             }
418              
419             sub _match_recursive {
420 281     281   495 my ( $obj, $index, $want_ref ) = @_;
421              
422 281         350 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       813 if ( ref $obj eq 'REF' ) {
432 2         3 $obj = ${$obj};
  2         3  
433             }
434              
435 281 100       422 if ( _arraylike($obj) ) {
    100          
436 82 0 33     208 if ( looks_like_number($index) && exists $obj->[$index] ) {
437 0 0       0 push @match, $want_ref ? \( $obj->[$index] ) : $obj->[$index];
438             }
439 82         118 for ( 0 .. $#{$obj} ) {
  82         175  
440 251 100       465 next unless ref $obj->[$_];
441 115         254 push @match, _match_recursive( $obj->[$_], $index, $want_ref );
442             }
443             }
444             elsif ( _hashlike($obj) ) {
445 191 100       351 if ( exists $obj->{$index} ) {
446 16 100       55 push @match, $want_ref ? \( $obj->{$index} ) : $obj->{$index};
447             }
448 191         238 for my $val ( values %{$obj} ) {
  191         402  
449 591 100       1067 next unless ref $val;
450 152         303 push @match, _match_recursive( $val, $index, $want_ref );
451             }
452             }
453 281         519 return @match;
454             }
455              
456             sub _filter_recursive {
457 40     40   80 my ( $self, $obj, $expression, $want_ref ) = @_;
458              
459 40         50 my @ret;
460              
461             # Evaluate the filter expression for the current object
462 40         82 my @matching_indices = $self->_process_filter( $obj, $expression );
463 40         74 for my $index (@matching_indices) {
464 4         12 my ($got) = _get( $obj, $index );
465 4 100       11 push @ret, $want_ref ? $got : ${$got};
  2         4  
466             }
467              
468             # Evaluate the filter expression for any subordinate objects
469 40         65 for my $index ( _indices($obj) ) {
470 122         224 my ($got) = _get( $obj, $index );
471 122         178 $got = ${$got}; # _get will always return a reference. We want the value, so dereference it
  122         181  
472 122 100       293 next unless ref $got;
473 38         112 push @ret, $self->_filter_recursive( $got, $expression, $want_ref );
474             }
475              
476 40         96 return @ret;
477             }
478              
479             sub _process_pseudo_js {
480 47     47   110 my ( $self, $object, $expression ) = @_;
481              
482 47         100 my ( $lhs, $operator, $rhs ) = _parse_psuedojs_expression($expression);
483              
484 47         134 my (@token_stream) = tokenize($lhs);
485              
486 47         79 my $index;
487              
488             my @lhs;
489 47 100       99 if ( _hashlike($object) ) {
    100          
490 27         44 @lhs = map { $self->_evaluate( $_, [@token_stream] ) } values %{$object};
  88         228  
  27         87  
491             }
492             elsif ( _arraylike($object) ) {
493 18         31 for my $value ( @{$object} ) {
  18         44  
494 57         162 my ($got) = $self->_evaluate( $value, [@token_stream] );
495 57         137 push @lhs, $got;
496             }
497             }
498              
499             # get indexes that pass compare()
500 47         80 my @matching;
501 47         120 for ( 0 .. $#lhs ) {
502 57         94 my $val = $lhs[$_];
503 57 100       108 push @matching, $_ if _compare( $operator, $val, $rhs );
504             }
505              
506 47         126 return @matching;
507             }
508              
509             sub _parse_psuedojs_expression {
510 47     47   71 my $expression = shift;
511 47         63 my @parts;
512              
513 47         80 my ( $lhs, $operator, $rhs );
514              
515             # The operator could be '=', '!=', '==', '===', '<=', or '>='
516 47 100       120 if ( $expression =~ /$EQUAL_SIGN/ ) {
517 2         18 my $position = index( $expression, '=' );
518 2 50       10 if ( substr( $expression, $position + 1, 1 ) eq $EQUAL_SIGN ) { # could be '==' or '==='
519 2 50       18 if ( substr( $expression, $position + 2, 1 ) eq $EQUAL_SIGN ) { # ===
520 0         0 $operator = $TOKEN_TRIPLE_EQUAL;
521             }
522             else {
523 2         18 $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         50 ( $lhs, $rhs ) = split /$operator/, $expression, 2;
542             }
543             else {
544 45         357 for ( grep { $OPERATORS{$_} eq $OPERATOR_TYPE_COMPARISON } keys %OPERATORS ) {
  900         10388  
545 107 100       805 next if /$EQUAL_SIGN/;
546 55 100       562 if ( $expression =~ /$_/ ) {
547 43         159 ( $lhs, $rhs ) = split /$_/, $expression, 2;
548 43         71 $operator = $_;
549 43         75 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       213 if ($operator) {
556 45   50     125 $rhs = _normalize( $rhs || '' );
557 45         76 $lhs = _normalize($lhs);
558             }
559             else {
560 2         8 $operator = $OPERATOR_IS_TRUE;
561 2         11 $lhs = $expression;
562             }
563 47         148 return ( $lhs, $operator, $rhs );
564             }
565              
566             sub _normalize {
567 90     90   144 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         281 $string =~ s/\s+$//; # trim trailing spaces
572 90         219 $string =~ s/^\s+//; # trim leading spaces
573 90         159 $string =~ s/^['"](.+)['"]$/$1/; # Strip quotes from index
574 90         166 return $string;
575             }
576              
577             sub _process_perl {
578 6     6   19 my ( $self, $object, $code ) = @_;
579              
580 6         54 my $cpt = Safe->new;
581 6         6557 $cpt->permit_only( ':base_core', qw/padsv padav padhv padany rv2gv/ );
582 6         684 ${ $cpt->varglob('root') } = dclone( $self->{root} );
  6         41  
583              
584 6         118 my @matching;
585 6 100       20 if ( _hashlike($object) ) {
586 2         6 for my $index ( keys %{$object} ) {
  2         7  
587 10         22 local $_ = $object->{$index};
588 10         25 my $ret = $cpt->reval($code);
589 10 50       4811 croak qq{Error in filter: $@} if $@;
590 10 100       28 push @matching, $index if $ret;
591             }
592             }
593             else {
594 4         9 for my $index ( 0 .. $#{$object} ) {
  4         18  
595 15         35 local $_ = $object->[$index];
596 15         48 my $ret = $cpt->reval($code);
597 15 50       8536 croak qq{Error in filter: $@} if $@;
598 15 100       50 push @matching, $index if $ret;
599             }
600             }
601 6         35 return @matching;
602             }
603              
604 0           sub _compare {
605 57     57   106 my ( $operator, $lhs, $rhs ) = @_;
606              
607 16     16   143 no warnings qw/uninitialized/;
  16         34  
  16         2604  
608 57 100       133 if ( $operator eq $OPERATOR_IS_TRUE ) {
609 7 100       55 return $lhs ? 1 : 0;
610             }
611              
612 50   66     362 my $use_numeric = looks_like_number($lhs) && looks_like_number($rhs);
613              
614 50 100 66     235 if ( $operator eq '=' || $operator eq '==' || $operator eq '===' ) {
      66        
615 7 50       48 return $use_numeric ? ( $lhs == $rhs ) : $lhs eq $rhs;
616             }
617 43 50       84 if ( $operator eq '<' ) {
618 0 0       0 return $use_numeric ? ( $lhs < $rhs ) : $lhs lt $rhs;
619             }
620 43 50       78 if ( $operator eq '>' ) {
621 43 100       173 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   118 use warnings qw/uninitialized/;
  16         47  
  16         1195  
633             }
634              
635             1;
636              
637             __END__