File Coverage

blib/lib/Jmespath/Parser.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Jmespath::Parser;
2 2     2   14815 use strict;
  2         3  
  2         42  
3 2     2   6 use warnings;
  2         2  
  2         35  
4 2     2   282 use Jmespath;
  2         2  
  2         47  
5 2     2   696 use Jmespath::Lexer;
  0            
  0            
6             use Jmespath::Ast;
7             use Jmespath::Visitor;
8             use Jmespath::ParsedResult;
9             use Jmespath::IncompleteExpressionException;
10             use List::Util qw(any);
11             use Try::Tiny;
12              
13             my $BINDING_POWER = { 'eof' => 0,
14             'unquoted_identifier' => 0,
15             'quoted_identifier' => 0,
16             'literal' => 0,
17             'rbracket' => 0,
18             'rparen' => 0,
19             'comma' => 0,
20             'rbrace' => 0,
21             'number' => 0,
22             'current' => 0,
23             'expref' => 0,
24             'colon' => 0,
25             'pipe' => 1,
26             'or' => 2,
27             'and' => 3,
28             'eq' => 5,
29             'gt' => 5,
30             'lt' => 5,
31             'gte' => 5,
32             'lte' => 5,
33             'ne' => 5,
34             'flatten' => 9,
35             # Everything above stops a projection.
36             'star' => 20,
37             'filter' => 21,
38             'dot' => 40,
39             'not' => 45,
40             'lbrace' => 50,
41             'lbracket' => 55,
42             'lparen' => 60 };
43              
44             # The maximum binding power for a token that can stop
45             # a projection.
46             my $PROJECTION_STOP = 10;
47             # The _MAX_SIZE most recent expressions are cached in
48             # _CACHE hash.
49             my $CACHE = {};
50             my $MAX_SIZE = 128;
51              
52             sub new {
53             my ( $class, $lookahead ) = @_;
54             my $self = bless {}, $class;
55             $lookahead = 2 if not defined $lookahead;
56             $self->{ tokenizer } = undef;
57             $self->{ tokens } = [ undef ] * $lookahead;
58             $self->{ buffer_size } = $lookahead;
59             $self->{ index } = 0;
60             return $self;
61             }
62              
63             sub parse {
64             my ($self, $expression) = @_;
65              
66             my $cached = $self->{_CACHE}->{$expression};
67             return $cached if defined $cached;
68              
69             my $parsed_result = $self->_do_parse($expression);
70              
71             $self->{_CACHE}->{expression} = $parsed_result;
72             if (scalar keys %{$self->{_CACHE}} > $MAX_SIZE) {
73             $self->_free_cache_entries;
74             }
75             return $parsed_result;
76             }
77              
78             sub _do_parse {
79             my ( $self, $expression ) = @_;
80             my $parsed;
81             try {
82             $parsed = $self->_parse($expression);
83             } catch {
84             if ($_->isa('Jmespath::LexerException')) {
85             $_->expression($self->{_expression});
86             $_->throw;
87             }
88             elsif ($_->isa('Jmespath::IncompleteExpressionException')) {
89             $_->expression($self->{_expression});
90             $_->throw;
91             }
92             elsif ($_->isa('Jmespath::ParseException')) {
93             $_->expression($self->{_expression});
94             $_->throw;
95             }
96             else {
97             $_->throw;
98             }
99             };
100             return $parsed;
101             }
102              
103             sub _parse {
104             my ( $self, $expression ) = @_;
105             $self->{_expression} = $expression;
106             $self->{_index} = 0;
107             $self->{_tokens} = Jmespath::Lexer->new->tokenize($expression);
108              
109             my $parsed = $self->_expression(0); #binding_power = 0
110             if ($self->_current_token_type ne 'eof') {
111             my $t = $self->_lookahead_token(0);
112             return Jmespath::ParseException->new(lex_position => $t->{start},
113             token_value => $t->{value},
114             token_type => $t->{type},
115             message => "Unexpected token: " . $t->{value})->throw;
116             }
117              
118             return Jmespath::ParsedResult->new($expression, $parsed);
119             }
120              
121              
122             sub _expression {
123             my ( $self, $binding_power ) = @_;
124             $binding_power = defined $binding_power ? $binding_power : 0;
125              
126             # Get the current token under evaluation
127             my $left_token = $self->_lookahead_token(0);
128              
129             # Advance the token index
130             $self->_advance;
131              
132             my $nud_function = \&{'_token_nud_' . $left_token->{type}};
133             if ( not defined &$nud_function ) { $self->_error_nud_token($left_token); }
134              
135             my $left_ast = &$nud_function($self, $left_token);
136             my $current_token = $self->_current_token_type;
137              
138             while ( $binding_power < $BINDING_POWER->{$current_token} ) {
139             my $led = \&{'_token_led_' . $current_token};
140              
141             if (not defined &$led) {
142             $self->_error_led_token($self->_lookahead_token(0));
143             }
144             else {
145             $self->_advance;
146             $left_ast = &$led($self, $left_ast);
147             $current_token = $self->_current_token_type;
148             }
149             }
150             return $left_ast;
151             }
152              
153             sub _token_nud_literal {
154             my ($self, $token) = @_;
155             return Jmespath::Ast->literal($token->{value});
156             }
157              
158             sub _token_nud_unquoted_identifier {
159             my ($self, $token) = @_;
160             return Jmespath::Ast->field($token->{value});
161             }
162              
163             sub _token_nud_quoted_identifier {
164             my ($self, $token) = @_;
165              
166             my $field = Jmespath::Ast->field($token->{value});
167              
168             # You can't have a quoted identifier as a function name.
169             if ( $self->_current_token_type eq 'lparen' ) {
170             my $t = $self->_lookahead_token(0);
171             Jmespath::ParseException
172             ->new( lex_position => 0,
173             token_value => $t->{value},
174             token_type => $t->{type},
175             message => 'Quoted identifier not allowed for function names.')
176             ->throw;
177             }
178             return $field;
179             }
180              
181             sub _token_nud_star {
182             my ($self, $token) = @_;
183             my $left = Jmespath::Ast->identity;
184             my $right;
185             if ( $self->_current_token_type eq 'rbracket' ) {
186             $right = Jmespath::Ast->identity;
187             }
188             else {
189             $right = $self->_parse_projection_rhs( $BINDING_POWER->{ star } );
190             }
191             return Jmespath::Ast->value_projection($left, $right);
192             }
193              
194             sub _token_nud_filter {
195             my ($self, $token) = @_;
196             return $self->_token_led_filter(Jmespath::Ast->identity);
197             }
198              
199             sub _token_nud_lbrace {
200             my ($self, $token) = @_;
201             return $self->_parse_multi_select_hash;
202             }
203              
204             sub _token_nud_lparen {
205             my ($self, $token) = @_;
206             my $expression = $self->_expression;
207             $self->_match('rparen');
208             return $expression;
209             }
210              
211             sub _token_nud_flatten {
212             my ($self, $token) = @_;
213             my $left = Jmespath::Ast->flatten(Jmespath::Ast->identity);
214             my $right = $self->_parse_projection_rhs( $BINDING_POWER->{ flatten } );
215             return Jmespath::Ast->projection($left, $right);
216             }
217              
218             sub _token_nud_not {
219             my ($self, $token) = @_;
220             my $expr = $self->_expression( $BINDING_POWER->{ not } );
221             return Jmespath::Ast->not_expression($expr);
222             }
223              
224             sub _token_nud_lbracket {
225             my ($self, $token) = @_;
226             if (any { $_ eq $self->_current_token_type } qw(number colon)) {
227             my $right = $self->_parse_index_expression;
228             return $self->_project_if_slice(Jmespath::Ast->identity, $right);
229             }
230             elsif ($self->_current_token_type eq 'star' and
231             $self->_lookahead(1) eq 'rbracket') {
232             $self->_advance;
233             $self->_advance;
234             my $right = $self->_parse_projection_rhs( $BINDING_POWER->{ star } );
235             return Jmespath::Ast->projection(Jmespath::Ast->identity, $right);
236             }
237             else {
238             return $self->_parse_multi_select_list;
239             }
240             }
241              
242             sub _parse_index_expression {
243             my ($self) = @_;
244             # We're here:
245             # [<current>
246             # ^
247             # | current token
248             if ($self->_lookahead(0) eq 'colon' or
249             $self->_lookahead(1) eq 'colon') {
250             return $self->_parse_slice_expression;
251             }
252             else {
253             #parse the syntax [number]
254             my $node = Jmespath::Ast->index_of($self->_lookahead_token(0)->{value});
255             $self->_advance;
256             $self->_match('rbracket');
257             return $node;
258             }
259             }
260              
261             sub _parse_slice_expression {
262             my ($self) = @_;
263             # [start:end:step]
264             # Where start, end, and step are optional.
265             # The last colon is optional as well.
266             my @parts = (undef, undef, undef);
267             my $index = 0;
268             my $current_token = $self->_current_token_type;
269             while ($current_token ne 'rbracket' and $index < 3) {
270             if ($current_token eq 'colon') {
271             $index += 1;
272             if ( $index == 3 ) {
273             $self->_raise_parse_error_for_token($self->_lookahead_token(0),
274             'syntax error');
275             }
276             $self->_advance;
277             }
278             elsif ($current_token eq 'number') {
279             $parts[$index] = $self->_lookahead_token(0)->{value};
280             $self->_advance;
281             }
282             else {
283             $self->_raise_parse_error_for_token( $self->_lookahead_token(0),
284             'syntax error');
285             $current_token = $self->_current_token_type;
286             }
287             $current_token = $self->_current_token_type;
288             }
289             $self->_match('rbracket');
290             return Jmespath::Ast->slice(@parts);
291             }
292              
293             sub _token_nud_current {
294             my ($self, $token) = @_;
295             return Jmespath::Ast->current_node;
296             }
297              
298             sub _token_nud_expref {
299             my ($self, $token) = @_;
300             my $expression = $self->_expression( $BINDING_POWER->{ expref } );
301             return Jmespath::Ast->expref($expression);
302             }
303              
304             sub _token_led_dot {
305             my ($self, $left) = @_;
306              
307             if ($self->_current_token_type ne 'star') {
308              
309             # Begin the evaluation of the subexpression.
310             my $right = $self->_parse_dot_rhs( $BINDING_POWER->{ dot } );
311              
312             if ($left->{type} eq 'subexpression') {
313             push @{$left->{children}}, $right;
314             return $left;
315             }
316              
317             # We have identified a subexpression, but the current AST is not a
318             # subexpression. Convert to a subexpression here.
319             return Jmespath::Ast->subexpression([$left, $right]);
320             }
321             $self->_advance;
322             my $right = $self->_parse_projection_rhs( $BINDING_POWER->{ dot } );
323             return Jmespath::Ast->value_projection($left, $right);
324             }
325              
326             sub _token_led_pipe {
327             my ($self, $left) = @_;
328             my $right = $self->_expression( $BINDING_POWER->{ pipe } );
329             return Jmespath::Ast->pipe_oper($left, $right);
330             }
331              
332             sub _token_led_or {
333             my ($self, $left) = @_;
334             my $right = $self->_expression( $BINDING_POWER->{ or } );
335             return Jmespath::Ast->or_expression($left, $right);
336             }
337              
338             sub _token_led_and {
339             my ($self, $left) = @_;
340             my $right = $self->_expression( $BINDING_POWER->{ and } );
341             return Jmespath::Ast->and_expression($left, $right);
342             }
343              
344             sub _token_led_lparen {
345             my ($self, $left) = @_;
346             if ( $left->{type} ne 'field' ) {
347             # 0 - first func arg or closing paren.
348             # -1 - '(' token
349             # -2 - invalid function "name".
350             my $prev_t = $self->_lookahead_token(-2);
351             my $message = "Invalid function name '" . $prev_t->{value} ."'";
352             Jmespath::ParseException
353             ->new( lex_position => $prev_t->{start},
354             token_value => $prev_t->{value},
355             token_type => $prev_t->{type},
356             message => $message)
357             ->throw;
358             }
359             my $name = $left->{value};
360             my $args = [];
361             while (not $self->_current_token_type eq 'rparen') {
362             my $expression = $self->_expression;
363             if ( $self->_current_token_type eq 'comma') {
364             $self->_match('comma');
365             }
366             push @$args, $expression;
367             }
368             $self->_match('rparen');
369             return Jmespath::Ast->function_expression($name, $args);
370             }
371              
372             sub _token_led_filter {
373             my ($self, $left) = @_;
374             my $right;
375             my $condition = $self->_expression(0);
376             $self->_match('rbracket');
377             if ( $self->_current_token_type eq 'flatten' ) {
378             $right = Jmespath::Ast->identity;
379             }
380             else {
381             $right = $self->_parse_projection_rhs( $BINDING_POWER->{ filter } );
382             }
383              
384             return Jmespath::Ast->filter_projection($left, $right, $condition);
385             }
386              
387             sub _token_led_eq {
388             my ($self, $left) = @_;
389             return $self->_parse_comparator($left, 'eq');
390             }
391              
392             sub _token_led_ne {
393             my ($self, $left) = @_;
394             return $self->_parse_comparator($left, 'ne');
395             }
396              
397             sub _token_led_gt {
398             my ($self, $left) = @_;
399             return $self->_parse_comparator($left, 'gt');
400             }
401              
402             sub _token_led_gte {
403             my ($self, $left) = @_;
404             return $self->_parse_comparator($left, 'gte');
405             }
406              
407             sub _token_led_lt {
408             my ($self, $left) = @_;
409             return $self->_parse_comparator($left, 'lt');
410             }
411              
412             sub _token_led_lte {
413             my ($self, $left) = @_;
414             return $self->_parse_comparator($left, 'lte');
415             }
416              
417             sub _token_led_flatten {
418             my ($self, $left) = @_;
419             $left = Jmespath::Ast->flatten($left);
420             my $right = $self->_parse_projection_rhs( $BINDING_POWER->{ flatten } );
421             return Jmespath::Ast->projection($left, $right);
422             }
423              
424             sub _token_led_lbracket {
425             my ($self, $left) = @_;
426             my $token = $self->_lookahead_token(0);
427             if ( any { $_ eq $token->{type}} qw(number colon) ) {
428             my $right = $self->_parse_index_expression();
429             if ($left->{type} eq 'index_expression') {
430             push @{$left->{children}}, $right;
431             return $left;
432             }
433             return $self->_project_if_slice($left, $right);
434             }
435             else {
436             $self->_match('star');
437             $self->_match('rbracket');
438             my $right = $self->_parse_projection_rhs( $BINDING_POWER->{ star } );
439             return Jmespath::Ast->projection($left, $right);
440             }
441             }
442              
443             sub _project_if_slice {
444             my ($self, $left, $right) = @_;
445             my $index_expr = Jmespath::Ast->index_expression([$left, $right]);
446             if ( $right->{type} eq 'slice' ) {
447             return Jmespath::Ast->projection( $index_expr,
448             $self->_parse_projection_rhs($BINDING_POWER->{star}));
449             }
450              
451             return $index_expr;
452             }
453              
454             sub _parse_comparator {
455             my ($self, $left, $comparator) = @_;
456             my $right = $self->_expression( $BINDING_POWER->{ $comparator } );
457             return Jmespath::Ast->comparator($comparator, $left, $right);
458             }
459              
460             sub _parse_multi_select_list {
461             my ($self) = @_;
462             my $expressions = [];
463             try {
464             while (1) {
465             my $expression = $self->_expression;
466             push @$expressions, $expression;
467             last if ($self->_current_token_type eq 'rbracket');
468             $self->_match('comma');
469             }
470             $self->_match('rbracket');
471             return Jmespath::Ast->multi_select_list($expressions);
472             } catch {
473             $_->throw;
474             }
475             }
476              
477              
478             sub _parse_multi_select_hash {
479             my ($self) = @_;
480             my @pairs;
481             while (1) {
482             my $key_token = $self->_lookahead_token(0);
483             # Before getting the token value, verify it's
484             # an identifier.
485             $self->_match_multiple_tokens( [ 'quoted_identifier', 'unquoted_identifier' ]);
486             my $key_name = $key_token->{ value };
487             $self->_match('colon');
488             my $value = $self->_expression(0);
489             my $node = Jmespath::Ast->key_val_pair( $key_name,
490             $value );
491             push @pairs, $node;
492             if ( $self->_current_token_type eq 'comma' ) {
493             $self->_match('comma');
494             }
495             elsif ( $self->_current_token_type eq 'rbrace' ) {
496             $self->_match('rbrace');
497             last;
498             }
499             }
500             return Jmespath::Ast->multi_select_hash(\@pairs);
501             }
502              
503             sub _parse_projection_rhs {
504             my ($self, $binding_power) = @_;
505             if ( $BINDING_POWER->{ $self->_current_token_type } < $PROJECTION_STOP) {
506             return Jmespath::Ast->identity();
507             }
508             elsif ($self->_current_token_type eq 'lbracket') {
509             return $self->_expression( $binding_power );
510             }
511             elsif ($self->_current_token_type eq 'filter') {
512             return $self->_expression( $binding_power );
513             }
514             elsif ($self->_current_token_type eq 'dot') {
515             $self->_match('dot');
516             return $self->_parse_dot_rhs($binding_power);
517             }
518              
519             $self->_raise_parse_error_for_token($self->_lookahead_token(0),
520             'syntax error');
521             return undef;
522             }
523              
524             sub _parse_dot_rhs {
525             my ($self, $binding_power) = @_;
526             # From the grammar:
527             # expression '.' ( identifier /
528             # multi-select-list /
529             # multi-select-hash /
530             # function-expression /
531             # *
532             # In terms of tokens that means that after a '.',
533             # you can have:
534             # my $lookahead = $self->_current_token_type;
535              
536             # What token do we have next in the index
537             my $lookahead = $self->_current_token_type;
538              
539             # Common case "foo.bar", so first check for an identifier.
540             if ( any { $_ eq $lookahead } qw(quoted_identifier unquoted_identifier star) ) {
541             return $self->_expression( $binding_power );
542             }
543             elsif ( $lookahead eq 'lbracket' ) {
544             $self->_match('lbracket');
545             return $self->_parse_multi_select_list;
546             }
547             elsif ( $lookahead eq 'lbrace' ) {
548             $self->_match('lbrace');
549             return $self->_parse_multi_select_hash;
550             }
551             else {
552             my $t = $self->_lookahead_token(0);
553             my @allowed = qw(quoted_identifier unquoted_identified lbracket lbrace);
554             my $msg = 'Expecting: ' . join(' ', @allowed) . ', got: ' . $t->{ type };
555             $self->_raise_parse_error_for_token( $t, $msg )->throw;
556             }
557             }
558             sub _error_nud_token {
559             my ($self, $token) = @_;
560             if ( $token->{type} eq 'eof' ) {
561             Jmespath::IncompleteExpressionException->new( lex_expression => $token->{ start },
562             token_value => $token->{ value },
563             token_type => $token->{ type } )->throw;
564             }
565             $self->_raise_parse_error_for_token($token, 'invalid token');
566             }
567              
568             sub _error_led_token {
569             my ($self, $token) = @_;
570             $self->_raise_parse_error_for_token($token, 'invalid token');
571             }
572              
573             sub _match {
574             my ($self, $token_type) = @_;
575             if ($self->_current_token_type eq $token_type ) {
576             $self->_advance();
577             }
578             else {
579             $self->_raise_parse_error_maybe_eof( $token_type,
580             $self->_lookahead_token(0) )->throw;
581             }
582             }
583              
584             sub _match_multiple_tokens {
585             my ( $self, $token_types ) = @_;
586             if ( not any { $_ eq $self->_current_token_type } @$token_types ) {
587             $self->_raise_parse_error_maybe_eof( $token_types,
588             $self->_lookahead_token(0) );
589             }
590              
591             $self->_advance();
592             }
593              
594             sub _advance {
595             my ($self) = @_;
596             $self->{ _index } += 1;
597             }
598              
599             sub _current_token_type {
600             my ($self) = @_;
601             return @{ $self->{ _tokens } }[ $self->{_index} ]->{type};
602             }
603              
604             # _lookahead
605             #
606             # retrieve the type of the token at position current + n.
607             sub _lookahead {
608             my ($self, $number) = @_;
609             $number = defined $number ? $number : 1;
610             return @{ $self->{ _tokens } }[ $self->{_index} + $number ]->{type};
611             }
612              
613             sub _lookahead_token {
614             my ($self, $number) = @_;
615              
616             my $lookahead = @{$self->{ _tokens }}[ $self->{_index} + $number ];
617              
618             return $lookahead;
619             }
620              
621             sub _raise_parse_error_for_token {
622             my ($self, $token, $reason) = @_;
623             my $lex_position = $token->{ start };
624             my $actual_value = $token->{ value };
625             my $actual_type = $token->{ type };
626              
627             Jmespath::ParseException->new( lex_position => $lex_position,
628             token_value => $actual_value,
629             token_type => $actual_type,
630             message => $reason )->throw;
631             }
632              
633             sub _raise_parse_error_maybe_eof {
634             my ($self, $expected_type, $token) = @_;
635             my $lex_position = $token->{ start };
636             my $actual_value = $token->{ value };
637             my $actual_type = $token->{ type };
638              
639             if ( $actual_type eq 'eof' ) {
640             Jmespath::IncompleteExpressionException
641             ->new( lex_position => $lex_position,
642             token_value => $actual_value,
643             token_type => $actual_type )
644             ->throw;
645             }
646              
647             my $message = "Expecting: $expected_type, got: $actual_type";
648              
649             Jmespath::ParseException
650             ->new( lex_position => $lex_position,
651             token_value => $actual_value,
652             token_type => $actual_type,
653             message => $message )
654             ->throw;
655             }
656              
657             sub _free_cache_entries {
658             my ($self) = @_;
659             my $key = $self->{_CACHE}{(keys %{$self->{_CACHE}})[rand keys %{$self->{_CACHE}}]};
660             delete $self->{ _CACHE }->{ $key };
661             }
662              
663             sub purge {
664             my ($self, $cls) = @_;
665             $cls->_CACHE->clear();
666             }
667              
668             1;