File Coverage

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