File Coverage

blib/lib/LINQ/FieldSet/Assertion.pm
Criterion Covered Total %
statement 184 184 100.0
branch 50 58 86.2
condition 10 10 100.0
subroutine 51 51 100.0
pod 5 5 100.0
total 300 308 97.4


line stmt bran cond sub pod time code
1 1     1   18 use 5.006;
  1         3  
2 1     1   5 use strict;
  1         2  
  1         32  
3 1     1   7 use warnings;
  1         1  
  1         195  
4              
5             package LINQ::FieldSet::Assertion;
6              
7             my $_process_args = sub {
8             require Scalar::Util;
9             if ( Scalar::Util::blessed( $_[0] )
10             and Scalar::Util::blessed( $_[1] )
11             and @_ < 4 )
12             {
13             return $_[2] ? ( $_[1], $_[0] ) : ( $_[0], $_[1] );
14             }
15            
16             my ( $self, @other ) = @_;
17             my $other = __PACKAGE__->new( @other );
18             return ( $self, $other );
19             };
20              
21             our $AUTHORITY = 'cpan:TOBYINK';
22             our $VERSION = '0.001';
23              
24 1     1   24 use Class::Tiny;
  1         3  
  1         7  
25 1     1   77 use parent qw( LINQ::FieldSet );
  1         2  
  1         6  
26              
27 1     1   46 use LINQ::Util::Internal ();
  1         2  
  1         26  
28              
29             use overload (
30 1         5 q[&{}] => 'coderef',
31             q[|] => 'or',
32             q[&] => 'and',
33             q[~] => 'not',
34 1     1   5 );
  1         2  
35              
36             sub _known_parameter_names {
37 41     41   63 my ( $self ) = ( shift );
38            
39             return (
40 41         96 $self->SUPER::_known_parameter_names,
41             'is' => 1,
42             'in' => 1,
43             'like' => 1,
44             'match' => 1,
45             'to' => 1,
46             'cmp' => 1,
47             'numeric' => 0,
48             'string' => 0,
49             'nix' => 0,
50             'nocase' => 0,
51             );
52             } #/ sub _known_parameter_names
53              
54             sub coderef {
55 69     69 1 223 my ( $self ) = ( shift );
56 69   100     239 $self->{coderef} ||= $self->_build_coderef;
57             }
58              
59             sub BUILD {
60 41     41 1 827 my ( $self ) = ( shift );
61 41 100       888 if ( $self->seen_asterisk ) {
62 1         8 LINQ::Util::Internal::throw(
63             "CallerError",
64             message => "Field '*' does not make sense for assertions",
65             );
66             }
67             }
68              
69             sub _build_coderef {
70 40     40   58 my ( $self ) = ( shift );
71 40         58 my @checks = map $self->_make_check( $_ ), @{ $self->fields };
  40         768  
72             return sub {
73 73     73   139 for my $check ( @checks ) {
74 75 100       1094 return !!0 unless $check->( $_ );
75             }
76 34         105 return !!1;
77 37         234 };
78             } #/ sub _build_coderef
79              
80             {
81             my %makers = (
82             'is' => '_make_is_check',
83             'in' => '_make_in_check',
84             'to' => '_make_to_check',
85             'like' => '_make_like_check',
86             'match' => '_make_match_check',
87             );
88              
89             sub _make_check {
90 41     41   260 my ( $self, $field ) = ( shift, @_ );
91            
92 41         60 my @found;
93 41         195 for my $key ( sort keys %makers ) {
94 205 100       3588 push @found, $key if exists $field->params->{$key};
95             }
96            
97 41 100       250 if ( @found > 1 ) {
98 1         6 my $params = join q[, ], map "-$_", @found;
99 1         4 LINQ::Util::Internal::throw(
100             "CallerError",
101 1         17 message => "Multiple conflicting assertions ($params) found for field '@{[ $field->name ]}'",
102             );
103             }
104            
105 40 100       79 if ( @found == 0 ) {
106 1         3 LINQ::Util::Internal::throw(
107             "CallerError",
108 1         17 message => "No assertions found for field '@{[ $field->name ]}'",
109             );
110             }
111            
112 39         75 my $method = $makers{ $found[0] };
113 39         106 return $self->$method( $field );
114             }
115             }
116              
117             {
118             my %templates = (
119             'numeric ==' => '%s == %s',
120             'numeric !=' => '%s != %s',
121             'numeric >' => '%s > %s',
122             'numeric >=' => '%s >= %s',
123             'numeric <' => '%s < %s',
124             'numeric <=' => '%s <= %s',
125             'string ==' => '%s eq %s',
126             'string !=' => '%s ne %s',
127             'string >' => '%s gt %s',
128             'string >=' => '%s ge %s',
129             'string <' => '%s lt %s',
130             'string <=' => '%s le %s',
131             'null ==' => '! defined( %s )',
132             );
133            
134             sub _make_is_check {
135 22     22   44 my ( $self, $field ) = ( shift, @_ );
136 22         47 my $getter = $field->getter;
137            
138 22         369 my $expected = $field->params->{is};
139 22   100     391 my $cmp = $field->params->{cmp} || "==";
140             my $type =
141             $field->params->{numeric} ? 'numeric'
142 22 50       418 : $field->params->{string} ? 'string'
    100          
    100          
    50          
    50          
143             : !defined( $expected ) ? 'null'
144             : $expected =~ /^[0-9]+(?:\.[0-9]+)$/ ? 'numeric'
145             : !ref( $expected ) ? 'string'
146             : 'numeric';
147 22 100       560 my $template = $templates{"$type $cmp"}
148             or LINQ::Util::Internal::throw(
149             "CallerError",
150             message => "Unexpected comparator '$cmp' for type '$type'",
151             );
152            
153 21         31 my $guts;
154 21 100       297 if ( $type eq 'null' ) {
    100          
155 2         9 $guts = sprintf( $template, '$getter->( $_ )' );
156             }
157             elsif ( $field->params->{nocase} ) {
158 1 50       7 my $fold = ( $] > 5.016 ) ? 'CORE::fc' : 'lc';
159             $guts = sprintf(
160             $template,
161             "$fold( \$getter->( \$_ ) )",
162 1 50       4 ref( $expected ) ? "$fold( \$expected )" : do {
163 1         42 require B;
164 1         10 "$fold( " . B::perlstring( $expected ) . ' )';
165             },
166             );
167             } #/ elsif ( $field->params->{...})
168             else {
169             $guts = sprintf(
170             $template,
171             '$getter->( $_ )',
172 18 50       83 ref( $expected ) ? '$expected' : do {
173 18         63 require B;
174 18         96 B::perlstring( $expected );
175             },
176             );
177             } #/ else [ if ( $type eq 'null' )]
178            
179 21 100       335 if ( $field->params->{nix} ) {
180 1         7 $guts = "!( $guts )";
181             }
182            
183 1     1   951 no warnings qw( uninitialized );
  1         2  
  1         293  
184 21         1730 return eval "sub { $guts }";
185             }
186            
187             sub _make_to_check {
188 7     7   12 my ( $self, $field ) = ( shift, @_ );
189 7         21 my $getter = $field->getter;
190            
191 7         141 my $other = 'LINQ::Field'->new( value => $field->params->{to} )->getter;
192 7   100     23 my $cmp = $field->params->{cmp} || "==";
193 7 100       308 my $type = $field->params->{string} ? 'string' : 'numeric';
194 7 50       47 my $template = $templates{"$type $cmp"}
195             or LINQ::Util::Internal::throw(
196             "CallerError",
197             message => "Unexpected comparator '$cmp' for type '$type'",
198             );
199            
200 7         12 my $guts;
201 7 100       102 if ( $field->params->{nocase} ) {
202 2 50       13 my $fold = ( $] > 5.016 ) ? 'CORE::fc' : 'lc';
203 2         10 $guts = sprintf(
204             $template,
205             "$fold( \$getter->( \$_ ) )",
206             "$fold( \$other->( \$_ ) )",
207             );
208             } #/ elsif ( $field->params->{...})
209             else {
210 5         35 $guts = sprintf(
211             $template,
212             '$getter->( $_ )',
213             '$other->( $_ )',
214             );
215             } #/ else [ if ( $type eq 'null' )]
216            
217 7 100       107 if ( $field->params->{nix} ) {
218 1         7 $guts = "!( $guts )";
219             }
220            
221 1     1   8 no warnings qw( uninitialized );
  1         2  
  1         1570  
222 7         577 return eval "sub { $guts }";
223             }
224             }
225              
226             sub _make_in_check {
227 3     3   6 my ( $self, $field ) = ( shift, @_ );
228 3         9 my $getter = $field->getter;
229 3         5 my @expected = @{ $field->params->{in} };
  3         114  
230            
231 3         66 my $nix = ! $field->params->{nix};
232            
233 3 100       53 if ( $field->params->{nocase} ) {
234             return sub {
235 2     2   9 my $value = lc $getter->( $_ );
236 2         4 for my $expected ( @expected ) {
237 3 100       16 return !!$nix if $value eq lc $expected;
238             }
239 1         4 return !$nix;
240 1         10 };
241             }
242             else {
243             return sub {
244 4     4   7 my $value = $getter->( $_ );
245 4         8 for my $expected ( @expected ) {
246 7 100       16 return !!$nix if $value eq $expected;
247             }
248 3         11 return !$nix;
249 2         21 };
250             }
251             }
252              
253             {
254             my $_like_to_regexp = sub {
255             my ( $like, $ci ) = @_;
256             my $re = '';
257             my %anchors = (
258             start => substr( $like, 0, 1 ) ne '%',
259             end => substr( $like, -1, 1 ) ne '%',
260             );
261             my @parts = split qr{(\\*[.%])}, $like;
262             for my $p ( @parts ) {
263             next unless length $p;
264             my $backslash_count =()= $p =~ m{(\\)}g;
265             my $wild_count =()= $p =~ m{([%.])}g;
266             if ( $wild_count ) {
267             if ( $backslash_count && $backslash_count % 2 ) {
268             my $last = substr( $p, -2, 2, '' );
269             $p =~ s{\\\\}{\\};
270             $re .= quotemeta( $p . substr( $last, -1, 1 ) );
271             }
272             elsif ( $backslash_count ) {
273             my $last = substr( $p, -1, 1, '' );
274             $p =~ s{\\\\}{\\};
275             $re .= quotemeta( $p ) . ( $last eq '%' ? '.*' : '.' );
276             }
277             else {
278             $re .= $p eq '%' ? '.*' : '.';
279             }
280             } #/ if ( $wild_count )
281             else {
282             $p =~ s{\\(.)}{$1}g;
283             $re .= quotemeta( $p );
284             }
285             } #/ for my $p ( @parts )
286            
287             substr( $re, 0, 0, '\A' ) if $anchors{start};
288             $re .= '\z' if $anchors{end};
289            
290             $ci ? qr/$re/i : qr/$re/;
291             };
292              
293             sub _make_like_check {
294 5     5   11 my ( $self, $field ) = ( shift, @_ );
295 5         12 my $getter = $field->getter;
296            
297             my $match = $_like_to_regexp->(
298             $field->params->{like},
299             $field->params->{nocase},
300 5         81 );
301            
302 5 100       93 if ( $field->params->{nix} ) {
303             return sub {
304 2     2   5 my $value = $getter->( $_ );
305 2         8 $value !~ $match;
306 1         10 };
307             }
308             else {
309             return sub {
310 6     6   11 my $value = $getter->( $_ );
311 6         33 $value =~ $match;
312 4         40 };
313             }
314             }
315             }
316              
317             sub _make_match_check {
318 2     2   5 my ( $self, $field ) = ( shift, @_ );
319 2         6 my $getter = $field->getter;
320            
321 2         33 my $match = $field->params->{match};
322            
323 2         607 require match::simple;
324            
325 2 100       2165 if ( $field->params->{nix} ) {
326             return sub {
327 2     2   5 my $value = $getter->( $_ );
328 2         9 not match::simple::match( $value, $match );
329 1         10 };
330             }
331             else {
332             return sub {
333 2     2   5 my $value = $getter->( $_ );
334 2         14 match::simple::match( $value, $match );
335 1         14 };
336             }
337             }
338              
339             sub not {
340 4     4 1 136 my ( $self ) = ( shift );
341 4         18 return 'LINQ::FieldSet::Assertion::NOT'->new(
342             left => $self,
343             );
344             }
345              
346             sub and {
347 5     5 1 59 my ( $self, $other ) = &$_process_args;
348 5         23 return 'LINQ::FieldSet::Assertion::AND'->new(
349             left => $self,
350             right => $other,
351             );
352             }
353              
354             sub or {
355 1     1 1 22 my ( $self, $other ) = &$_process_args;
356 1         10 return 'LINQ::FieldSet::Assertion::OR'->new(
357             left => $self,
358             right => $other,
359             );
360             }
361              
362             package LINQ::FieldSet::Assertion::Combination;
363              
364             our $AUTHORITY = 'cpan:TOBYINK';
365             our $VERSION = '0.001';
366              
367 1     1   8 use Role::Tiny;
  1         2  
  1         6  
368             requires( qw/ left right _build_coderef / );
369              
370             sub coderef {
371 20     20   501 my ( $self ) = ( shift );
372 20   100     95 $self->{coderef} ||= $self->_build_coderef;
373             }
374              
375             sub not {
376 1     1   25 my ( $self ) = ( shift );
377 1         6 return 'LINQ::FieldSet::Assertion::NOT'->new(
378             left => $self,
379             );
380             }
381              
382             sub and {
383 1     1   27 my ( $self, $other ) = &$_process_args;
384 1         4 return 'LINQ::FieldSet::Assertion::AND'->new(
385             left => $self,
386             right => $other,
387             );
388             }
389              
390             sub or {
391 2     2   54 my ( $self, $other ) = &$_process_args;
392 2         9 return 'LINQ::FieldSet::Assertion::OR'->new(
393             left => $self,
394             right => $other,
395             );
396             }
397              
398             package LINQ::FieldSet::Assertion::NOT;
399              
400             our $AUTHORITY = 'cpan:TOBYINK';
401             our $VERSION = '0.001';
402              
403 1     1   414 use Class::Tiny qw( left );
  1         2  
  1         26  
404 1     1   241 use Role::Tiny::With ();
  1         2  
  1         24  
405             Role::Tiny::With::with( 'LINQ::FieldSet::Assertion::Combination' );
406              
407 1     1   5 use overload ();
  1         2  
  1         196  
408             'overload'->import(
409             q[&{}] => 'coderef',
410             q[|] => 'or',
411             q[&] => 'and',
412             q[~] => 'not',
413             );
414              
415             sub _build_coderef {
416 3     3   5 my ( $self ) = ( shift );
417 3         53 my $left = $self->left->coderef;
418 3     6   26 return sub { not $left->( $_ ) };
  6         12  
419             }
420              
421             sub not {
422 1     1   26 my ( $self ) = ( shift );
423 1         18 return $self->left;
424             }
425              
426             sub right {
427 1     1   28 LINQ::Util::Internal::throw(
428             "InternalError",
429             message => 'Unexpected second branch to NOT.',
430             );
431             }
432              
433             package LINQ::FieldSet::Assertion::AND;
434              
435             our $AUTHORITY = 'cpan:TOBYINK';
436             our $VERSION = '0.001';
437              
438 1     1   9 use Class::Tiny qw( left right );
  1         2  
  1         4  
439 1     1   376 use Role::Tiny::With ();
  1         2  
  1         22  
440             Role::Tiny::With::with( 'LINQ::FieldSet::Assertion::Combination' );
441              
442 1     1   5 use overload ();
  1         2  
  1         145  
443             'overload'->import(
444             q[&{}] => 'coderef',
445             q[|] => 'or',
446             q[&] => 'and',
447             q[~] => 'not',
448             );
449              
450             sub _build_coderef {
451 6     6   11 my ( $self ) = ( shift );
452 6         136 my $left = $self->left->coderef;
453 6         109 my $right = $self->right->coderef;
454 6 100   17   33 return sub { $left->( $_ ) and $right->( $_ ) };
  17         30  
455             }
456              
457             package LINQ::FieldSet::Assertion::OR;
458              
459             our $AUTHORITY = 'cpan:TOBYINK';
460             our $VERSION = '0.001';
461              
462 1     1   7 use Class::Tiny qw( left right );
  1         1  
  1         4  
463 1     1   339 use Role::Tiny::With ();
  1         3  
  1         36  
464             Role::Tiny::With::with( 'LINQ::FieldSet::Assertion::Combination' );
465              
466 1     1   6 use overload ();
  1         2  
  1         133  
467             'overload'->import(
468             q[&{}] => 'coderef',
469             q[|] => 'or',
470             q[&] => 'and',
471             q[~] => 'not',
472             );
473              
474             sub _build_coderef {
475 3     3   6 my ( $self ) = ( shift );
476 3         51 my $left = $self->left->coderef;
477 3         56 my $right = $self->right->coderef;
478 3 100   10   12 return sub { $left->( $_ ) or $right->( $_ ) };
  10         17  
479             }
480              
481             1;
482              
483             __END__
484              
485             =pod
486              
487             =encoding utf-8
488              
489             =head1 NAME
490              
491             LINQ::FieldSet::Assertion - represents an SQL-WHERE-like assertion/check
492              
493             =head1 DESCRIPTION
494              
495             LINQ::FieldSet::Assertion is a subclass of L<LINQ::FieldSet>.
496              
497             This is used internally by LINQ and you probably don't need to know about it
498             unless you're writing very specific extensions for LINQ. The end user
499             interface is the C<check_fields> function in L<LINQ::Util>.
500              
501             =head1 CONSTRUCTOR
502              
503             =over
504              
505             =item C<< new( ARGSLIST ) >>
506              
507             Constructs a fieldset from a list of fields like:
508              
509             'LINQ::FieldSet::Assertion'->new(
510             'field1', -param1 => 'value1', -param2,
511             'field2', -param1 => 'value2',
512             );
513              
514             Allowed parameters are:
515             C<< -is >> (followed by a value),
516             C<< -to >> (followed by a value),
517             C<< -in >> (followed by a value),
518             C<< -like >> (followed by a value),
519             C<< -match >> (followed by a value),
520             C<< -cmp >> (followed by a value),
521             C<< -numeric >> (no value),
522             C<< -string >> (no value),
523             C<< -not >> (no value), and
524             C<< -nocase >> (no value).
525              
526             =back
527              
528             =begin trustme
529              
530             =item BUILD
531              
532             =end trustme
533              
534             =head1 METHODS
535              
536             =over
537              
538             =item C<< and( OTHER ) >>
539              
540             Return a LINQ::FieldSet::Assertion::AND object which is a conjunction of this
541             assertion and another assertion.
542              
543             =item C<< or( OTHER ) >>
544              
545             Return a LINQ::FieldSet::Assertion::OR object which is an inclusive disjunction
546             of this assertion and another assertion.
547              
548             =item C<not>
549              
550             Return a LINQ::FieldSet::Assertion::NOT object which is the negation of this
551             assertion.
552              
553             =item C<coderef>
554              
555             Gets a coderef for this assertion; the coderef operates on C<< $_ >>.
556              
557             =back
558              
559             The LINQ::FieldSet::Assertion::{AND,OR,NOT} classes are lightweight classes
560             which also implement the C<and>, C<or>, C<not>, and C<coderef> methods, and
561             have the same overloading as LINQ::FieldSet::Assertion, but do not inherit
562             from it.
563              
564             =head1 OVERLOADING
565              
566             This class overloads
567             C<< & >> to call the C<< and >> method;
568             C<< | >> to call the C<< or >> method;
569             C<< ~ >> to call the C<< not >> method; and
570             C<< &{} >> to call the C<< coderef >> method.
571              
572             =head1 BUGS
573              
574             Please report any bugs to
575             L<http://rt.cpan.org/Dist/Display.html?Queue=LINQ>.
576              
577             =head1 SEE ALSO
578              
579             L<LINQ::FieldSet>, L<LINQ::Util>.
580              
581             =head1 AUTHOR
582              
583             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
584              
585             =head1 COPYRIGHT AND LICENCE
586              
587             This software is copyright (c) 2021 by Toby Inkster.
588              
589             This is free software; you can redistribute it and/or modify it under
590             the same terms as the Perl 5 programming language system itself.
591              
592             =head1 DISCLAIMER OF WARRANTIES
593              
594             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
595             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
596             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.