File Coverage

blib/lib/Params/CheckCompiler/Compiler.pm
Criterion Covered Total %
statement 197 243 81.0
branch 53 74 71.6
condition 4 6 66.6
subroutine 33 37 89.1
pod 0 6 0.0
total 287 366 78.4


line stmt bran cond sub pod time code
1             package Params::CheckCompiler::Compiler;
2              
3 8     8   26 use strict;
  8         5  
  8         160  
4 8     8   20 use warnings;
  8         8  
  8         214  
5              
6             our $VERSION = '0.07';
7              
8 8     8   3291 use Eval::Closure;
  8         20449  
  8         299  
9 8     8   3390 use List::SomeUtils qw( first_index );
  8         93387  
  8         57  
10 8     8   5565 use Params::CheckCompiler::Exceptions;
  8         15  
  8         194  
11 8     8   33 use Scalar::Util qw( blessed looks_like_number reftype );
  8         18  
  8         298  
12 8     8   26 use Sub::Name qw( subname );
  8         6  
  8         233  
13 8     8   23 use overload ();
  8         6  
  8         13222  
14              
15             # I'd rather use Moo here but I want to make things relatively high on the
16             # CPAN river like DateTime use this distro, so reducing deps is important.
17             sub new {
18 17     17 0 32 my $class = shift;
19 17         42 my %p = @_;
20              
21 17         31 my $self = bless \%p, $class;
22              
23 17         60 $self->{_source} = [];
24 17         136 $self->{_env} = {};
25              
26 17         46 return $self;
27             }
28              
29 1     1 0 4 sub name { $_[0]->{name} }
30 17     17   64 sub _has_name { exists $_[0]->{name} }
31              
32             # I have no idea why critic thinks _caller isn't used.
33              
34             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
35 0     0   0 sub _caller { $_[0]->{caller} }
36             ## use critic
37 1     1   5 sub _has_caller { exists $_[0]->{caller} }
38              
39 50     50 0 114 sub params { $_[0]->{params} }
40              
41 25     25 0 133 sub slurpy { $_[0]->{slurpy} }
42              
43 115     115   312 sub _source { $_[0]->{_source} }
44              
45 36     36   1168 sub _env { $_[0]->{_env} }
46              
47             sub subref {
48 17     17 0 20 my $self = shift;
49              
50 17         31 $self->_compile;
51             my $sub = eval_closure(
52 17         20 source => 'sub { ' . ( join "\n", @{ $self->_source } ) . ' };',
  17         21  
53             environment => $self->_env,
54             );
55              
56 17 100       6590 if ( $self->_has_name ) {
57 1 50       3 my $caller = $self->_has_caller ? $self->_caller : caller(1);
58 1         2 my $name = join '::', $caller, $self->name;
59 1         7 subname( $name, $sub );
60             }
61              
62 17         37 return $sub;
63             }
64              
65             sub source {
66 0     0 0 0 my $self = shift;
67              
68 0         0 $self->_compile;
69             return (
70 0         0 ( join "\n", @{ $self->_source } ),
  0         0  
71             $self->_env,
72             );
73             }
74              
75             sub _compile {
76 17     17   20 my $self = shift;
77              
78 17 100       30 if ( ref $self->params eq 'HASH' ) {
    50          
79 13         32 $self->_compile_named_args_check;
80             }
81             elsif ( ref $self->params eq 'ARRAY' ) {
82 4         9 $self->_compile_positional_args_check;
83             }
84             }
85              
86             sub _compile_named_args_check {
87 13     13   14 my $self = shift;
88              
89 13         13 push @{ $self->_source }, $self->_set_named_args_hash;
  13         21  
90              
91 13         17 my $params = $self->params;
92              
93 13         15 for my $name ( sort keys %{$params} ) {
  13         53  
94 18         18 my $spec = $params->{$name};
95 18 100       41 $spec = { optional => !$spec } unless ref $spec;
96              
97 18         55 my $qname = B::perlstring($name);
98 18         26 my $access = "\$args{$qname}";
99              
100             $self->_add_check_for_required_named_param( $access, $name )
101 18 100 66     77 unless $spec->{optional} || exists $spec->{default};
102              
103             $self->_add_default_assignment( $access, $name, $spec->{default} )
104 18 100       41 if exists $spec->{default};
105              
106             $self->_add_type_check( $access, $name, $spec )
107 18 100       121 if $spec->{type};
108             }
109              
110 13 100       22 if ( $self->slurpy ) {
111 2 100       6 $self->_add_check_for_extra_hash_param_types( $self->slurpy )
112             if ref $self->slurpy;
113             }
114             else {
115 11         22 $self->_add_check_for_extra_hash_params;
116             }
117              
118 13         15 push @{ $self->_source }, 'return %args;';
  13         17  
119              
120 13         15 return;
121             }
122              
123             sub _set_named_args_hash {
124 13     13   14 my $self = shift;
125              
126 13         10 push @{ $self->_source }, <<'EOF';
  13         19  
127             my %args;
128             if ( @_ % 2 == 0 ) {
129             %args = @_;
130             }
131             elsif ( @_ == 1 ) {
132             if ( ref $_[0] ) {
133             if ( Scalar::Util::blessed( $_[0] ) ) {
134             if ( overload::Overloaded( $_[0] )
135             && defined overload::Method( $_[0], '%{}' ) ) {
136              
137             %args = %{ $_[0] };
138             }
139             else {
140             Params::CheckCompiler::Exception::BadArguments->throw(
141             message =>
142             'Expected a hash or hash reference but got a single object argument'
143             );
144             }
145             }
146             elsif ( Scalar::Util::reftype( $_[0] ) eq 'HASH' ) {
147             %args = %{ $_[0] };
148             }
149             else {
150             Params::CheckCompiler::Exception::BadArguments->throw(
151             message =>
152             'Expected a hash or hash reference but got a single '
153             . ( Scalar::Util::reftype( $_[0] ) )
154             . ' reference argument',
155             );
156             }
157             }
158             else {
159             Params::CheckCompiler::Exception::BadArguments->throw(
160             message =>
161             'Expected a hash or hash reference but got a single non-reference argument',
162             );
163             }
164             }
165             else {
166             Params::CheckCompiler::Exception::BadArguments->throw(
167             message =>
168             'Expected a hash or hash reference but got an odd number of arguments',
169             );
170             }
171             EOF
172              
173 13         17 return;
174             }
175              
176             sub _add_check_for_required_named_param {
177 12     12   13 my $self = shift;
178 12         10 my $access = shift;
179 12         11 my $name = shift;
180              
181 12         20 my $qname = B::perlstring($name);
182 12         12 push @{ $self->_source }, sprintf( <<'EOF', $access, ($qname) x 2 );
  12         17  
183             exists %s
184             or Params::CheckCompiler::Exception::Named::Required->throw(
185             message => %s . ' is a required parameter',
186             parameter => %s,
187             );
188             EOF
189              
190 12         13 return;
191             }
192              
193             sub _add_check_for_extra_hash_param_types {
194 1     1   1 my $self = shift;
195 1         1 my $type = shift;
196              
197 1         2 $self->_env->{'%known'} = { map { $_ => 1 } keys %{ $self->params } };
  1         3  
  1         1  
198              
199             # We need to set the name argument to something that won't conflict with
200             # names someone would actually use for a parameter.
201 1         4 my $check = join q{}, $self->_type_check(
202             '$args{$key}',
203             '__PCC extra parameters__',
204             $type,
205             );
206 1         2 push @{ $self->_source }, sprintf( <<'EOF', $check );
  1         2  
207             for my $key ( grep { !$known{$_} } keys %%args ) {
208             %s;
209             }
210             EOF
211              
212 1         1 return;
213             }
214              
215             sub _add_check_for_extra_hash_params {
216 11     11   10 my $self = shift;
217              
218 11         11 $self->_env->{'%known'} = { map { $_ => 1 } keys %{ $self->params } };
  16         36  
  11         14  
219 11         13 push @{ $self->_source }, <<'EOF';
  11         17  
220             my @extra = grep { ! $known{$_} } keys %args;
221             if ( @extra ) {
222             my $u = join ', ', sort @extra;
223             Params::CheckCompiler::Exception::Named::Extra->throw(
224             message => "found extra parameters: [$u]",
225             parameters => \@extra,
226             );
227             }
228             EOF
229              
230 11         12 return;
231             }
232              
233             sub _compile_positional_args_check {
234 4     4   4 my $self = shift;
235              
236 4         8 my @specs = $self->_munge_and_check_positional_params;
237              
238 4     6   38 my $first_optional_idx = first_index { $_->{optional} } @specs;
  6         8  
239              
240             # If optional params start anywhere after the first parameter spec then we
241             # must require at least one param. If there are no optional params then
242             # they're all required.
243 4 100       24 $self->_add_check_for_required_positional_params(
    50          
244             $first_optional_idx == -1 ? ( scalar @specs ) : $first_optional_idx )
245             if $first_optional_idx != 0;
246              
247 4 100       6 $self->_add_check_for_extra_positional_params( scalar @specs )
248             unless $self->slurpy;
249              
250 4         15 for my $i ( 0 .. $#specs ) {
251 6         4 my $spec = $specs[$i];
252              
253 6         9 my $name = "Parameter $i";
254 6         7 my $access = "\$_[$i]";
255              
256             $self->_add_default_assignment( $access, $name, $spec->{default} )
257 6 50       12 if exists $spec->{default};
258              
259             $self->_add_type_check( $access, $name, $spec )
260 6 100       52 if $spec->{type};
261             }
262              
263 4 100       6 if ( ref $self->slurpy ) {
264 1         2 $self->_add_check_for_extra_positional_param_types(
265             scalar @specs,
266             $self->slurpy,
267             );
268             }
269              
270 4         6 push @{ $self->_source }, 'return @_;';
  4         5  
271              
272 4         5 return;
273             }
274              
275             sub _munge_and_check_positional_params {
276 4     4   4 my $self = shift;
277              
278 4         4 my @specs;
279 4         5 my $in_optional = 0;
280              
281 4         4 for my $spec ( @{ $self->params } ) {
  4         6  
282 6 100       16 $spec = ref $spec ? $spec : { optional => !$spec };
283 6 100       15 if ( $spec->{optional} ) {
    50          
284 2         3 $in_optional = 1;
285             }
286             elsif ($in_optional) {
287 0         0 die
288             'Parameter list contains an optional parameter followed by a required parameter.';
289             }
290              
291 6         8 push @specs, $spec;
292             }
293              
294 4         9 return @specs;
295             }
296              
297             sub _add_check_for_required_positional_params {
298 4     4   4 my $self = shift;
299 4         5 my $min = shift;
300              
301 4         2 push @{ $self->_source }, sprintf( <<'EOF', ($min) x 3 );
  4         9  
302             if ( @_ < %d ) {
303             my $got = scalar @_;
304             my $got_n = @_ == 1 ? 'parameter' : 'parameters';
305             Params::CheckCompiler::Exception::Positional::Required->throw(
306             message => "got $got $got_n but expected at least %d",
307             minimum => %d,
308             got => scalar @_,
309             );
310             }
311             EOF
312              
313 4         6 return;
314             }
315              
316             sub _add_check_for_extra_positional_param_types {
317 1     1   1 my $self = shift;
318 1         2 my $max = shift;
319 1         1 my $type = shift;
320              
321             # We need to set the name argument to something that won't conflict with
322             # names someone would actually use for a parameter.
323 1         3 my $check = join q{}, $self->_type_check(
324             '$_[$i]',
325             '__PCC extra parameters__',
326             $type,
327             );
328 1         2 push @{ $self->_source }, sprintf( <<'EOF', $max, $max, $check );
  1         2  
329             if ( @_ > %d ) {
330             for my $i ( %d .. $#_ ) {
331             %s;
332             }
333             }
334             EOF
335              
336 1         2 return;
337             }
338              
339             sub _add_check_for_extra_positional_params {
340 2     2   4 my $self = shift;
341 2         2 my $max = shift;
342              
343 2         3 push @{ $self->_source }, sprintf( <<'EOF', ($max) x 3 );
  2         4  
344             if ( @_ > %d ) {
345             my $extra = @_ - %d;
346             my $extra_n = $extra == 1 ? 'parameter' : 'parameters';
347             Params::CheckCompiler::Exception::Positional::Extra->throw(
348             message => "got $extra extra $extra_n",
349             maximum => %d,
350             got => scalar @_,
351             );
352             }
353             EOF
354              
355 2         2 return;
356             }
357              
358             sub _add_default_assignment {
359 4     4   3 my $self = shift;
360 4         3 my $access = shift;
361 4         3 my $name = shift;
362 4         3 my $default = shift;
363              
364 4 50 66     11 die 'Default must be either a plain scalar or a subroutine reference'
365             if ref $default && reftype($default) ne 'CODE';
366              
367 4         6 my $qname = B::perlstring($name);
368 4         4 push @{ $self->_source }, "unless ( exists \$args{$qname} ) {";
  4         4  
369              
370 4 100       5 if ( ref $default ) {
371 1         2 push @{ $self->_source }, "$access = \$defaults{$qname}->();";
  1         1  
372 1         3 $self->_env->{'%defaults'}{$name} = $default;
373             }
374             else {
375 3 100       4 if ( defined $default ) {
376 2 100       5 if ( looks_like_number($default) ) {
377 1         2 push @{ $self->_source }, "$access = $default;";
  1         1  
378             }
379             else {
380 1         1 push @{ $self->_source },
  1         2  
381             "$access = " . B::perlstring($default) . ';';
382             }
383             }
384             else {
385 1         1 push @{ $self->_source }, "$access = undef;";
  1         2  
386             }
387             }
388              
389 4         2 push @{ $self->_source }, '}';
  4         5  
390              
391 4         4 return;
392             }
393              
394             sub _add_type_check {
395 8     8   42 my $self = shift;
396 8         6 my $access = shift;
397 8         8 my $name = shift;
398 8         7 my $spec = shift;
399              
400 8         7 my $type = $spec->{type};
401 8 50       26 die "Passed a type that is not an object for $name: $type"
402             unless blessed $type;
403              
404 2         4 push @{ $self->_source }, sprintf( 'if ( exists %s ) {', $access )
405 8 100       15 if $spec->{optional};
406              
407 8         11 push @{ $self->_source },
408 8         7 $self->_type_check( $access, $name, $spec->{type} );
409              
410 2         5 push @{ $self->_source }, '}'
411 8 100       19 if $spec->{optional};
412              
413 8         19 return;
414             }
415              
416             sub _type_check {
417 10     10   9 my $self = shift;
418 10         9 my $access = shift;
419 10         9 my $name = shift;
420 10         7 my $type = shift;
421              
422             # Specio
423 10 0       27 return $type->can('can_inline_coercion_and_check')
    50          
    50          
424             ? $self->_add_specio_check( $access, $name, $type )
425              
426             # Type::Tiny
427             : $type->can('inline_assert')
428             ? $self->_add_type_tiny_check( $access, $name, $type )
429              
430             # Moose
431             : $type->can('can_be_inlined')
432             ? $self->_add_moose_check( $access, $name, $type )
433             : die 'Unknown type object ' . ref $type;
434             }
435              
436             sub _add_type_tiny_check {
437 10     10   187 my $self = shift;
438 10         11 my $access = shift;
439 10         11 my $name = shift;
440 10         7 my $type = shift;
441              
442 10         9 my @source;
443 10 100       24 if ( $type->has_coercion ) {
444 4         32 my $coercion = $type->coercion;
445 4 100       18 if ( $coercion->can_be_inlined ) {
446 1         55 push @source,
447             "$access = " . $coercion->inline_coercion($access) . ';';
448             }
449             else {
450 3         93 $self->_env->{'%tt_coercions'}{$name}
451             = $coercion->compiled_coercion;
452 3         16 push @source,
453             sprintf(
454             '%s = $tt_coercions{%s}->( %s );',
455             $access, $name, $access,
456             );
457             }
458             }
459              
460 10 100       344 if ( $type->can_be_inlined ) {
461 7         128 push @source,
462             $type->inline_assert($access);
463             }
464             else {
465 3         45 push @source,
466             sprintf( '$types{%s}->assert_valid( %s );', $name, $access );
467 3         4 $self->_env->{'%types'}{$name} = $type;
468             }
469              
470 10         388 return @source;
471             }
472              
473             sub _add_specio_check {
474 0     0     my $self = shift;
475 0           my $access = shift;
476 0           my $name = shift;
477 0           my $type = shift;
478              
479 0           my $qname = B::perlstring($name);
480              
481 0           my @source;
482              
483 0 0         if ( $type->can_inline_coercion_and_check ) {
484 0 0         if ( $type->has_coercions ) {
485 0           my ( $source, $env ) = $type->inline_coercion_and_check($access);
486 0           push @source, sprintf( '%s = %s;', $access, $source );
487 0           $self->_env->{$_} = $env->{$_} for keys %{$env};
  0            
488             }
489             else {
490 0           my ( $source, $env ) = $type->inline_assert($access);
491 0           push @source, $source . ';';
492 0           $self->_env->{$_} = $env->{$_} for keys %{$env};
  0            
493             }
494             }
495             else {
496 0           my @coercions = $type->coercions;
497 0           $self->_env->{'%specio_coercions'}{$name} = \@coercions;
498 0           for my $i ( 0 .. $#coercions ) {
499 0           my $c = $coercions[$i];
500 0 0         if ( $c->can_be_inlined ) {
501 0           push @source,
502             sprintf(
503             '%s = %s if %s;',
504             $access,
505             $c->inline_coercion($access),
506             $c->from->inline_check($access)
507             );
508             }
509             else {
510 0           push @source,
511             sprintf(
512             '%s = $specio_coercions{%s}[%s]->coerce(%s) if $specio_coercions{%s}[%s]->from->value_is_valid(%s);',
513             $access,
514             $qname,
515             $i,
516             $access,
517             $qname,
518             $i,
519             $access
520             );
521             }
522             }
523              
524 0           push @source,
525             sprintf( '$types{%s}->validate_or_die(%s);', $name, $access );
526 0           $self->_env->{'%types'}{$name} = $type;
527             }
528              
529 0           return @source;
530             }
531              
532             sub _add_moose_check {
533 0     0     my $self = shift;
534 0           my $access = shift;
535 0           my $name = shift;
536 0           my $type = shift;
537              
538 0           my @source;
539              
540 0 0         if ( $type->has_coercion ) {
541 0           $self->_env->{'%moose_coercions'}{$name} = $type->coercion;
542 0           push @source,
543             sprintf(
544             '%s = $moose_coercions{%s}->coerce( %s );',
545             $access, $name, $access,
546             );
547             }
548              
549 0           $self->_env->{'%types'}{$name} = $type;
550              
551 0           my $code = <<'EOF';
552             if ( !%s ) {
553             my $type = $types{%s};
554             my $msg = $type->get_message(%s);
555             die
556             Params::CheckCompiler::Exception::ValidationFailedForMooseTypeConstraint
557             ->new(
558             message => $msg,
559             parameter => 'The ' . %s . ' parameter',
560             value => %s,
561             type => $type,
562             );
563             }
564             EOF
565              
566 0 0         my $check
567             = $type->can_be_inlined
568             ? $type->_inline_check($access)
569             : sprintf( '$types{%s}->check( %s )', $name, $access );
570              
571 0           my $qname = B::perlstring($name);
572 0           push @source, sprintf(
573             $code,
574             $check,
575             $qname,
576             $access,
577             $qname,
578             $access,
579             );
580              
581 0           return @source;
582             }
583              
584             1;
585              
586             # ABSTRACT: Object that implements the check subroutine compilation
587              
588             __END__
589              
590             =pod
591              
592             =encoding UTF-8
593              
594             =head1 NAME
595              
596             Params::CheckCompiler::Compiler - Object that implements the check subroutine compilation
597              
598             =head1 VERSION
599              
600             version 0.07
601              
602             =for Pod::Coverage .*
603              
604             =head1 SUPPORT
605              
606             Bugs may be submitted through L<the RT bug tracker|http://rt.cpan.org/Public/Dist/Display.html?Name=Params-CheckCompiler>
607             (or L<bug-params-checkcompiler@rt.cpan.org|mailto:bug-params-checkcompiler@rt.cpan.org>).
608              
609             I am also usually active on IRC as 'drolsky' on C<irc://irc.perl.org>.
610              
611             =head1 AUTHOR
612              
613             Dave Rolsky <autarch@urth.org>
614              
615             =head1 COPYRIGHT AND LICENCE
616              
617             This software is Copyright (c) 2016 by Dave Rolsky.
618              
619             This is free software, licensed under:
620              
621             The Artistic License 2.0 (GPL Compatible)
622              
623             =cut