File Coverage

blib/lib/JSON/Path/Evaluator.pm
Criterion Covered Total %
statement 362 408 88.7
branch 163 214 76.1
condition 27 51 52.9
subroutine 40 42 95.2
pod 3 3 100.0
total 595 718 82.8


line stmt bran cond sub pod time code
1             package JSON::Path::Evaluator;
2             $JSON::Path::Evaluator::VERSION = '0.5';
3 17     17   581127 use strict;
  17         69  
  17         517  
4 17     17   85 use warnings;
  17         33  
  17         469  
5 17     17   398 use 5.008;
  17         61  
6              
7             # ABSTRACT: A module that recursively evaluates JSONPath expressions with native support for Javascript-style filters
8              
9 17     17   95 use Carp;
  17         32  
  17         1077  
10 17     17   8801 use Carp::Assert qw(assert);
  17         21070  
  17         109  
11 17     17   4294 use Exporter::Tiny ();
  17         11378  
  17         342  
12 17     17   1538 use JSON::MaybeXS;
  17         17413  
  17         951  
13 17     17   7398 use JSON::Path::Constants qw(:operators :symbols);
  17         52  
  17         6232  
14 17     17   8582 use JSON::Path::Tokenizer qw(tokenize);
  17         46  
  17         1021  
15 17     17   122 use List::Util qw/pairs uniq/;
  17         35  
  17         983  
16 17     17   125 use Readonly;
  17         33  
  17         534  
17 17     17   9926 use Safe;
  17         524521  
  17         1276  
18 17     17   176 use Scalar::Util qw/looks_like_number blessed refaddr/;
  17         33  
  17         1400  
19 17     17   11584 use Storable qw/dclone/;
  17         48579  
  17         1285  
20 17     17   8520 use Sys::Hostname qw/hostname/;
  17         15632  
  17         1043  
21 17     17   8521 use Try::Tiny;
  17         31204  
  17         1045  
22              
23             # VERSION
24 17     17   133 use base q(Exporter);
  17         38  
  17         34120  
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 114     114 1 226 my $class = shift;
59 114 50       591 my %args = ref $_[0] eq 'HASH' ? %{ $_[0] } : @_;
  0         0  
60 114         262 my $self = {};
61 114         283 for my $key (qw/root expression/) {
62 228 50       614 croak qq{Missing required argument '$key' in constructor} unless $args{$key};
63 228         507 $self->{$key} = $args{$key};
64             }
65 114   50     541 $self->{want_ref} = $args{want_ref} || 0;
66 114   50     410 $self->{_calling_context} = $args{_calling_context} || 0;
67              
68             my $script_engine =
69 114 100       510 $args{script_engine} ? $args{script_engine} : $self->{expression} =~ /\$_/ ? 'perl' : undef;
    100          
70 114   100     430 $self->{script_engine} = $script_engine || 'PseudoJS';
71 114         216 bless $self, $class;
72 114         290 return $self;
73             }
74              
75              
76             sub evaluate_jsonpath {
77 114     114 1 268074 my ( $json_object, $expression, %args ) = @_;
78              
79 114 100       436 if ( !ref $json_object ) {
80             try {
81 57     57   4964 $json_object = decode_json($json_object);
82             }
83             catch {
84 0     0   0 croak qq{Unable to decode $json_object as JSON: $_};
85             }
86 57         493 }
87              
88 114   100     1706 my $want_ref = delete $args{want_ref} || 0;
89 114   100     478 my $want_path = delete $args{want_path} || 0;
90              
91 114 100       693 my $self = __PACKAGE__->new(
92             root => $json_object,
93             expression => $expression,
94             _calling_context => wantarray ? 'ARRAY' : 'SCALAR',
95             %args
96             );
97 114         362 return $self->evaluate( $expression, want_ref => $want_ref, want_path => $want_path );
98             }
99              
100              
101             sub evaluate {
102 114     114 1 360 my ( $self, $expression, %args ) = @_;
103              
104 114         304 my $json_object = $self->{root};
105              
106 114         438 my $token_stream = [ tokenize($expression) ];
107 114 50       366 shift @{$token_stream} if $token_stream->[0] eq $TOKEN_ROOT;
  114         621  
108 114 100       301 shift @{$token_stream} if $token_stream->[0] eq $TOKEN_CHILD;
  95         475  
109              
110 114 100       473 if ( $args{want_path} ) {
111 11         37 my %reftable = $self->_reftable_walker($json_object);
112 11         788 my @refs = $self->_evaluate( $json_object, dclone $token_stream, 1 );
113              
114 11         29 my @paths;
115 11         28 for my $ref (@refs) {
116 17 100       25 my $refaddr = ref ${$ref} ? refaddr ${$ref} : refaddr $ref;
  17         57  
  9         23  
117 17         50 push @paths, $reftable{$refaddr};
118             }
119 11         193 return @paths;
120             }
121              
122 103         364 my @ret = $self->_evaluate( $json_object, $token_stream, $args{want_ref} );
123 103         1281 return @ret;
124             }
125              
126             sub _reftable_walker {
127 77     77   155 my ( $self, $json_object, $base_path ) = @_;
128              
129 77 100       124 $base_path = defined $base_path ? $base_path : '$';
130 77 50       129 $json_object = defined $json_object ? $json_object : $self->root;
131              
132 77         179 my @entries = ( refaddr $json_object => $base_path );
133              
134 77 100       128 if ( _arraylike($json_object) ) {
    100          
135 8         22 for ( 0 .. $#{$json_object} ) {
  8         47  
136 30         76 my $path = sprintf q{%s['%d']}, $base_path, $_;
137 30 50       61 if ( ref $json_object->[$_] ) {
138 30         67 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         83 for my $index ( keys %{$json_object} ) {
  63         166  
147 234         501 my $path = sprintf q{%s['%s']}, $base_path, $index;
148 234 100       382 if ( ref $json_object->{$index} ) {
149 36         103 push @entries, $self->_reftable_walker( $json_object->{$index}, $path );
150             }
151             else {
152 198         403 push @entries, refaddr \( $json_object->{$index} ) => $path;
153             }
154             }
155             }
156 77         652 return @entries;
157             }
158              
159             sub _evaluate { # This assumes that the token stream is syntactically valid
160 576     576   1305 my ( $self, $obj, $token_stream, $want_ref ) = @_;
161              
162 576 100       1189 return unless ref $obj;
163              
164 492 50       890 $token_stream = defined $token_stream ? $token_stream : [];
165              
166 492         662 while ( defined( my $token = shift @{$token_stream} ) ) {
  928         3649  
167 928 100       3003 next if $token eq $TOKEN_CURRENT;
168 781 100       3798 next if $token eq $TOKEN_CHILD;
169              
170 492 100       2166 if ( $token eq $TOKEN_FILTER_OPEN ) {
    100          
171 30         147 my $filter_expression = shift @{$token_stream};
  30         67  
172              
173 30         45 my $closing_token = shift @{$token_stream};
  30         56  
174 30 50       78 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 30         271 my @matching_indices = $self->_process_filter( $obj, $filter_expression );
178              
179 30 100       52 if ( !@{$token_stream} ) {
  30         107  
180 11         28 my @got = map { _get( $obj, $_ ) } @matching_indices;
  16         57  
181 11 100       55 return $want_ref ? @got : map { ${$_} } @got;
  7         9  
  7         33  
182             }
183             else {
184 19         64 return map { $self->_evaluate( _get( $obj, $_ ), dclone($token_stream), $want_ref ) } @matching_indices;
  22         44  
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         137 my $index = shift @{$token_stream};
  16         32  
190 16         25 my @matched;
191 16 100       31 if ( $index eq $TOKEN_FILTER_OPEN ) {
192 2         10 my $filter_expression = shift @{$token_stream};
  2         4  
193              
194 2         4 my $closing_token = shift @{$token_stream};
  2         4  
195 2 50       6 assert( $closing_token eq $TOKEN_FILTER_SCRIPT_CLOSE, q{Closing token seen} ) if $ASSERT_ENABLE;
196              
197 2         18 return $self->_filter_recursive( $obj, $filter_expression, $want_ref );
198             }
199              
200 14         120 @matched = _match_recursive( $obj, $index, $want_ref );
201 14 100       21 if ( !@{$token_stream} ) {
  14         40  
202 6         28 return @matched;
203             }
204 8         20 return map { $self->_evaluate( $_, dclone($token_stream), $want_ref ) } @matched;
  8         411  
205             }
206             else {
207 446         3399 my $index;
208 446 100       771 if ( $token eq $TOKEN_SUBSCRIPT_OPEN ) {
209 63         265 $index = shift @{$token_stream};
  63         116  
210 63         98 my $closing_token = shift @{$token_stream};
  63         127  
211 63 50       137 assert $closing_token eq $TOKEN_SUBSCRIPT_CLOSE if $ASSERT_ENABLE;
212             }
213             else {
214 383         1494 $index = $token;
215             }
216              
217 446 100       1176 assert( !$OPERATORS{$index}, qq{"$index" is not an operator} ) if $index ne $TOKEN_ALL;
218 446 50       6373 assert( !ref $index, q{Index is a scalar} ) if $ASSERT_ENABLE;
219              
220 446         3066 my (@got) = _get( $obj, $index, create_key => $want_ref ); # This always returns a ref
221 446 100       639 if ( !@{$token_stream} ) {
  446         781  
222 280 100       713 return $want_ref ? @got : map { ${$_} } @got;
  216         252  
  216         841  
223             }
224             else {
225 166         286 return map { $self->_evaluate( ${$_}, dclone($token_stream), $want_ref ) } @got;
  201         276  
  201         5434  
226             }
227             }
228             }
229             }
230              
231             sub _process_filter {
232 70     70   130 my ( $self, $obj, $filter_expression ) = @_;
233              
234 70         99 my @matching_indices;
235 70 100       172 if ( $self->{script_engine} eq 'PseudoJS' ) {
    50          
236 64         195 @matching_indices = $self->_process_pseudo_js( $obj, $filter_expression );
237             }
238             elsif ( $self->{script_engine} eq 'perl' ) {
239 6         29 @matching_indices = $self->_process_perl( $obj, $filter_expression );
240             }
241             else {
242 0         0 croak qq{Unsupported script engine "$self->{script_engine}"};
243             }
244 70         789 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 610     610   1351 my ( $object, $index, %args ) = @_;
255              
256 610 50 0     1166 assert( _hashlike($object) || _arraylike($object), 'Object is a hashref or an arrayref' ) if $ASSERT_ENABLE;
257              
258 610         3846 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 610 100       1129 if ( ref $object eq 'REF' ) {
264 23         33 $object = ${$object};
  23         44  
265             }
266              
267 610 100       1018 if ( $index eq $TOKEN_ALL ) {
268 15 100       83 if ( _hashlike($object) ) {
269 1         3 return map { \($_) } values %{$object};
  9         13  
  1         4  
270             }
271             else {
272 14         29 return map { \($_) } @{$object};
  52         111  
  14         30  
273             }
274             }
275             else {
276 595         2373 my @indices;
277 595 100       1150 if ( $index =~ /$TOKEN_ARRAY_SLICE/ ) {
    100          
278 9 50       95 my $length = _hashlike($object) ? scalar values %{$object} : scalar @{$object};
  0         0  
  9         23  
279 9         32 @indices = _slice( $index, $length );
280             }
281             elsif ( $index =~ /$TOKEN_UNION/ ) {
282 5         50 @indices = split /$TOKEN_UNION/, $index;
283             }
284             else {
285 581         5374 @indices = ($index);
286             }
287              
288 595 100       1126 if ( _hashlike($object) ) {
289 463 100       728 if ($create_key) {
290 84         157 return map { \( $object->{$_} ) } @indices;
  85         364  
291             }
292             else {
293 379         439 my @ret;
294 379         586 for my $index (@indices) {
295 380 100       1026 push @ret, \( $object->{$index} ) if exists $object->{$index};
296             }
297 379         989 return @ret;
298             }
299             }
300             else {
301 132         230 my @numeric_indices = grep { looks_like_number($_) } @indices;
  138         512  
302 132 100       244 if ($create_key) {
303 18         38 return map { \( $object->[$_] ) } @numeric_indices;
  20         81  
304             }
305             else {
306 114         134 my @ret;
307 114         176 for my $index (@numeric_indices) {
308 105 50       312 push @ret, \( $object->[$index] ) if exists $object->[$index];
309             }
310 114         694 return @ret;
311             }
312             }
313             }
314             }
315              
316             sub _indices {
317 40     40   54 my $object = shift;
318             return
319 27         72 _hashlike($object) ? keys %{$object}
320 40 100       50 : _arraylike($object) ? ( 0 .. $#{$object} )
  11 100       38  
321             : ();
322             }
323              
324             sub _hashlike {
325 1023     1023   1300 my $object = shift;
326 1023   66     3403 return ( ref $object eq 'HASH' || ( blessed $object && $object->can('typeof') && $object->typeof eq 'HASH' ) );
327             }
328              
329             sub _arraylike {
330 434     434   505 my $object = shift;
331 434   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 17     17   174 no warnings qw/uninitialized/;
  17         36  
  17         1364  
366 0 0       0 if ( $substream[2] eq $TOKEN_ARRAY_SLICE ) {
367 0         0 @substream = ( @substream[ ( 0, 1 ) ], undef, @substream[ ( 2 .. $#substream ) ] );
368             }
369 17     17   107 use warnings qw/uninitialized/;
  17         34  
  17         27512  
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     141 if ( !defined($start) || $start eq '' ) {
397 0         0 $start = 0;
398             }
399 9 100 66     44 if ( !defined($end) || $end eq '' ) {
400 6         15 $end = -1;
401             }
402 9 50 33     37 if ( !defined($step) || $step eq '' ) {
403 9         12 $step = 1;
404             }
405              
406 9 100       37 $start = ( $length - 1 ) if $start == -1;
407 9 100       22 $end = $length if $end == -1;
408              
409 9         15 my @indices;
410 9 50       27 if ( $step < 0 ) {
411 0         0 @indices = grep { %_ % -$step == 0 } reverse( $start .. ( $end - 1 ) );
  0         0  
412             }
413             else {
414 9         29 @indices = grep { $_ % $step == 0 } ( $start .. ( $end - 1 ) );
  12         64  
415             }
416 9         25 return @indices;
417             }
418              
419             sub _match_recursive {
420 281     281   410 my ( $obj, $index, $want_ref ) = @_;
421              
422 281         286 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       422 if ( ref $obj eq 'REF' ) {
432 2         2 $obj = ${$obj};
  2         4  
433             }
434              
435 281 100       352 if ( _arraylike($obj) ) {
    100          
436 82 0 33     177 if ( looks_like_number($index) && exists $obj->[$index] ) {
437 0 0       0 push @match, $want_ref ? \( $obj->[$index] ) : $obj->[$index];
438             }
439 82         149 for ( 0 .. $#{$obj} ) {
  82         152  
440 251 100       450 next unless ref $obj->[$_];
441 115         173 push @match, _match_recursive( $obj->[$_], $index, $want_ref );
442             }
443             }
444             elsif ( _hashlike($obj) ) {
445 191 100       336 if ( exists $obj->{$index} ) {
446 16 100       44 push @match, $want_ref ? \( $obj->{$index} ) : $obj->{$index};
447             }
448 191         203 for my $val ( values %{$obj} ) {
  191         372  
449 591 100       904 next unless ref $val;
450 152         232 push @match, _match_recursive( $val, $index, $want_ref );
451             }
452             }
453 281         444 return @match;
454             }
455              
456             sub _filter_recursive {
457 40     40   61 my ( $self, $obj, $expression, $want_ref ) = @_;
458              
459 40         45 my @ret;
460              
461             # Evaluate the filter expression for the current object
462 40         71 my @matching_indices = $self->_process_filter( $obj, $expression );
463 40         55 for my $index (@matching_indices) {
464 4         8 my ($got) = _get( $obj, $index );
465 4 100       9 push @ret, $want_ref ? $got : ${$got};
  2         5  
466             }
467              
468             # Evaluate the filter expression for any subordinate objects
469 40         69 for my $index ( _indices($obj) ) {
470 122         188 my ($got) = _get( $obj, $index );
471 122         136 $got = ${$got}; # _get will always return a reference. We want the value, so dereference it
  122         155  
472 122 100       248 next unless ref $got;
473 38         84 push @ret, $self->_filter_recursive( $got, $expression, $want_ref );
474             }
475              
476 40         85 return @ret;
477             }
478              
479             sub _process_pseudo_js {
480 64     64   136 my ( $self, $object, $expressions ) = @_;
481              
482 64         182 my @expressions_or = split /\Q||\E/, $expressions;
483 64         83 my @matching_or;
484              
485 64         113 foreach my $expression (@expressions_or) {
486 74         135 my @expressions_and = split /\Q&&\E/, $expression;
487 74         138 my %matching_and;
488              
489 74         104 foreach my $expression (@expressions_and) {
490              
491 90         170 my ( $lhs, $operator, $rhs ) = _parse_psuedojs_expression($expression);
492              
493 90         240 my (@token_stream) = tokenize($lhs);
494              
495 90         126 my @lhs;
496 90 100       172 if ( _hashlike($object) ) {
    100          
497 27         35 @lhs = map { $self->_evaluate( $_, [@token_stream] ) } values %{$object};
  88         201  
  27         84  
498             }
499             elsif ( _arraylike($object) ) {
500 61         84 for my $value ( @{$object} ) {
  61         115  
501 143         377 my ($got) = $self->_evaluate( $value, [@token_stream] );
502 143         298 push @lhs, $got;
503             }
504             }
505              
506             # get indexes that pass compare()
507 90         304 for ( 0 .. $#lhs ) {
508 143         209 my $val = $lhs[$_];
509 143 100       260 $matching_and{$_}++ if _compare( $operator, $val, $rhs );
510             }
511             }
512 74         247 while (my ($idx, $val) = each(%matching_and)) {
513 49 100       220 push @matching_or, $idx if ($val == @expressions_and);
514             }
515             }
516              
517 64         332 return sort(uniq(@matching_or));
518             }
519              
520             sub _parse_psuedojs_expression {
521 90     90   132 my $expression = shift;
522 90         98 my @parts;
523              
524 90         122 my ( $lhs, $operator, $rhs );
525              
526             # The operator could be '=', '!=', '==', '===', '<=', or '>='
527 90 100       231 if ( $expression =~ /$EQUAL_SIGN/ ) {
528 23         169 my $position = index( $expression, '=' );
529 23 100       70 if ( substr( $expression, $position + 1, 1 ) eq $EQUAL_SIGN ) { # could be '==' or '==='
530 12 50       79 if ( substr( $expression, $position + 2, 1 ) eq $EQUAL_SIGN ) { # ===
531 0         0 $operator = $TOKEN_TRIPLE_EQUAL;
532             }
533             else {
534 12         64 $operator = $TOKEN_DOUBLE_EQUAL;
535             }
536             }
537             else {
538 11         67 my $preceding_char = substr( $expression, $position - 1, 1 );
539 11 100       26 if ( $preceding_char eq $GREATER_THAN_SIGN ) {
    50          
    50          
540 9         44 $operator = $TOKEN_GREATER_EQUAL;
541             }
542             elsif ( $preceding_char eq $LESS_THAN_SIGN ) {
543 0         0 $operator = $TOKEN_LESS_EQUAL;
544             }
545             elsif ( $preceding_char eq $EXCLAMATION_MARK ) {
546 0         0 $operator = $TOKEN_NOT_EQUAL;
547             }
548             else {
549 2         33 $operator = $TOKEN_SINGLE_EQUAL;
550             }
551             }
552 23         312 ( $lhs, $rhs ) = split /$operator/, $expression, 2;
553             }
554             else {
555 67         489 for ( grep { $OPERATORS{$_} eq $OPERATOR_TYPE_COMPARISON } keys %OPERATORS ) {
  1340         13063  
556 438 100       2239 next if /$EQUAL_SIGN/;
557 121 100       1267 if ( $expression =~ /$_/ ) {
558 65         210 ( $lhs, $rhs ) = split /$_/, $expression, 2;
559 65         90 $operator = $_;
560 65         103 last;
561             }
562             }
563             }
564              
565             # FIXME: RHS is assumed to be a single value. This isn't necessarily a safe assumption.
566 90 100       301 if ($operator) {
567 88   100     251 $rhs = _normalize( $rhs || '' );
568 88         147 $lhs = _normalize($lhs);
569             }
570             else {
571 2         8 $operator = $OPERATOR_IS_TRUE;
572 2         10 $lhs = $expression;
573             }
574 90         269 return ( $lhs, $operator, $rhs );
575             }
576              
577             sub _normalize {
578 176     176   317 my $string = shift;
579              
580             # NB: Stripping spaces *before* stripping quotes allows the caller to quote spaces in an index.
581             # So an index of 'foo ' will be correctly normalized as 'foo', but '"foo "' will normalize to 'foo '.
582 176         763 $string =~ s/\s+$//; # trim trailing spaces
583 176         358 $string =~ s/^\s+//; # trim leading spaces
584 176         316 $string =~ s/^['"](.+)['"]$/$1/; # Strip quotes from index
585 176         342 return $string;
586             }
587              
588             sub _process_perl {
589 6     6   16 my ( $self, $object, $code ) = @_;
590              
591 6         56 my $cpt = Safe->new;
592 6         5924 $cpt->permit_only( ':base_core', qw/padsv padav padhv padany rv2gv/ );
593 6         611 ${ $cpt->varglob('root') } = dclone( $self->{root} );
  6         41  
594              
595 6         113 my @matching;
596 6 100       18 if ( _hashlike($object) ) {
597 2         4 for my $index ( keys %{$object} ) {
  2         9  
598 10         21 local $_ = $object->{$index};
599 10         26 my $ret = $cpt->reval($code);
600 10 50       4925 croak qq{Error in filter: $@} if $@;
601 10 100       30 push @matching, $index if $ret;
602             }
603             }
604             else {
605 4         11 for my $index ( 0 .. $#{$object} ) {
  4         16  
606 15         28 local $_ = $object->[$index];
607 15         42 my $ret = $cpt->reval($code);
608 15 50       7302 croak qq{Error in filter: $@} if $@;
609 15 100       43 push @matching, $index if $ret;
610             }
611             }
612 6         40 return @matching;
613             }
614              
615 0           sub _compare {
616 143     143   241 my ( $operator, $lhs, $rhs ) = @_;
617              
618 17     17   176 no warnings qw/uninitialized/;
  17         38  
  17         2805  
619 143 100       293 if ( $operator eq $OPERATOR_IS_TRUE ) {
620 7 100       48 return $lhs ? 1 : 0;
621             }
622              
623 136   100     910 my $use_numeric = looks_like_number($lhs) && looks_like_number($rhs);
624              
625 136 100 100     507 if ( $operator eq '=' || $operator eq '==' || $operator eq '===' ) {
      66        
626 31 100       146 return $use_numeric ? ( $lhs == $rhs ) : $lhs eq $rhs;
627             }
628 105 100       174 if ( $operator eq '<' ) {
629 18 50       79 return $use_numeric ? ( $lhs < $rhs ) : $lhs lt $rhs;
630             }
631 87 100       136 if ( $operator eq '>' ) {
632 69 100       258 return $use_numeric ? ( $lhs > $rhs ) : $lhs gt $rhs;
633             }
634 18 50       33 if ( $operator eq '<=' ) {
635 0 0       0 return $use_numeric ? ( $lhs <= $rhs ) : $lhs le $rhs;
636             }
637 18 50       34 if ( $operator eq '>=' ) {
638 18 100       93 return $use_numeric ? ( $lhs >= $rhs ) : $lhs ge $rhs;
639             }
640 0 0 0       if ( $operator eq '!=' || $operator eq '!==' ) {
641 0 0         return $use_numeric ? ( $lhs != $rhs ) : $lhs ne $rhs;
642             }
643 17     17   129 use warnings qw/uninitialized/;
  17         39  
  17         1345  
644             }
645              
646             1;
647              
648             __END__