File Coverage

blib/lib/Params/CheckCompiler/Compiler.pm
Criterion Covered Total %
statement 220 240 91.6
branch 60 74 81.0
condition 4 6 66.6
subroutine 33 36 91.6
pod 0 6 0.0
total 317 362 87.5


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