File Coverage

blib/lib/Elasticsearch/Compat/QueryParser.pm
Criterion Covered Total %
statement 237 247 95.9
branch 153 160 95.6
condition 52 63 82.5
subroutine 28 29 96.5
pod 3 3 100.0
total 473 502 94.2


line stmt bran cond sub pod time code
1             package Elasticsearch::Compat::QueryParser;
2             $Elasticsearch::Compat::QueryParser::VERSION = '0.05';
3 1     1   34584 use strict;
  1         2  
  1         36  
4 1     1   5 use warnings FATAL => 'all';
  1         1  
  1         38  
5 1     1   2798 use Elasticsearch::Util qw(parse_params throw);
  1         93030  
  1         9  
6 1     1   787 use Scalar::Util qw(weaken);
  1         3  
  1         3624  
7             @Elasticsearch::Error::QueryParser::ISA = 'Elasticsearch::Error';
8              
9             # ABSTRACT: Check or filter query strings
10              
11              
12             #===================================
13             sub new {
14             #===================================
15 2     2 1 407 my ( $proto, $params ) = parse_params(@_);
16 2   66     30 my $class = ref $proto || $proto;
17 2         22 $params = {
18             escape_reserved => 0,
19             fields => 0,
20             wildcard_prefix => 1,
21             allow_bool => 1,
22             allow_boost => 1,
23             allow_fuzzy => 1,
24             allow_slop => 1,
25             allow_ranges => 0,
26             %$params,
27             };
28 2         13 return bless {
29             _default_opts => $params,
30             _opts => $params,
31             }, $class;
32             }
33              
34             #===================================
35             sub filter {
36             #===================================
37 97     97 1 64286 my $self = shift;
38 97         141 my $text = shift;
39 97         331 my ( undef, $opts ) = parse_params( $self, @_ );
40 97         852 $opts->{fix} = 1;
41 97         254 return $self->_parse( $text, $opts );
42             }
43              
44             #===================================
45             sub check {
46             #===================================
47 97     97 1 126 my $self = shift;
48 97         109 my $text = shift;
49 97         468 my ( undef, $opts ) = parse_params( $self, @_ );
50 97         833 $opts->{fix} = 0;
51 97         205 return $self->_parse( $text, $opts );
52             }
53              
54             #===================================
55             sub _parse {
56             #===================================
57 194     194   220 my $self = shift;
58 194         285 my $text = shift;
59 194 50       444 $text = '' unless defined $text;
60 194         353 utf8::upgrade($text);
61              
62 194         220 my $opts = shift;
63 194         185 $self->{_opts} = { %{ $self->{_default_opts} }, %$opts };
  194         1535  
64              
65 194         801 $self->{_tokeniser} = $self->_init_tokeniser($text);
66 194         1714 $self->{_tokens} = [];
67 194         427 $self->{_stack} = [ {} ];
68 194         459 $self->{_start_pos} = 0;
69 194         205 $self->{_done} = 0;
70              
71 194         183 my $phrase;
72 194         229 eval {
73 194         465 $phrase = $self->_multi_clauses;
74 144 100 66     886 die "Syntax error\n"
75             unless $self->{_done} || $opts->{fix};
76             };
77 194 100       393 if ($@) {
78 58         303 $@ =~ s/\n$/:\n/;
79 58         380 substr( $text, $self->{_start_pos}, 0, ' ' );
80 58         230 throw( 'QueryParser', "$@$text" );
81             }
82 136         685 return $phrase;
83             }
84              
85             #===================================
86             sub _dump_tokens {
87             #===================================
88 0     0   0 my $self = shift;
89 0         0 my $text = shift;
90 0 0       0 $text = '' unless defined $text;
91 0         0 utf8::upgrade($text);
92              
93 0         0 my $tokeniser = $self->_init_tokeniser($text);
94              
95 0         0 while ( my $next = $tokeniser->() ) {
96 0         0 printf "TOKEN: %-15s VARS: %s\n", shift @$next,
97 0         0 join( ', ', grep { defined $_ } @$next );
98             }
99             }
100              
101             #===================================
102             sub _next_token {
103             #===================================
104 1140     1140   1150 my $self = shift;
105 1140   100     1310 my $next = shift @{ $self->{_tokens} }
106             || $self->{_tokeniser}->();
107 1140 100       3625 return $next if $next;
108 257         328 $self->{_done} = 1;
109 257         967 return;
110             }
111              
112             #===================================
113             sub _return_token {
114             #===================================
115 174     174   191 my $self = shift;
116 174         164 push @{ $self->{_tokens} }, shift;
  174         321  
117 174         275 $self->{_done} = 0;
118             }
119              
120             # 1 = Can follow
121             # 0 = Cannot follow, drop token and try next token
122             # undef = Cannot follow, stop looking
123              
124             my %Clauses = (
125             _LPAREN => 1,
126             _PLUS_MINUS => 1,
127             _EXISTS => 1,
128             _FIELD => 1,
129             _TERM => 1,
130             _PHRASE => 1,
131             _WILDTERM => 1,
132             _RANGE => 1,
133             _NOT => 1,
134             _AND_OR => 1,
135             _SPACE => 1,
136             _RESERVED => 1,
137             _ESCAPE => 1,
138             );
139              
140             my %Boost = ( _BOOST => 1 );
141              
142             my %Allowed = (
143             _CLAUSE => \%Clauses,
144             _LPAREN => { %Clauses, _RPAREN => 1 },
145             _AND_OR => { %Clauses, _AND_OR => 0 },
146             _NOT => { %Clauses, _NOT => 0, _AND_OR => 0 },
147             _PLUS_MINUS => {
148             %Clauses,
149             _NOT => 0,
150             _AND_OR => 0,
151             _PLUS_MINUS => 0,
152             _SPACE => undef,
153             },
154             _FIELD => {
155             _LPAREN => 1,
156             _TERM => 1,
157             _WILDTERM => 1,
158             _PHRASE => 1,
159             _RANGE => 1,
160             },
161             _PHRASE => { _BOOST => 1, _FUZZY => 1 },
162             _TERM => { _BOOST => 1, _FUZZY => 1 },
163             _WILDTERM => \%Boost,
164             _RANGE => \%Boost,
165             _FUZZY => \%Boost,
166             _RPAREN => \%Boost,
167             _EXISTS => \%Boost,
168             _BOOST => {},
169             _SPACE => {},
170             _RESERVED => {},
171             _ESCAPE => {},
172             );
173              
174             #===================================
175             sub _parse_context {
176             #===================================
177 942     942   1085 my $self = shift;
178 942         1286 my $context = shift;
179 942         1230 my $allowed = $Allowed{$context};
180              
181 1111 100       1998 TOKEN: {
182 942         875 my $token = $self->_next_token or return;
183              
184 856         2484 my ( $type, @args ) = @$token;
185 856 100       5952 if ( $allowed->{$type} ) {
    100          
186 666 100       1636 redo TOKEN if $type eq '_SPACE';
187 506         1247 return $self->$type(@args);
188             }
189             elsif ( defined $allowed->{$type} ) {
190 17 100       90 die "Syntax error\n" unless $self->{_opts}{fix};
191 9         21 redo TOKEN;
192             }
193             else {
194 173         348 $self->_return_token($token);
195 173         746 return undef;
196             }
197             }
198             }
199              
200             #===================================
201             sub _multi_clauses {
202             #===================================
203 214     214   252 my $self = shift;
204 214         204 my @clauses;
205 214         199 while (1) {
206 515         2229 my $clause = $self->_parse_context('_CLAUSE');
207 465 100       957 if ( !defined $clause ) {
208             last
209 173 100 100     154 if @{ $self->{_stack} } > 1
  173   100     1105  
210             || !$self->{_opts}{fix}
211             || $self->{_done};
212 9         26 $self->_next_token;
213 9         16 next;
214             }
215 292 100       647 next unless length $clause;
216 264         367 push @clauses, $clause;
217 264         739 $self->{_stack}[-1]{clauses}++;
218             }
219 164         518 return join( ' ', @clauses );
220             }
221              
222             #===================================
223             sub _AND_OR {
224             #===================================
225 45     45   59 my $self = shift;
226 45         57 my $op = shift;
227 45         66 my $opts = $self->{_opts};
228              
229 45 100       276 unless ( $self->{_stack}[-1]{clauses} ) {
230 8 100       33 return '' if $opts->{fix};
231 4         32 die "$op must be preceded by another clause\n";
232             }
233 37 100       72 unless ( $opts->{allow_bool} ) {
234 3 100       26 die qq("$op" not allowed) unless $opts->{fix};
235 2         9 return '';
236             }
237              
238 34         63 my $next = $self->_parse_context('_AND_OR');
239 30 100 66     213 return "$op $next"
240             if defined $next && length $next;
241              
242 4 100       16 return '' if $opts->{fix};
243 2         15 die "$op must be followed by a clause\n";
244             }
245              
246             #===================================
247             sub _NOT {
248             #===================================
249 27     27   35 my $self = shift;
250 27         32 my $op = shift;
251              
252 27         37 my $opts = $self->{_opts};
253 27 100       59 unless ( $opts->{allow_bool} ) {
254 3 100       18 die qq("$op" not allowed) unless $opts->{fix};
255 2         8 return '';
256             }
257              
258 24         116 my $next = $self->_parse_context('_NOT');
259 21 100       48 $next = '' unless defined $next;
260              
261 21 100 100     88 die "$op cannot be followed by + or -"
262             if $next =~ s/^[+-]+// && !$opts->{fix};
263              
264 20 100       106 return "$op $next"
265             if length $next;
266              
267 4 100       25 return '' if $opts->{fix};
268 2         15 die "$op must be followed by a clause\n";
269             }
270              
271             #===================================
272             sub _PLUS_MINUS {
273             #===================================
274 21     21   25 my $self = shift;
275 21         23 my $op = shift;
276 21         48 my $next = $self->_parse_context('_PLUS_MINUS');
277              
278 20 100 66     155 return "$op$next" if defined $next && length $next;
279              
280 3 100       16 return '' if $self->{_opts}{fix};
281 1         14 die "$op must be followed by a clause";
282             }
283              
284             #===================================
285             sub _LPAREN {
286             #===================================
287 20     20   29 my $self = shift;
288 20         24 push @{ $self->{_stack} }, {};
  20         46  
289 20         51 my $clause = $self->_multi_clauses;
290              
291 20         28 my $close = ')';
292 20         41 my $rparen = $self->_next_token;
293 20 100 66     96 if ( $rparen && $rparen->[0] eq '_RPAREN' ) {
    100          
294 18   100     35 my $next = $self->_parse_context('_RPAREN') || '';
295 18 100       43 $close .= $next if $next;
296 18         21 pop @{ $self->{_stack} };
  18         35  
297             }
298             elsif ( $self->{_opts}{fix} ) {
299 1         28 $self->_return_token($rparen);
300             }
301             else {
302 1         7 die "Missing closing parenthesis\n";
303             }
304 19 50       117 return $clause ? "(${clause}${close}" : '';
305             }
306              
307             #===================================
308             sub _BOOST {
309             #===================================
310 46     46   69 my $self = shift;
311 46 100       119 unless ( $self->{_opts}{allow_boost} ) {
312 7 100       45 die "Boost not allowed" unless $self->{_opts}{fix};
313 4         26 return '';
314             }
315 39         48 my $val = shift;
316 39 100 66     173 unless ( defined $val && length $val ) {
317 4 100       24 return '' if $self->{_opts}{fix};
318 2         18 die "Missing boost value\n";
319             }
320 35         191 return "^$val";
321             }
322              
323             #===================================
324             sub _FUZZY {
325             #===================================
326 40     40   54 my $self = shift;
327 40         41 my $fuzzy = shift;
328 40         49 my $opts = $self->{_opts};
329 40         55 my $fix = $opts->{fix};
330              
331 40 100       82 if ( $self->{current} eq '_PHRASE' ) {
332              
333             # phrase slop
334 21 100       44 if ( $opts->{allow_slop} ) {
335 17   100     48 $fuzzy = int( $fuzzy || 0 );
336 17 100       43 $fuzzy = $fuzzy ? "~$fuzzy" : '';
337             }
338             else {
339 4 100       25 die "Phrase slop not allowed\n" unless $fix;
340 2         3 $fuzzy = '';
341             }
342             }
343             else {
344              
345             # fuzzy
346 19 100       32 if ( $opts->{allow_fuzzy} ) {
347 15 100       22 if ( defined $fuzzy ) {
348 11 100       29 if ( $fuzzy <= 1 ) {
349 9         21 $fuzzy = "~$fuzzy";
350             }
351             else {
352 2 100       12 die "Fuzzy value must be between 0.0 and 1.0\n"
353             unless $fix;
354 1         1 $fuzzy = '';
355             }
356             }
357             else {
358 4         8 $fuzzy = '~';
359             }
360             }
361             else {
362 4 100       23 die "Fuzzy not allowed\n"
363             unless $fix;
364 2         4 $fuzzy = '';
365             }
366             }
367              
368 35   100     69 my $next = $self->_parse_context('_FUZZY') || '';
369 33         171 return "$fuzzy$next";
370             }
371              
372             #===================================
373             sub _PHRASE {
374             #===================================
375 28     28   38 my $self = shift;
376 28         34 my $string = shift;
377              
378 28         73 local $self->{current} = '_PHRASE';
379 28   100     52 my $next = $self->_parse_context('_PHRASE') || '';
380              
381 25         135 return qq("$string"$next);
382             }
383              
384             #===================================
385             sub _EXISTS {
386             #===================================
387 14     14   18 my $self = shift;
388 14         15 my $prefix = shift;
389 14         15 my $field = shift;
390              
391 14         28 my $opts = $self->{_opts};
392 14   100     24 my $next = $self->_parse_context('_EXISTS') || '';
393 14 100 100     65 unless ( $opts->{fields}
      66        
394             and ( !ref $opts->{fields} || $opts->{fields}{$field} ) )
395             {
396 5 100       19 return '' if $opts->{fix};
397 2         18 die qq("Field "$field" not allowed);
398             }
399              
400 9 100       43 return "$prefix:$field$next"
401             if $field;
402 3 100       14 return '' if $self->{_opts}{fix};
403 1         6 die "Missing field name for $prefix\n";
404             }
405              
406             #===================================
407             sub _FIELD {
408             #===================================
409 18     18   21 my $self = shift;
410 18         24 my $field = shift;
411              
412 18         28 my $opts = $self->{_opts};
413 18         37 my $next = $self->_parse_context('_FIELD');
414              
415 18 50 33     86 unless ( defined $next && length $next ) {
416 0 0       0 die "Missing clause after field $field\n"
417             unless $opts->{fix};
418 0         0 return '';
419             }
420              
421 18 100 100     139 return "$field:$next"
      66        
422             if $opts->{fields}
423             and !ref $opts->{fields} || $opts->{fields}{$field};
424              
425 10 100       67 die qq("Field "$field" not allowed)
426             unless $opts->{fix};
427              
428 5         20 return $next;
429             }
430              
431             #===================================
432             sub _TERM {
433             #===================================
434 196     196   252 my $self = shift;
435 196         455 local $self->{current} = '_TERM';
436 196   100     363 my $next = $self->_parse_context('_TERM') || '';
437 189         951 return shift(@_) . $next;
438             }
439              
440             #===================================
441             sub _WILDTERM {
442             #===================================
443 13     13   17 my $self = shift;
444 13         17 my $term = shift;
445 13         24 my $min = $self->{_opts}{wildcard_prefix};
446 13   50     28 my $next = $self->_parse_context('_WILDTERM') || '';
447 13 100       167 if ( $term !~ /^[^*?]{$min}/ ) {
448 5 100       36 die "Wildcard cannot have * or ? "
    100          
449             . (
450             $min == 1 ? 'as first character' : "in first $min characters" )
451             unless $self->{_opts}{fix};
452 3         14 $term =~ s/[*?].*//;
453 3 100       14 return '' unless length $term;
454             }
455 10         51 return "$term$next";
456             }
457              
458             #===================================
459             sub _RANGE {
460             #===================================
461 26     26   27 my $self = shift;
462 26         46 my ( $open, $close, $from, $to ) = @_;
463 26         32 my $opts = $self->{_opts};
464 26   100     47 my $next = $self->_parse_context('_RANGE') || '';
465 26 100       53 unless ( $opts->{allow_ranges} ) {
466 2 100       9 die "Ranges not allowed\n"
467             unless $opts->{fix};
468 1         4 return '';
469             }
470 24 100       48 unless ( defined $to ) {
471 4 100       18 die "Malformed range\n" unless $opts->{fix};
472 2         7 return '';
473             }
474 20         109 return "$open$from TO $to$close$next";
475             }
476              
477             #===================================
478             sub _RESERVED {
479             #===================================
480 10     10   13 my $self = shift;
481 10         13 my $char = shift;
482 10 100       57 die "Reserved character $char\n"
483             unless $self->{_opts}{fix};
484 5 100       25 return $self->{_opts}{escape_reserved}
485             ? "\\$char"
486             : '';
487             }
488              
489             #===================================
490             sub _ESCAPE {
491             #===================================
492 2     2   3 my $self = shift;
493 2 100       12 die qq(Cannot end with "\\"\n)
494             unless $self->{_opts}{fix};
495 1         5 return '';
496             }
497              
498             my $DECIMAL = qr/[0-9]+(?:[.][0-9]+)?/;
499             my $NUM_CHAR = qr/[0-9]/;
500             my $ESC_CHAR = qr/\\./;
501             my $WS = qr/[ \t\n\r\x{3000}]/;
502             my $TERM_START_CHAR
503             = qr/[^ \t\n\r\x{3000}+\-!():^[\]"{}~*?\\&|] | $ESC_CHAR/x;
504             my $TERM_CHAR = qr/$TERM_START_CHAR |$ESC_CHAR | [-+]/x;
505             my $QUOTE_RANGE = qr/(?: " (?: \\" | [^"] )* ")/x;
506             my $RANGE_SEP = qr/ \s+ (?: TO \s+)?/x;
507              
508             #===================================
509             sub _init_tokeniser {
510             #===================================
511 207     207   7333 my $self = shift;
512 207         311 my $text = shift;
513              
514 207         232 my $weak_self = $self;
515 207         495 Scalar::Util::weaken($weak_self);
516             return sub {
517 1046   100     4334 TOKEN: {
518 1046     1046   1382 $weak_self->{_start_pos} = pos($text) || 0;
519 1046 100       9420 return ['_SPACE']
520             if $text =~ m/\G$WS/gc;
521 866 100       2399 return [ '_AND_OR', $1 ]
522             if $text =~ m/\G(AND\b | && | OR\b | \|{2})/gcx;
523 805 100       1746 return [ '_NOT', $1 ]
524             if $text =~ m/\G(NOT\b | !)/gcx;
525 774 100       1569 return [ '_PLUS_MINUS', $1 ]
526             if $text =~ m/\G([-+])/gc;
527 748 100       1487 return ['_LPAREN']
528             if $text =~ m/\G[(]/gc;
529 727 100       1627 return ['_RPAREN']
530             if $text =~ m/\G[)]/gc;
531 706 100       2848 return [ '_BOOST', $1 ]
532             if $text =~ m/\G\^($DECIMAL)?/gc;
533 654 100       2990 return [ '_FUZZY', $1 ]
534             if $text =~ m/\G[~]($DECIMAL)?/gc;
535 603 100       2461 return [ '_PHRASE', $1, $2 ]
536             if $text =~ m/\G " ( (?: $ESC_CHAR | [^"\\])*) "/gcx;
537 574 100       2967 return [ '_EXISTS', $1, $2 ]
538             if $text =~ m/\G
539             (_exists_|_missing_):
540             ((?:$TERM_START_CHAR $TERM_CHAR*)?)
541             /gcx;
542 558 100       3008 return [ '_FIELD', $1 ]
543             if $text =~ m/\G ($TERM_START_CHAR $TERM_CHAR*):/gcx;
544 539 100       4542 return [ '_TERM', $1 ]
545             if $text =~ m/\G
546             ( $TERM_START_CHAR $TERM_CHAR*)
547             (?!$TERM_CHAR | [*?])
548             /gcx;
549 338 100       1913 return [ '_WILDTERM', $1 ]
550             if $text =~ m/\G (
551             (?:$TERM_START_CHAR | [*?])
552             (?:$TERM_CHAR | [*?])*
553             )/gcx;
554 321 100       1871 return [ '_RANGE', '[', ']', $1, $2 ]
555             if $text =~ m/\G \[
556             ( $QUOTE_RANGE | [^ \]]+ )
557             (?: $RANGE_SEP
558             ( $QUOTE_RANGE | [^ \]]* )
559             )?
560             \]
561             /gcx;
562 293 100       1382 return [ '_RANGE', '{', '}', $1, $2 ]
563             if $text =~ m/\G \{
564             ( $QUOTE_RANGE | [^ }]+ )
565             (?:
566             $RANGE_SEP
567             ( $QUOTE_RANGE | [^ }]* )
568             )?
569             \}
570             /gcx;
571              
572 291 100       623 return [ '_RESERVED', $1 ]
573             if $text =~ m/\G ( ["&|!(){}[\]~^:+\-] )/gcx;
574              
575 273 100       955 return ['_ESCAPE']
576             if $text =~ m/\G\\$/gc;
577             }
578 270         863 return;
579              
580 207         883 };
581             }
582              
583             1;
584              
585             __END__