File Coverage

blib/lib/Jmespath/Lexer.pm
Criterion Covered Total %
statement 174 177 98.3
branch 52 54 96.3
condition 3 3 100.0
subroutine 29 30 96.6
pod 0 3 0.0
total 258 267 96.6


line stmt bran cond sub pod time code
1             package Jmespath::Lexer;
2 3     3   15688 use strict;
  3         4  
  3         65  
3 3     3   10 use warnings;
  3         11  
  3         55  
4 3     3   869 use Jmespath::LexerException;
  3         9  
  3         126  
5 3     3   1211 use Jmespath::EmptyExpressionException;
  3         6  
  3         80  
6 3     3   1174 use JSON;
  3         16492  
  3         12  
7 3     3   1633 use String::Util qw(trim);
  3         6458  
  3         200  
8 3     3   16 use List::Util qw(any);
  3         3  
  3         145  
9 3     3   12 use Try::Tiny;
  3         3  
  3         117  
10 3     3   1019 use utf8;
  3         19  
  3         14  
11 3     3   72 use feature 'unicode_strings';
  3         21  
  3         5659  
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 862     862 0 715 my ( $class ) = @_;
31 862         961 my $self = bless {}, $class;
32 862         1389 $self->{STACK} = [];
33 862         1531 return $self;
34             }
35              
36             sub stack {
37 0     0 0 0 my $self = shift;
38 0         0 return $self->{STACK};
39             }
40              
41             sub tokenize {
42 880     880 0 23411 my ( $self, $expression ) = @_;
43 880 50       1433 Jmespath::EmptyExpressionError->new->throw
44             if not defined $expression;
45 880         925 $self->{STACK} = [];
46 880         961 $self->{_position} = 0;
47 880         904 $self->{_expression} = $expression;
48 880         905 @{$self->{_chars}} = split //, $expression;
  880         4852  
49 880         864 $self->{_current} = @{$self->{_chars}}[$self->{_position}];
  880         1398  
50 880         902 $self->{_length} = length $expression;
51              
52 880         1457 while (defined $self->{_current}) {
53 5058 100   41618   15580 if ( any { $_ eq $self->{_current} } keys %$SIMPLE_TOKENS ) {
  41618 100       33406  
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
54 1678         4756 push @{$self->{STACK}},
55             { type => $SIMPLE_TOKENS->{ $self->{_current} },
56             value => $self->{_current},
57             start => $self->{_position},
58 1678         1075 end => $self->{_position} + 1 };
59 1678         2139 $self->_next;
60             }
61              
62 149859     149859   90284 elsif ( any { $_ eq $self->{_current} } @$START_IDENTIFIER ) {
63 1465         1302 my $start = $self->{_position};
64 1465         1147 my $buff = $self->{_current};
65 1465         934 my $next;
66 1465         2078 while ( $self->_has_identifier( $self->_next, $VALID_IDENTIFIER )) {
67 4487         9171 $buff .= $self->{_current};
68             }
69 1465         1171 push @{$self->{STACK}},
  1465         7444  
70             { type => 'unquoted_identifier',
71             value => $buff,
72             start => $start,
73             end => $start + length $buff };
74             }
75              
76 5875     5875   7978 elsif ( any { $_ eq $self->{_current} } @$WHITESPACE ) {
77 597         670 $self->_next;
78             }
79              
80             elsif ( $self->{_current} eq q/[/ ) {
81 465         396 my $start = $self->{_position};
82 465         563 my $next_char = $self->_next;
83             # use Data::Dumper;
84             # print Dumper $next_char;
85             # if not defined $next_char;
86 465 100       975 if (not defined $next_char) {
    100          
    100          
87             Jmespath::LexerException->new( lexer_position => $start,
88             lexer_value => $self->{_current},
89 3         15 message => "Unexpected end of expression" )->throw;
90             }
91             elsif ( $next_char eq q/]/ ) {
92 96         124 $self->_next;
93 96         79 push @{$self->{STACK}},
  96         469  
94             { type => 'flatten',
95             value => '[]',
96             start => $start,
97             end => $start + 2 };
98             }
99             elsif ( $next_char eq q/?/ ) {
100 89         103 $self->_next;
101 89         60 push @{$self->{STACK}},
  89         435  
102             { type => 'filter',
103             value => '[?',
104             start => $start,
105             end => $start + 2 };
106             }
107              
108             else {
109 277         205 push @{$self->{STACK}},
  277         1354  
110             { type => 'lbracket',
111             value => '[',
112             start => $start,
113             end => $start + 1 };
114             }
115             }
116              
117             elsif ( $self->{_current} eq q/'/ ) {
118 61         47 push @{$self->{STACK}},
  61         108  
119             $self->_consume_raw_string_literal;
120             }
121              
122             elsif ( $self->{_current} eq q/|/ ) {
123 78         58 push @{$self->{STACK}},
  78         146  
124             $self->_match_or_else('|', 'or', 'pipe');
125             }
126              
127             elsif ( $self->{_current} eq q/&/ ) {
128 60         70 push @{$self->{STACK}},
  60         147  
129             $self->_match_or_else('&', 'and', 'expref');
130             }
131              
132             elsif ( $self->{_current} eq q/`/ ) {
133 186         139 push @{$self->{STACK}},
  186         356  
134             $self->_consume_literal;
135             }
136              
137 3282     3282   2918 elsif ( any {$_ eq $self->{_current} } @$VALID_NUMBER ) {
138 177         189 my $start = $self->{_position};
139 177         232 my $buff = $self->_consume_number;
140 177         147 push @{$self->{STACK}},
  177         906  
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 24         24 my $start = $self->{_position};
150 24         35 my $buff = $self->_consume_number;
151 24 100       37 if (length $buff > 1) {
152 23         20 push @{$self->{STACK}},
  23         125  
153             { type => 'number',
154             value => $buff,
155             start => $start,
156             end => $start + length $buff };
157             }
158             else {
159 1         7 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 116         91 push @{$self->{STACK}}, $self->_consume_quoted_identifier;
  116         227  
167             }
168              
169             elsif ( $self->{_current} eq q/</ ) {
170 13         13 push @{$self->{STACK}}, $self->_match_or_else('=', 'lte', 'lt');
  13         29  
171             }
172              
173             elsif ( $self->{_current} eq q/>/ ) {
174 11         9 push @{$self->{STACK}}, $self->_match_or_else('=', 'gte', 'gt');
  11         23  
175             }
176              
177             elsif ( $self->{_current} eq q/!/ ) {
178 34         45 push @{$self->{STACK}}, $self->_match_or_else('=', 'ne', 'not');
  34         69  
179             }
180              
181             elsif ( $self->{_current} eq q/=/ ) {
182 87 50       109 if ($self->_next eq '=') {
183 87         259 push @{$self->{STACK}},
184             { type => 'eq',
185             value => '==',
186             start => $self->{_position} - 1,
187 87         52 end => $self->{_position} };
188 87         108 $self->_next;
189             }
190             else {
191 0         0 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 6         32 message => 'Unknown token ' . $self->{_current})->throw;
200             }
201             }
202 866         1965 push @{$self->{STACK}},
203             { type => 'eof',
204             value => '',
205             start => $self->{_length},
206 866         616 end => $self->{_length} };
207 866         2534 return $self->{STACK};
208             }
209              
210             sub _consume_number {
211 201     201   175 my ($self) = @_;
212 201         174 my $start = $self->{_position};
213 201         156 my $buff = $self->{_current};
214 201         245 while ( $self->_has_identifier( $self->_next, $VALID_NUMBER)) {
215 42         96 $buff .= $self->{_current};
216             }
217 201         274 return $buff;
218             }
219              
220             sub _has_identifier {
221 6195     6195   5130 my ($self, $value, $identifier) = @_;
222 6195 100       7894 return 0 if not defined $value;
223 5982 100   258638   12861 return 1 if any { $_ eq $value } @$identifier;
  258638         147693  
224 1453         3135 return 0;
225             }
226              
227             sub _next {
228 11770     11770   8697 my ($self) = @_;
229 11770 100       13768 if ( $self->{_position} == $self->{_length} - 1 ) {
230 872         905 $self->{_current} = undef;
231             }
232             else {
233 10898         7264 $self->{_position} += 1;
234 10898         6968 $self->{_current} = @{$self->{_chars}}[$self->{_position}];
  10898         11625  
235             }
236 11770         18231 return $self->{_current};
237             }
238              
239             sub _consume_until {
240 363     363   323 my ($self, $delimiter) = @_;
241 363         275 my $start = $self->{_position};
242 363         310 my $buff = '';
243 363         429 $self->_next;
244 363         628 while ($self->{_current} ne $delimiter ) {
245 1340 100       1653 if ($self->{_current} eq $BACKSLASH) {
246 110         105 $buff .= $BACKSLASH;
247             # Advance to escaped character.
248 110         117 $self->_next;
249             }
250 1340         1004 $buff .= $self->{_current};
251 1340         1304 $self->_next;
252 1340 100       2629 if (not defined $self->{_current}) {
253             Jmespath::LexerException
254             ->new( lexer_position => $start,
255             lexer_value => $self->{_expression},
256 1         6 message => "Unclosed delimiter $delimiter" )
257             ->throw;
258             }
259             }
260 362         385 $self->_next;
261 362         529 return $buff;
262             }
263              
264             sub _consume_literal {
265 186     186   157 my ($self) = @_;
266 186         157 my $start = $self->{_position};
267 186         220 my $lexeme = $self->_consume_until('`');
268 186         264 $lexeme =~ s/\\`/`/;
269 186         114 my $parsed_json;
270             try {
271 186     186   4676 $parsed_json = JSON->new->allow_nonref->decode($lexeme);
272             } catch {
273             try {
274 5         115 $parsed_json = JSON->new->allow_nonref->decode('"' . trim($lexeme) . '"');
275             } catch {
276             Jmespath::LexerException->new( lexer_position => $start,
277             lexer_value => $self->{_expression},
278 2         80 message => "Bad token $lexeme" )->throw;
279 5     5   62 };
280 186         817 };
281              
282 184         1948 my $token_len = $self->{_position} - $start;
283 184         1098 return { type => 'literal',
284             value => $parsed_json,
285             start => $start,
286             end => $token_len, };
287             }
288              
289             sub _consume_quoted_identifier {
290 116     116   119 my ( $self ) = @_;
291 116         124 my $start = $self->{_position};
292 116         209 my $lexeme = '"' . $self->_consume_until('"') . '"';
293 115         114 my $error = "error consuming quoted identifier";
294 115         102 my $decoded_lexeme;
295              
296             try {
297 115     115   2788 $decoded_lexeme = JSON->new->allow_nonref->decode($lexeme);
298             } catch {
299 1     1   10 Jmespath::LexerException->new( lexer_position => $start,
300             expression => $lexeme,
301             message => $error )->throw;
302 115         513 };
303             return { type => 'quoted_identifier',
304             value => $decoded_lexeme,
305             start => $start,
306 114         1871 end => $self->{ _position } - $start,
307             };
308             }
309              
310             sub _consume_raw_string_literal {
311 61     61   56 my ( $self ) = @_;
312 61         56 my $start = $self->{ _position };
313 61         75 my $lexeme = $self->_consume_until("'"); $lexeme =~ s/\\\'/\'/g;
  61         86  
314 61         60 my $token_len = $self->{_position} - $start;
315 61         325 return { 'type' => 'literal',
316             'value' => $lexeme,
317             'start' => $start,
318             'end' => $token_len,
319             };
320             }
321              
322              
323             sub _match_or_else {
324 196     196   318 my ( $self, $expected, $match_type, $else_type) = @_;
325 196         183 my $start = $self->{_position};
326 196         170 my $current = $self->{_current};
327 196         217 my $next_char = $self->_next;
328 196 100 100     726 if ( not defined $next_char or
329             $next_char ne $expected ) {
330 91         509 return { 'type' => $else_type,
331             'value' => $current,
332             'start' => $start,
333             'end' => $start,
334             };
335             }
336              
337 105         123 $self->_next();
338 105         544 return { 'type' => $match_type,
339             'value' => $current . $next_char,
340             'start' => $start,
341             'end' => $start + 1,
342             };
343             }
344              
345              
346             1;