File Coverage

blib/lib/MooseX/Params/Util.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package MooseX::Params::Util;
2             {
3             $MooseX::Params::Util::VERSION = '0.010';
4             }
5              
6             # ABSTRACT: Parameter processing utilities
7              
8 8     8   85289 use strict;
  8         14  
  8         234  
9 8     8   37 use warnings;
  8         15  
  8         177  
10 8     8   84 use 5.10.0;
  8         26  
  8         376  
11 8     8   13395 use Moose::Util::TypeConstraints qw(find_type_constraint);
  0            
  0            
12             use Try::Tiny qw(try catch);
13             use List::Util qw(max first);
14             use Scalar::Util qw(isweak);
15             use Perl6::Caller qw(caller);
16             use B::Hooks::EndOfScope qw(on_scope_end); # magic fails without this, have to find out why ...
17             use Sub::Identify qw(sub_name);
18             use Sub::Mutate qw(when_sub_bodied);
19             use Scalar::Readonly qw(readonly_on);
20             use Carp qw(croak);
21             use Class::MOP::Class;
22             use MooseX::Params::Meta::Method;
23             use Package::Stash;
24             use Text::CSV_XS;
25             use MooseX::Params::Meta::Parameter;
26             use MooseX::Params::Magic::Wizard;
27              
28             # DESCRIPTION: Build a parameter from either a default value or a builder
29             # USED BY: MooseX::Params::Util::process_args
30             sub build
31             {
32             my ($param, $stash) = @_;
33              
34             my $value;
35              
36             my $default = $param->default;
37              
38             if (defined $default and ref($default) ne 'CODE')
39             {
40             $value = $default;
41             }
42             else
43             {
44             my $coderef;
45              
46             if ($default)
47             {
48             $coderef = $default;
49             }
50             else
51             {
52             my $coderef = $stash->get_symbol('&' . $param->builder);
53             Carp::croak("Cannot find builder " . $param->builder) unless $coderef;
54             }
55              
56             $value = try {
57             $coderef->();
58             } catch {
59             Carp::croak("Error executing builder for parameter " . $param->name . ": $_");
60             };
61             }
62              
63             return $value;
64             }
65              
66             # DESCRIPTION: Localize %_ around a method
67             # USED BY: MooseX::Params::Args
68             sub wrap_method
69             {
70             my ($package_name, $method_name, $coderef) = @_;
71              
72             return sub
73             {
74             my $meta = Class::MOP::Class->initialize($package_name);
75             my $method = $meta->get_method($method_name);
76             my $wantarray = wantarray;
77              
78             local %_;
79              
80             if ( $method->has_parameters )
81             {
82              
83             %_ = process_args($meta, $method, @_);
84             readonly_on($_) for values %_;
85             my $wizard = MooseX::Params::Magic::Wizard->new;
86              
87             Variable::Magic::cast(%_, $wizard,
88             parameters => $method->parameters,
89             wrapper => \&wrap_param_builder,
90             package => $package_name,
91             );
92             }
93              
94             if ( $method->has_return_value_constraint)
95             {
96             return process_return_values($method, $wantarray, $coderef->(@_));
97             }
98             else
99             {
100             return $coderef->(@_);
101             }
102             };
103             }
104              
105             # DESCRIPTION: Localize %_ around a parameter builder
106             # USED BY: MooseX::Params::Wizard::fetch
107             sub wrap_param_builder
108             {
109             my ($coderef, $package_name, $parameters, $key) = @_;
110              
111             return sub
112             {
113             local %_ = @_;
114              
115             my $wizard = MooseX::Params::Magic::Wizard->new;
116              
117             Variable::Magic::cast(%_, $wizard,
118             parameters => $parameters,
119             wrapper => \&wrap_param_builder,
120             package => $package_name,
121             );
122              
123             my $value = validate($parameters->{$key}, $coderef->(%_));
124             return %_, $key => $value;
125             };
126             }
127              
128             # DESCRIPTION: Localize %_ around a checkargs sub
129             # USED BY: MooseX::Params::Wizard::process
130             sub wrap_checkargs
131             {
132             my ($coderef, $package_name, $parameters) = @_;
133              
134             return sub
135             {
136             local %_ = @_;
137              
138             my $wizard = MooseX::Params::Magic::Wizard->new;
139              
140             Variable::Magic::cast(%_, $wizard,
141             parameters => $parameters,
142             wrapper => \&wrap_param_builder,
143             package => $package_name,
144             );
145              
146             $coderef->(%_);
147             };
148             }
149              
150             # DESCRIPTION: Get the parameters passed to a method, pair them with parameter definitions,
151             # build, coerce, validate and return them as a hash
152             # USED BY: MooseX::Params::Util::wrap_method
153             sub process_args
154             {
155             my ( $meta, $method, @parameters ) = @_;
156              
157             my @parameter_objects = $method->all_parameters if $method->has_parameters;
158             return unless @parameter_objects;
159              
160             if ($method->buildargs)
161             {
162             my $buildargs = $meta->get_method($method->buildargs);
163             @parameters = $buildargs->body->(@parameters);
164             }
165              
166             # separate named from positional parameters
167             my $last_index = $#parameters;
168              
169             my $last_positional_index = max
170             map { $_->index }
171             grep { $_->type eq 'positional' }
172             @parameter_objects;
173              
174             $last_positional_index = -1 unless defined $last_positional_index;
175              
176             my $last_positional_is_slurpy =
177             first { $_->index == $last_positional_index and $_->slurpy }
178             @parameter_objects;
179              
180             my $first_named_index = $last_positional_index + 1;
181              
182             my %named;
183             {
184             no warnings 'misc';
185             # disable 'Odd number of elements in hash assignment' warning
186             %named = @parameters[ $first_named_index .. $last_index ]
187             unless $last_positional_is_slurpy;
188             }
189              
190             # start processing
191             my %return_values;
192              
193             my $stash = Package::Stash->new($method->package_name);
194              
195             foreach my $param (@parameter_objects)
196             {
197             # $is_set - has a value been passed for this parameter
198             # $is_required - is the parameter required
199             # $is_lazy - should we build the value now or on first use
200             # $has_default - does the parameter have a default value or a builder
201             # $original_value - the value passed for this parameter
202             # $value - the value to be returned for this parameter, after any coercions
203              
204             my ( $is_set, $original_value );
205              
206             if ( $param->type eq 'positional' )
207             {
208             $is_set = $param->index > $last_index ? 0 : 1;
209              
210             if ($is_set)
211             {
212             if ($last_positional_is_slurpy and $param->index == $last_positional_index)
213             {
214             my @slurpy_values = @parameters[$last_positional_index .. $last_index];
215             $original_value = \@slurpy_values;
216             }
217             else
218             {
219             $original_value = $parameters[$param->index];
220             }
221             }
222             }
223             else
224             {
225             if ($param->init_arg)
226             {
227             $is_set = exists $named{$param->init_arg};
228             $original_value = $named{$param->init_arg} if $is_set;
229             }
230             else
231             {
232             $is_set = exists $named{$param->name};
233             $original_value = $named{$param->name} if $is_set;
234             }
235             }
236              
237             my $is_required = $param->required;
238             my $is_lazy = $param->lazy;
239             my $has_default = ( defined $param->default or $param->builder );
240              
241             my $value;
242              
243             # if required but not set, attempt to build the value
244             if ( !$is_set and !$is_lazy and $is_required )
245             {
246             Carp::croak ("Parameter " . $param->name . " is required") unless $has_default;
247             $value = build($param, $stash);
248             }
249             # if not required and not set, but not lazy either, check for a default
250             elsif ( !$is_set and !$is_required and !$is_lazy and $has_default )
251             {
252             $value = build($param, $stash);
253             }
254             # lazy parameters are built later
255             elsif ( !$is_set and $is_lazy)
256             {
257             next;
258             }
259             elsif ( $is_set )
260             {
261             $value = $original_value;
262             }
263              
264             $value = validate($param, $value);
265              
266             $return_values{$param->name} = $value;
267              
268             #FIXME
269             if ($param->weak_ref and !isweak($value))
270             {
271             #weaken($value);
272             #weaken($return_values{$param->name});
273             }
274             }
275              
276             if ($method->checkargs)
277             {
278             my $checkargs = $meta->get_method($method->checkargs)->body;
279             my $wrapped = wrap_checkargs($checkargs, $method->package_name, $method->parameters);
280             $wrapped->(%return_values);
281             }
282              
283             return %return_values;
284             }
285              
286             sub process_return_values
287             {
288             my ( $method, $wantarray, @values ) = @_;
289              
290             return @values unless $method->has_return_value_constraint;
291              
292             my $constraint =
293             Moose::Util::TypeConstraints::find_or_parse_type_constraint(
294             $method->returns
295             );
296              
297             if ( $constraint->is_a_type_of('Array'))
298             {
299             $constraint->assert_valid(\@values);
300             return @values if $wantarray;
301              
302             given ($method->returns_scalar)
303             {
304             when ('First') { return $values[0] }
305             when ('Last') { return $values[-1] }
306             when ('ArrayRef') { return \@values }
307             when ('Count') { return scalar @values }
308             default { return @values }
309             }
310             }
311             elsif ( $constraint->is_a_type_of('Hash') )
312             {
313             $constraint->assert_valid({@values});
314             return @values if $wantarray;
315              
316             given ($method->returns_scalar)
317             {
318             when ('HashRef') { return {@values} }
319             default { return @values }
320             }
321             }
322             else
323             {
324             $constraint->assert_valid($values[0]);
325             return $values[0];
326             }
327              
328             }
329              
330             # DESCRIPTION: Given a parameter specification and a value, validate and coerce the value
331             # USED BY: MooseX::Params::Util::process_args
332             sub validate
333             {
334             my ($param, $value) = @_;
335              
336             if ( $param->constraint )
337             {
338             # fetch type definition
339             my $constraint = find_type_constraint($param->constraint)
340             or Carp::croak("Could not find definition of type '" . $param->constraint . "'");
341              
342             # coerce
343             if ($param->coerce and $constraint->has_coercion)
344             {
345             $value = $constraint->assert_coerce($value);
346             }
347              
348             # validate
349             $constraint->assert_valid($value);
350             }
351              
352             return $value;
353             }
354              
355             sub parse_attribute
356             {
357             my $string = shift;
358             my @params;
359              
360             # join lines
361             $string =~ s/\R//g;
362              
363             if ($string =~ s/^\s*(\w+)://)
364             {
365             my $invocant = $1;
366              
367             push @params, {
368             name => $invocant,
369             init_arg => $invocant,
370             required => 1,
371             type => 'positional',
372             #TODO isa => ,
373             };
374             }
375              
376             my $csv_parser = Text::CSV_XS->new({ allow_loose_quotes => 1 });
377             $csv_parser->parse($string) or Carp::croak("Cannot parse param specs");
378              
379             my $format = qr/^
380             # TYPE AND COERCION
381             ( (?<coerce>\&)? (?<type> [\w\:\[\]]+) \s+ )?
382              
383             # LAZY_BUILD
384             (?<default>=)?
385              
386             # SLURPY
387             (?<slurpy>\*)?
388              
389             # NAME
390             (
391             ( (?<named>:) (?<init_arg>\w*) \( (?<name>\w+) \) )
392             |( (?<named>:)? (?<init_arg>(?<name>\w+)) )
393             )
394              
395             # REQUIRED OR OPTIONAL
396             (?<required>[!?])? \s*
397              
398             # DEFAULT VALUE
399             (
400             (?<default>=)\s*(
401             (?<number> \d+ )
402             | ( (?<code>\w+) (\(\))? )
403             | ( (?<delimiter>["']) (?<string>.*) \g{delimiter} )
404             )?
405             )?
406              
407             $/x;
408              
409              
410             foreach my $param ($csv_parser->fields)
411             {
412             $param =~ s/^\s*//;
413             $param =~ s/\s*$//;
414              
415             if ($param =~ $format)
416             {
417             my %options =
418             (
419             name => $+{name},
420             init_arg => $+{init_arg} eq '' ? undef : $+{init_arg},
421             required => ( defined $+{required} and $+{required} eq '?' ) ? 0 : 1,
422             type => $+{named} ? 'named' : 'positional',
423             slurpy => $+{slurpy} ? 1 : 0,
424             isa => defined $+{type} ? $+{type} : undef,
425             coerce => $+{coerce} ? 1 : 0,
426             default => defined $+{number} ? $+{number} : $+{string},
427             builder => ( defined $+{default} and not defined $+{number} and not defined $+{string} )
428             ? ( defined $+{code} ? $+{code} : "_build_param_$+{name}" ) : undef,
429             lazy => ( defined $+{default} and not defined $+{number} and not defined $+{string} ) ? 1 : 0,
430             );
431              
432             push @params, \%options;
433             }
434             else
435             {
436             Carp::croak "Error parsing parameter specification '$param'";
437             }
438             }
439              
440             return @params;
441             }
442              
443             # TODO: Merge with process_attribute
444             # DESCRIPTION: Given a parameter specification attribute as a string,
445             # inflate into a list of MooseX::Param::Meta::Parameter objects
446             # USED BY: MooseX::Params::Args
447             sub inflate_parameters
448             {
449             my ($package, $data) = @_;
450              
451             my @parameters = parse_attribute($data);
452             my $position = 0;
453             my @inflated_parameters;
454              
455             foreach my $param (@parameters)
456             {
457             my $parameter_object = MooseX::Params::Meta::Parameter->new(
458             index => $position,
459             package => $package,
460             %$param,
461             );
462              
463             push @inflated_parameters, $parameter_object;
464             $position++;
465             }
466              
467             my %inflated_parameters = map { $_->name => $_ } @inflated_parameters;
468              
469             return \%inflated_parameters;
470             }
471              
472             sub prepare_attribute_handler
473             {
474             my $handler = Moose::Meta::Class->initialize('MooseX::Params')
475             ->get_method(shift)
476             ->body;
477              
478             return sub
479             {
480             my ($symbol, $attr, $data, $caller) = @_;
481              
482             my ($package, $filename, $line, $subroutine, $hasargs, $wantarray,
483             $evaltext, $is_require, $hints, $bitmask, $hinthash) = @$caller;
484              
485             when_sub_bodied ( $symbol, sub
486             {
487             my $coderef = shift;
488             my $name = sub_name($coderef);
489              
490             croak "MooseX::Params currently does not support anonymous subroutines"
491             if $name eq "__ANON__";
492              
493             my $metaclass = Moose::Meta::Class->initialize($package);
494             my $method = $metaclass->get_method($name);
495              
496             unless ( $method->isa('MooseX::Params::Meta::Method') )
497             {
498             my $wrapped_coderef = MooseX::Params::Util::wrap_method($package, $name, $coderef);
499              
500             my $wrapped_method = MooseX::Params::Meta::Method->wrap(
501             $wrapped_coderef,
502             name => $name,
503             package_name => $package,
504             );
505              
506             $metaclass->add_method($name, $wrapped_method);
507              
508             $method = $wrapped_method;
509             }
510              
511             return $handler->($method, $data);
512             });
513             };
514             }
515             1;
516              
517             __END__
518             =pod
519              
520             =for :stopwords Peter Shangov TODO invocant isa metaroles metarole multimethods sourcecode
521             backwards buildargs checkargs slurpy preprocess
522              
523             =head1 NAME
524              
525             MooseX::Params::Util - Parameter processing utilities
526              
527             =head1 VERSION
528              
529             version 0.010
530              
531             =head1 AUTHOR
532              
533             Peter Shangov <pshangov@yahoo.com>
534              
535             =head1 COPYRIGHT AND LICENSE
536              
537             This software is copyright (c) 2012 by Peter Shangov.
538              
539             This is free software; you can redistribute it and/or modify it under
540             the same terms as the Perl 5 programming language system itself.
541              
542             =cut
543