Accept hash/array ref for params to validate
[gitmo/MooseX-Params-Validate.git] / lib / MooseX / Params / Validate.pm
1 package MooseX::Params::Validate;
2
3 use strict;
4 use warnings;
5
6 use Carp 'confess';
7 use Devel::Caller 'caller_cv';
8 use Scalar::Util 'blessed', 'refaddr', 'reftype';
9
10 use Moose::Util::TypeConstraints qw( find_type_constraint class_type role_type );
11 use Params::Validate             ();
12 use Sub::Exporter -setup => {
13     exports => [
14         qw( validated_hash validated_list pos_validated_list validate validatep )
15     ],
16     groups => {
17         default => [qw( validated_hash validated_list pos_validated_list )],
18         deprecated => [qw( validate validatep )],
19     },
20 };
21
22 my %CACHED_SPECS;
23
24 sub validated_hash {
25     my ( $args, %spec ) = @_;
26
27     my $cache_key = _cache_key( \%spec );
28
29     my $allow_extra = delete $spec{MX_PARAMS_VALIDATE_ALLOW_EXTRA};
30
31     if ( exists $CACHED_SPECS{$cache_key} ) {
32         ( ref $CACHED_SPECS{$cache_key} eq 'HASH' )
33             || confess
34             "I was expecting a HASH-ref in the cached $cache_key parameter"
35             . " spec, you are doing something funky, stop it!";
36         %spec = %{ $CACHED_SPECS{$cache_key} };
37     }
38     else {
39         my $should_cache = delete $spec{MX_PARAMS_VALIDATE_NO_CACHE} ? 0 : 1;
40
41         $spec{$_} = _convert_to_param_validate_spec( $spec{$_} )
42             foreach keys %spec;
43
44         $CACHED_SPECS{$cache_key} = \%spec
45             if $should_cache;
46     }
47
48     my $instance;
49     $instance = shift @$args if blessed $args->[0];
50
51     my %args 
52         = @$args == 1
53         && ref $args->[0]
54         && reftype( $args->[0] ) eq 'HASH' ? %{ $args->[0] } : @$args;
55
56     $args{$_} = $spec{$_}{constraint}->coerce( $args{$_} )
57         for grep { $spec{$_}{coerce} && exists $args{$_} } keys %spec;
58
59     %args = Params::Validate::validate_with(
60         params      => \%args,
61         spec        => \%spec,
62         allow_extra => $allow_extra,
63         called      => _caller_name(),
64     );
65
66     return ( ( defined $instance ? $instance : () ), %args );
67 }
68
69 *validate = \&validated_hash;
70
71 sub validated_list {
72     my ( $args, @spec ) = @_;
73
74     my %spec = @spec;
75
76     my $cache_key = _cache_key( \%spec );
77
78     my $allow_extra = delete $spec{MX_PARAMS_VALIDATE_ALLOW_EXTRA};
79
80     my @ordered_spec;
81     if ( exists $CACHED_SPECS{$cache_key} ) {
82         ( ref $CACHED_SPECS{$cache_key} eq 'ARRAY' )
83             || confess
84             "I was expecting a ARRAY-ref in the cached $cache_key parameter"
85             . " spec, you are doing something funky, stop it!";
86         %spec         = %{ $CACHED_SPECS{$cache_key}->[0] };
87         @ordered_spec = @{ $CACHED_SPECS{$cache_key}->[1] };
88     }
89     else {
90         my $should_cache = delete $spec{MX_PARAMS_VALIDATE_NO_CACHE} ? 0 : 1;
91
92         @ordered_spec = grep { exists $spec{$_} } @spec;
93
94         $spec{$_} = _convert_to_param_validate_spec( $spec{$_} )
95             foreach keys %spec;
96
97         $CACHED_SPECS{$cache_key} = [ \%spec, \@ordered_spec ]
98             if $should_cache;
99     }
100
101     my $instance;
102     $instance = shift @$args if blessed $args->[0];
103
104     my %args 
105         = @$args == 1
106         && ref $args->[0]
107         && reftype( $args->[0] ) eq 'HASH' ? %{ $args->[0] } : @$args;
108
109     $args{$_} = $spec{$_}{constraint}->coerce( $args{$_} )
110         for grep { $spec{$_}{coerce} && exists $args{$_} } keys %spec;
111
112     %args = Params::Validate::validate_with(
113         params      => \%args,
114         spec        => \%spec,
115         allow_extra => $allow_extra,
116         called      => _caller_name(),
117     );
118
119     return (
120         ( defined $instance ? $instance : () ),
121         @args{@ordered_spec}
122     );
123 }
124
125 *validatep = \&validated_list;
126
127 sub pos_validated_list {
128     my $args = shift;
129
130     my @spec;
131     push @spec, shift while ref $_[0];
132
133     my %extra = @_;
134
135     my $cache_key = _cache_key( \%extra );
136
137     my $allow_extra = delete $extra{MX_PARAMS_VALIDATE_ALLOW_EXTRA};
138
139     my @pv_spec;
140     if ( exists $CACHED_SPECS{$cache_key} ) {
141         ( ref $CACHED_SPECS{$cache_key} eq 'ARRAY' )
142             || confess
143             "I was expecting an ARRAY-ref in the cached $cache_key parameter"
144             . " spec, you are doing something funky, stop it!";
145         @pv_spec = @{ $CACHED_SPECS{$cache_key} };
146     }
147     else {
148         my $should_cache = exists $extra{MX_PARAMS_VALIDATE_NO_CACHE} ? 0 : 1;
149
150         # prepare the parameters ...
151         @pv_spec = map { _convert_to_param_validate_spec($_) } @spec;
152
153         $CACHED_SPECS{$cache_key} = \@pv_spec
154             if $should_cache;
155     }
156
157     my @args 
158         = @$args == 1
159         && ref $args->[0]
160         && reftype( $args->[0] ) eq 'ARRAY' ? @{ $args->[0] } : @$args;
161
162     $args[$_] = $pv_spec[$_]{constraint}->coerce( $args[$_] )
163         for grep { $pv_spec[$_] && $pv_spec[$_]{coerce} } 0 .. $#args;
164
165     @args = Params::Validate::validate_with(
166         params      => \@args,
167         spec        => \@pv_spec,
168         allow_extra => $allow_extra,
169         called      => _caller_name(),
170     );
171
172     return @args;
173 }
174
175 sub _cache_key {
176     my $spec = shift;
177
178     if ( exists $spec->{MX_PARAMS_VALIDATE_CACHE_KEY} ) {
179         return delete $spec->{MX_PARAMS_VALIDATE_CACHE_KEY};
180     }
181     else {
182         return refaddr( caller_cv(2) );
183     }
184 }
185
186 sub _convert_to_param_validate_spec {
187     my ($spec) = @_;
188     my %pv_spec;
189
190     $pv_spec{optional} = $spec->{optional}
191         if exists $spec->{optional};
192
193     $pv_spec{default} = $spec->{default}
194         if exists $spec->{default};
195
196     $pv_spec{coerce} = $spec->{coerce}
197         if exists $spec->{coerce};
198
199     my $constraint;
200     if ( defined $spec->{isa} ) {
201         $constraint
202              = _is_tc( $spec->{isa} )
203             || Moose::Util::TypeConstraints::find_or_parse_type_constraint(
204             $spec->{isa} )
205             || class_type( $spec->{isa} );
206     }
207     elsif ( defined $spec->{does} ) {
208         $constraint
209             = _is_tc( $spec->{isa} )
210             || find_type_constraint( $spec->{does} )
211             || role_type( $spec->{does} );
212     }
213
214     $pv_spec{callbacks} = $spec->{callbacks}
215         if exists $spec->{callbacks};
216
217     if ($constraint) {
218         $pv_spec{constraint} = $constraint;
219
220         $pv_spec{callbacks}
221             { 'checking type constraint for ' . $constraint->name }
222             = sub { $constraint->check( $_[0] ) };
223     }
224
225     delete $pv_spec{coerce}
226         unless $pv_spec{constraint} && $pv_spec{constraint}->has_coercion;
227
228     return \%pv_spec;
229 }
230
231 sub _is_tc {
232     my $maybe_tc = shift;
233
234     return $maybe_tc
235         if defined $maybe_tc
236             && blessed $maybe_tc
237             && $maybe_tc->isa('Moose::Meta::TypeConstraint');
238 }
239
240 sub _caller_name {
241     my $depth = shift || 0;
242
243     return ( caller( 2 + $depth ) )[3];
244 }
245
246 1;
247
248 # ABSTRACT: an extension of Params::Validate using Moose's types
249
250 __END__
251
252 =pod
253
254 =head1 SYNOPSIS
255
256   package Foo;
257   use Moose;
258   use MooseX::Params::Validate;
259
260   sub foo {
261       my ( $self, %params ) = validated_hash(
262           \@_,
263           bar => { isa => 'Str', default => 'Moose' },
264       );
265       return "Hooray for $params{bar}!";
266   }
267
268   sub bar {
269       my $self = shift;
270       my ( $foo, $baz, $gorch ) = validated_list(
271           \@_,
272           foo   => { isa => 'Foo' },
273           baz   => { isa => 'ArrayRef | HashRef', optional => 1 },
274           gorch => { isa => 'ArrayRef[Int]', optional => 1 }
275       );
276       [ $foo, $baz, $gorch ];
277   }
278
279 =head1 DESCRIPTION
280
281 This module fills a gap in Moose by adding method parameter validation
282 to Moose. This is just one of many developing options, it should not
283 be considered the "official" one by any means though.
284
285 You might also want to explore C<MooseX::Method::Signatures> and
286 C<MooseX::Declare>.
287
288 =head1 CAVEATS
289
290 It is not possible to introspect the method parameter specs; they are
291 created as needed when the method is called and cached for subsequent
292 calls.
293
294 =head1 EXPORTS
295
296 =over 4
297
298 =item B<validated_hash( \@_, %parameter_spec )>
299
300 This behaves similarly to the standard Params::Validate C<validate>
301 function and returns the captured values in a HASH. The one exception
302 is where if it spots an instance in the C<@_>, then it will handle
303 it appropriately (unlike Params::Validate which forces you to shift
304 you C<$self> first).
305
306 The C<%parameter_spec> accepts the following options:
307
308 =over 4
309
310 =item I<isa>
311
312 The C<isa> option can be either; class name, Moose type constraint
313 name or an anon Moose type constraint.
314
315 =item I<does>
316
317 The C<does> option can be either; role name or an anon Moose type
318 constraint.
319
320 =item I<default>
321
322 This is the default value to be used if the value is not supplied.
323
324 =item I<optional>
325
326 As with Params::Validate, all options are considered required unless
327 otherwise specified. This option is passed directly to
328 Params::Validate.
329
330 =item I<coerce>
331
332 If this is true and the parameter has a type constraint which has
333 coercions, then the coercion will be called for this parameter. If the
334 type does have coercions, then this parameter is ignored.
335
336 =back
337
338 This function is also available under its old name, C<validate>.
339
340 =item B<validated_list( \@_, %parameter_spec )>
341
342 The C<%parameter_spec> accepts the same options as above, but returns
343 the parameters as positional values instead of a HASH. This is best
344 explained by example:
345
346   sub foo {
347       my ( $self, $foo, $bar ) = validated_list(
348           \@_,
349           foo => { isa => 'Foo' },
350           bar => { isa => 'Bar' },
351       );
352       $foo->baz($bar);
353   }
354
355 We capture the order in which you defined the parameters and then
356 return them as a list in the same order. If a param is marked optional
357 and not included, then it will be set to C<undef>.
358
359 Like C<validated_hash>, if it spots an object instance as the first
360 parameter of C<@_>, it will handle it appropriately, returning it as
361 the first argument.
362
363 This function is also available under its old name, C<validatep>.
364
365 =item B<pos_validated_list( \@_, $spec, $spec, ... )>
366
367 This function validates a list of positional parameters. Each C<$spec>
368 should validate one of the parameters in the list:
369
370   sub foo {
371       my $self = shift;
372       my ( $foo, $bar ) = pos_validated_list(
373           \@_,
374           { isa => 'Foo' },
375           { isa => 'Bar' },
376       );
377
378       ...
379   }
380
381 Unlike the other functions, this function I<cannot> find C<$self> in
382 the argument list. Make sure to shift it off yourself before doing
383 validation.
384
385 If a parameter is marked as optional and is not present, it will
386 simply not be returned.
387
388 If you want to pass in any of the cache control parameters described
389 below, simply pass them after the list of parameter validation specs:
390
391   sub foo {
392       my $self = shift;
393       my ( $foo, $bar ) = pos_validated_list(
394           \@_,
395           { isa => 'Foo' },
396           { isa => 'Bar' },
397           MX_PARAMS_VALIDATE_NO_CACHE => 1,
398       );
399
400       ...
401   }
402
403 =back
404
405 =head1 ALLOWING EXTRA PARAMETERS
406
407 By default, any parameters not mentioned in the parameter spec cause this
408 module to throw an error. However, you can have have this module simply ignore
409 them by setting C<MX_PARAMS_VALIDATE_ALLOW_EXTRA> to a true value when calling
410 a validation subroutine.
411
412 When calling C<validated_hash> or C<pos_validated_list> the extra parameters
413 are simply returned in the hash or list as appropriate. However, when you call
414 C<validated_list> the extra parameters will not be returned at all. You can
415 get them by looking at the original value of C<@_>.
416
417 =head1 EXPORTS
418
419 By default, this module exports the C<validated_hash>,
420 C<validated_list>, and C<pos_validated_list>.
421
422 If you would prefer to import the now deprecated functions C<validate>
423 and C<validatep> instead, you can use the C<:deprecated> tag to import
424 them.
425
426 =head1 IMPORTANT NOTE ON CACHING
427
428 When a validation subroutine is called the first time, the parameter spec is
429 prepared and cached to avoid unnecessary regeneration. It uses the fully
430 qualified name of the subroutine (package + subname) as the cache key.  In
431 99.999% of the use cases for this module, that will be the right thing to do.
432
433 However, I have (ab)used this module occasionally to handle dynamic
434 sets of parameters. In this special use case you can do a couple
435 things to better control the caching behavior.
436
437 =over 4
438
439 =item *
440
441 Passing in the C<MX_PARAMS_VALIDATE_NO_CACHE> flag in the parameter
442 spec this will prevent the parameter spec from being cached.
443
444   sub foo {
445       my ( $self, %params ) = validated_hash(
446           \@_,
447           foo                         => { isa => 'Foo' },
448           MX_PARAMS_VALIDATE_NO_CACHE => 1,
449       );
450
451   }
452
453 =item *
454
455 Passing in C<MX_PARAMS_VALIDATE_CACHE_KEY> with a value to be used as
456 the cache key will bypass the normal cache key generation.
457
458   sub foo {
459       my ( $self, %params ) = validated_hash(
460           \@_,
461           foo                          => { isa => 'Foo' },
462           MX_PARAMS_VALIDATE_CACHE_KEY => 'foo-42',
463       );
464
465   }
466
467 =back
468
469 =head1 MAINTAINER
470
471 Dave Rolsky E<lt>autarch@urth.orgE<gt>
472
473 =head1 BUGS
474
475 Please submit bugs to the CPAN RT system at
476 http://rt.cpan.org/NoAuth/ReportBug.html?Queue=moosex-params-validate or via
477 email at bug-moosex-params-validate@rt.cpan.org.
478
479 =cut