File Coverage

blib/lib/Jmespath/Lexer.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Jmespath::Lexer;
2 3     3   26418 use strict;
  3         4  
  3         77  
3 3     3   13 use warnings;
  3         3  
  3         72  
4 3     3   1029 use Jmespath::LexerException;
  0            
  0            
5             use Jmespath::EmptyExpressionException;
6             use JSON;
7             use String::Util qw(trim);
8             use List::Util qw(any);
9             use Try::Tiny;
10             use utf8;
11             use feature 'unicode_strings';
12              
13             our $START_IDENTIFIER = ['A'..'Z','a'..'z','_'];
14             our $VALID_IDENTIFIER = ['A'..'Z','a'..'z',0..9,'_'];
15             our $VALID_NUMBER = [0..9];
16             our $WHITESPACE = [' ', "\t", "\n", "\r"];
17             our $SIMPLE_TOKENS = { '.' => 'dot',
18             '*' => "star",
19             ']' => 'rbracket',
20             ',' => 'comma',
21             ':' => 'colon',
22             '@' => 'current',
23             '(' => 'lparen',
24             ')' => 'rparen',
25             '{' => 'lbrace',
26             '}' => 'rbrace', };
27             our $BACKSLASH = "\\";
28              
29             sub new {
30             my ( $class ) = @_;
31             my $self = bless {}, $class;
32             $self->{STACK} = [];
33             return $self;
34             }
35              
36             sub stack {
37             my $self = shift;
38             return $self->{STACK};
39             }
40              
41             sub tokenize {
42             my ( $self, $expression ) = @_;
43             Jmespath::EmptyExpressionError->new->throw
44             if not defined $expression;
45             $self->{STACK} = [];
46             $self->{_position} = 0;
47             $self->{_expression} = $expression;
48             @{$self->{_chars}} = split //, $expression;
49             $self->{_current} = @{$self->{_chars}}[$self->{_position}];
50             $self->{_length} = length $expression;
51              
52             while (defined $self->{_current}) {
53             if ( any { $_ eq $self->{_current} } keys %$SIMPLE_TOKENS ) {
54             push @{$self->{STACK}},
55             { type => $SIMPLE_TOKENS->{ $self->{_current} },
56             value => $self->{_current},
57             start => $self->{_position},
58             end => $self->{_position} + 1 };
59             $self->_next;
60             }
61              
62             elsif ( any { $_ eq $self->{_current} } @$START_IDENTIFIER ) {
63             my $start = $self->{_position};
64             my $buff = $self->{_current};
65             my $next;
66             while ( $self->_has_identifier( $self->_next, $VALID_IDENTIFIER )) {
67             $buff .= $self->{_current};
68             }
69             push @{$self->{STACK}},
70             { type => 'unquoted_identifier',
71             value => $buff,
72             start => $start,
73             end => $start + length $buff };
74             }
75              
76             elsif ( any { $_ eq $self->{_current} } @$WHITESPACE ) {
77             $self->_next;
78             }
79              
80             elsif ( $self->{_current} eq q/[/ ) {
81             my $start = $self->{_position};
82             my $next_char = $self->_next;
83             # use Data::Dumper;
84             # print Dumper $next_char;
85             # if not defined $next_char;
86             if (not defined $next_char) {
87             Jmespath::LexerException->new( lexer_position => $start,
88             lexer_value => $self->{_current},
89             message => "Unexpected end of expression" )->throw;
90             }
91             elsif ( $next_char eq q/]/ ) {
92             $self->_next;
93             push @{$self->{STACK}},
94             { type => 'flatten',
95             value => '[]',
96             start => $start,
97             end => $start + 2 };
98             }
99             elsif ( $next_char eq q/?/ ) {
100             $self->_next;
101             push @{$self->{STACK}},
102             { type => 'filter',
103             value => '[?',
104             start => $start,
105             end => $start + 2 };
106             }
107              
108             else {
109             push @{$self->{STACK}},
110             { type => 'lbracket',
111             value => '[',
112             start => $start,
113             end => $start + 1 };
114             }
115             }
116              
117             elsif ( $self->{_current} eq q/'/ ) {
118             push @{$self->{STACK}},
119             $self->_consume_raw_string_literal;
120             }
121              
122             elsif ( $self->{_current} eq q/|/ ) {
123             push @{$self->{STACK}},
124             $self->_match_or_else('|', 'or', 'pipe');
125             }
126              
127             elsif ( $self->{_current} eq q/&/ ) {
128             push @{$self->{STACK}},
129             $self->_match_or_else('&', 'and', 'expref');
130             }
131              
132             elsif ( $self->{_current} eq q/`/ ) {
133             push @{$self->{STACK}},
134             $self->_consume_literal;
135             }
136              
137             elsif ( any {$_ eq $self->{_current} } @$VALID_NUMBER ) {
138             my $start = $self->{_position};
139             my $buff = $self->_consume_number;
140             push @{$self->{STACK}},
141             { type => 'number',
142             value => $buff,
143             start => $start,
144             end => $start + length $buff };
145             }
146              
147             # negative number
148             elsif ( $self->{_current} eq q/-/ ) {
149             my $start = $self->{_position};
150             my $buff = $self->_consume_number;
151             if (length $buff > 1) {
152             push @{$self->{STACK}},
153             { type => 'number',
154             value => $buff,
155             start => $start,
156             end => $start + length $buff };
157             }
158             else {
159             Jmespath::LexerException->new( lexer_position => $start,
160             lexer_value => $buff,
161             message => "Unknown token '$buff'" )->throw;
162             }
163             }
164              
165             elsif ( $self->{_current} eq q/"/ ) {
166             push @{$self->{STACK}}, $self->_consume_quoted_identifier;
167             }
168              
169             elsif ( $self->{_current} eq q/</ ) {
170             push @{$self->{STACK}}, $self->_match_or_else('=', 'lte', 'lt');
171             }
172              
173             elsif ( $self->{_current} eq q/>/ ) {
174             push @{$self->{STACK}}, $self->_match_or_else('=', 'gte', 'gt');
175             }
176              
177             elsif ( $self->{_current} eq q/!/ ) {
178             push @{$self->{STACK}}, $self->_match_or_else('=', 'ne', 'not');
179             }
180              
181             elsif ( $self->{_current} eq q/=/ ) {
182             if ($self->_next eq '=') {
183             push @{$self->{STACK}},
184             { type => 'eq',
185             value => '==',
186             start => $self->{_position} - 1,
187             end => $self->{_position} };
188             $self->_next;
189             }
190             else {
191             Jmespath::LexerException->new( lexer_position => $self->{_position} - 1,
192             lexer_value => '=',
193             message => 'Unknown token =' )->throw;
194             }
195             }
196             else {
197             Jmespath::LexerException->new( lexer_position => $self->{_position},
198             lexer_value => $self->{_current},
199             message => 'Unknown token ' . $self->{_current})->throw;
200             }
201             }
202             push @{$self->{STACK}},
203             { type => 'eof',
204             value => '',
205             start => $self->{_length},
206             end => $self->{_length} };
207             return $self->{STACK};
208             }
209              
210             sub _consume_number {
211             my ($self) = @_;
212             my $start = $self->{_position};
213             my $buff = $self->{_current};
214             while ( $self->_has_identifier( $self->_next, $VALID_NUMBER)) {
215             $buff .= $self->{_current};
216             }
217             return $buff;
218             }
219              
220             sub _has_identifier {
221             my ($self, $value, $identifier) = @_;
222             return 0 if not defined $value;
223             return 1 if any { $_ eq $value } @$identifier;
224             return 0;
225             }
226              
227             sub _next {
228             my ($self) = @_;
229             if ( $self->{_position} == $self->{_length} - 1 ) {
230             $self->{_current} = undef;
231             }
232             else {
233             $self->{_position} += 1;
234             $self->{_current} = @{$self->{_chars}}[$self->{_position}];
235             }
236             return $self->{_current};
237             }
238              
239             sub _consume_until {
240             my ($self, $delimiter) = @_;
241             my $start = $self->{_position};
242             my $buff = '';
243             $self->_next;
244             while ($self->{_current} ne $delimiter ) {
245             if ($self->{_current} eq $BACKSLASH) {
246             $buff .= $BACKSLASH;
247             # Advance to escaped character.
248             $self->_next;
249             }
250             $buff .= $self->{_current};
251             $self->_next;
252             if (not defined $self->{_current}) {
253             Jmespath::LexerException
254             ->new( lexer_position => $start,
255             lexer_value => $self->{_expression},
256             message => "Unclosed delimiter $delimiter" )
257             ->throw;
258             }
259             }
260             $self->_next;
261             return $buff;
262             }
263              
264             sub _consume_literal {
265             my ($self) = @_;
266             my $start = $self->{_position};
267             my $lexeme = $self->_consume_until('`');
268             $lexeme =~ s/\\`/`/;
269             my $parsed_json;
270             try {
271             $parsed_json = JSON->new->allow_nonref->decode($lexeme);
272             } catch {
273             try {
274             $parsed_json = JSON->new->allow_nonref->decode('"' . trim($lexeme) . '"');
275             } catch {
276             Jmespath::LexerException->new( lexer_position => $start,
277             lexer_value => $self->{_expression},
278             message => "Bad token $lexeme" )->throw;
279             };
280             };
281              
282             my $token_len = $self->{_position} - $start;
283             return { type => 'literal',
284             value => $parsed_json,
285             start => $start,
286             end => $token_len, };
287             }
288              
289             sub _consume_quoted_identifier {
290             my ( $self ) = @_;
291             my $start = $self->{_position};
292             my $lexeme = '"' . $self->_consume_until('"') . '"';
293             my $error = "error consuming quoted identifier";
294             my $decoded_lexeme;
295              
296             try {
297             $decoded_lexeme = JSON->new->allow_nonref->decode($lexeme);
298             } catch {
299             Jmespath::LexerException->new( lexer_position => $start,
300             expression => $lexeme,
301             message => $error )->throw;
302             };
303             return { type => 'quoted_identifier',
304             value => $decoded_lexeme,
305             start => $start,
306             end => $self->{ _position } - $start,
307             };
308             }
309              
310             sub _consume_raw_string_literal {
311             my ( $self ) = @_;
312             my $start = $self->{ _position };
313             my $lexeme = $self->_consume_until("'"); $lexeme =~ s/\\\'/\'/g;
314             my $token_len = $self->{_position} - $start;
315             return { 'type' => 'literal',
316             'value' => $lexeme,
317             'start' => $start,
318             'end' => $token_len,
319             };
320             }
321              
322              
323             sub _match_or_else {
324             my ( $self, $expected, $match_type, $else_type) = @_;
325             my $start = $self->{_position};
326             my $current = $self->{_current};
327             my $next_char = $self->_next;
328             if ( not defined $next_char or
329             $next_char ne $expected ) {
330             return { 'type' => $else_type,
331             'value' => $current,
332             'start' => $start,
333             'end' => $start,
334             };
335             }
336              
337             $self->_next();
338             return { 'type' => $match_type,
339             'value' => $current . $next_char,
340             'start' => $start,
341             'end' => $start + 1,
342             };
343             }
344              
345              
346             1;