File Coverage

blib/lib/LINQ/FieldSet/Assertion.pm
Criterion Covered Total %
statement 184 188 97.8
branch 50 58 86.2
condition 10 10 100.0
subroutine 51 53 96.2
pod 6 6 100.0
total 301 315 95.5


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