File Coverage

blib/lib/MooX/Params/CompiledValidators.pm
Criterion Covered Total %
statement 75 76 98.6
branch 17 22 77.2
condition 4 6 66.6
subroutine 10 11 90.9
pod 5 5 100.0
total 111 120 92.5


line stmt bran cond sub pod time code
1             use Moo::Role;
2 2     2   408848  
  2         10  
  2         12  
3             our $VERSION = '0.04_03';
4              
5             use Hash::Util 'lock_hash';
6 2     2   1453 use Params::ValidationCompiler 'validation_for';
  2         4541  
  2         11  
7 2     2   858 use Types::Standard qw( StrMatch Enum HashRef ArrayRef );
  2         35635  
  2         88  
8 2     2   13  
  2         4  
  2         12  
9             requires 'ValidationTemplates';
10              
11             # local cache for compiled-validators (including our own)
12             my $_validators = {
13             _parameter => validation_for(
14             params => [
15             {
16             type => StrMatch[ qr{^ \w+ $}x ],
17             optional => 0
18             },
19             {
20             type => Enum [qw( 0 1 )],
21             optional => 1,
22             default => 1
23             },
24             {
25             type => HashRef,
26             optional => 1,
27             default => sub { {} }
28             },
29             ],
30             name => 'parameter',
31             ),
32             _validate_positional_parameters => validation_for(
33             params => [
34             { type => ArrayRef, optional => 0 },
35             { type => ArrayRef, optional => 0 },
36             ],
37             name => 'validate_positional_parameters',
38             ),
39             _validate_parameters => validation_for(
40             params => [
41             { type => HashRef, optional => 0 },
42             { type => HashRef, optional => 0 },
43             ],
44             name => 'validate_parameters',
45             ),
46             };
47              
48             =head1 NAME
49              
50             MooX::Params::CompiledValidators - A L<Moo::Role> for using L<Params::ValidationCompiler>.
51              
52             =head1 SYNOPSIS
53              
54             use Moo;
55             use Types::Standard qw( Str );
56             with 'MooX::Params::CompiledValidators';
57              
58             sub any_sub {
59             my $self = shift;
60             my $arguments = $self->validate_parameters(
61             {
62             $self->parameter(customer_id => $self->Required),
63             },
64             { @_ }
65             );
66             ...
67             }
68              
69             # Implement a local version of the ValidationTemplates
70             sub ValidationTemplates {
71             return {
72             customer_id => { type => Str },
73             };
74             }
75              
76             =head1 DESCRIPTION
77              
78             This role uses L<Params::ValidationCompiler> to create parameter validators on a
79             per method basis that can be used in the methods of your L<Moo> or L<Moose>
80             projects.
81              
82             The objective is to create a single set of validation criteria - ideally in a
83             seperate role that can be used along side of this role - that can be used to
84             consistently validate parameters throughout your application.
85              
86             The validators created by L<Params::ValidationCompiler> are cached after they
87             are created the first time, so they will only be created once.
88              
89             =head2 Validation-Templates
90              
91             A validation-template is a structure (HashRef) that
92             C<Params::ValidationCompiler::validation_for()> uses to validate the parameter
93             and basically contains three keys:
94              
95             =over
96              
97             =item B<type>
98              
99             C<Params::ValidationCompiler> supports a number of type systems, see their documentation.
100              
101             =item B<default>
102              
103             Define a default value for this parameter, either a simple scalar or a code-ref
104             that returns a more complex value.
105              
106             =item B<optional>
107              
108             By default false, required parameters are preferred by C<Params::ValidationCompiler>
109              
110             =back
111              
112             =head2 The I<required> C<ValidationTemplates()> method
113              
114             The objective of this module (Role) is to standardise parameter validation by
115             defining a single set of Validation Templates for all the parameters in a project.
116             This is why the C<MooX::Params::CompiledValidators> role B<< C<requires> >> a
117             C<ValidationTemplates> method in its consuming class. The C<ValidationTemplates>
118             method is needed for the C<parameter()> method that is also supplied by this
119             role.
120              
121             This could be as simple as:
122              
123             package MyTemplates;
124             use Moo::Role;
125              
126             use Types::Standard qw(Str);
127             sub ValidationTemplates {
128             return {
129             customer_id => { type => Str },
130             username => { type => Str },
131             };
132             }
133              
134             =head2 The C<Required()> method
135              
136             C<validation_for()> uses the attribute C<optional> so this returns C<0>
137              
138             =head2 The C<Optional> method
139              
140             C<validation_for()> uses the attribute C<optional> so this returns C<1>
141              
142             =cut
143              
144              
145 8     8 1 10160 =head2 The C<validate_parameters()> method
146 0     0 1 0  
147             Returns a (locked) hashref with validated parameters or C<die()>s trying...
148              
149             Given:
150              
151             use Moo;
152             with 'MooX::Params::CompiledValidators';
153              
154             sub show_user_info {
155             my $self = shift;
156             my $args = $self->validate_parameters(
157             {
158             customer_id => { type => Str, optional => 0 },
159             username => { type => Str, optional => 0 },
160             },
161             { @_ }
162             );
163             return {
164             customer => $args->{customer_id},
165             username => $args->{username},
166             };
167             }
168              
169             One would call this as:
170              
171             my $user_info = $instance->show_user_info(
172             customer_id => 'Blah42',
173             username => 'blah42',
174             );
175              
176             =head3 Parameters
177              
178             Positional:
179              
180             =over
181              
182             =item 1. C<$validation_templates>
183              
184             A hashref with the parameter-names as keys and the L</Validation-Templates> as values.
185              
186              
187             =item 2. C<$values>
188              
189             A hashref with the actual parameter-name/value pairs that need to be validated.
190              
191             =back
192              
193             =head3 Responses
194              
195             =over
196              
197             =item B<Success> (scalar context, recommended)
198              
199             A locked hashref.
200              
201             =item B<Success> (list context, only if you need to manipulate the result)
202              
203             A list that can be coerced into a hash.
204              
205             =item B<Error>
206              
207             Anything L<Params::ValidationCompiler> will throw for invalid values.
208              
209             =back
210              
211             =cut
212              
213             my $self = shift;
214             my $validate_us = $_validators->{_validate_parameters};
215             my ($templates, $values) = $validate_us->(@_);
216              
217 4     4 1 8 # remember where to store values in (scoped) variables
218 4         12 # should we die() if that value is not a SCALAR-Ref?
219 4         83 my %store_params = map {
220             (exists($templates->{$_}{store}) and ref($templates->{$_}{store}) eq 'SCALAR')
221             ? ($_ => delete($templates->{$_}{store}))
222             : ()
223             } keys %$templates;
224 4         91  
225 4 100 66     30 my $called_from = (caller(1))[3];
226             my $this_validator = "validation_for>$called_from";
227              
228             if (not exists($_validators->{ $this_validator })) {
229 4         47 local $Type::Tiny::AvoidCallbacks;
230 4         16 $_validators->{$this_validator} = validation_for(
231             params => $templates,
232 4 100       15 name => $this_validator,
233 3         4 );
234 3         15 }
235             my $validator = $_validators->{ $this_validator };
236              
237             my %validated = eval { $validator->(%$values) };
238             if (my $error = $@) {
239 4         3132 _sniff_it($error);
240             }
241 4         10  
  4         77  
242 4 100       10611 # store values in the their (scoped) variables
243 2         20 for my $to_store (keys %store_params) {
244             ${ $store_params{$to_store} } = $validated{$to_store};
245             }
246              
247 2         6 return wantarray ? (%validated) : lock_hash(%validated);
248 1         2 }
  1         3  
249              
250             =head2 The C<validate_positional_parameters()> method
251 2 50       9  
252             Like C<< $instance->validate_parameters() >>, but now the pairs of I<name>,
253             I<validation-template> are passed in an arrayref, that is split into lists of
254             the names and templates. The parameters passed -as an array- will be validated
255             against the templates-list, and the validated results are combined back into
256             a hash with name/value pairs. This makes the programming interface almost the
257             same for both named-parameters and positional-parameters.
258              
259             Returns a (locked) hashref with validated parameters or C<die()>s trying...
260              
261             Given:
262              
263             use Moo;
264             with 'MooX::Params::CompiledValidators';
265              
266             sub show_user_info {
267             my $self = shift;
268             my $args = $self->validate_positional_parameters(
269             [
270             customer_id => { type => Str, optional => 0 },
271             username => { type => Str, optional => 0 },
272             ],
273             \@_
274             );
275             return {
276             customer => $args->{customer_id},
277             username => $args->{username},
278             };
279             }
280              
281             One would call this as:
282              
283             my $user_info = $instance->show_user_info('Blah42', 'blah42');
284              
285             =head3 Parameters
286              
287             Positional:
288              
289             =over
290              
291             =item 1. C<$validation_templates>
292              
293             A arrayref with pairs of parameter-names and L<validation templates>.
294              
295              
296             =item 2. C<$values>
297              
298             A arrayref with the actual values that need to be validated.
299              
300             =back
301              
302             =head3 Responses
303              
304             =over
305              
306             =item B<Success> (list context)
307              
308             A list that can be coerced into a hash.
309              
310             =item B<Success> (scalar context)
311              
312             A locked hashref.
313              
314             =item B<Error>
315              
316             Anything L<Params::ValidationCompiler> will throw for invalid values.
317              
318             =back
319              
320             =cut
321              
322             my $self = shift;
323             my $validate_us = $_validators->{_validate_positional_parameters};
324             my ($templates, $data) = $validate_us->(@_);
325              
326             my ($positional_templates, $positional_names);
327 4     4 1 9 my @names_and_templates = @$templates;
328 4         19 while (@names_and_templates) {
329 4         83 my ($pname, $ptemplate) = splice(@names_and_templates, 0, 2);
330             push @$positional_templates, $ptemplate;
331 4         69 push @$positional_names, $pname;
332 4         11 }
333 4         15  
334 4         16 # remember where to store values in (scoped) variables
335 4         12 # should we die() if that value is not a SCALAR-Ref?
336 4         15 my %store_params;
337             for my $i (0 .. $#{$positional_templates}) {
338             if ( exists($positional_templates->[$i]{store})
339             and ref($positional_templates->[$i]{store}) eq 'SCALAR')
340             {
341 4         6 $store_params{ $positional_names->[$i] } = delete(
342 4         8 $positional_templates->[$i]{store}
  4         17  
343 4 100 66     21 );
344             }
345             }
346              
347             my $called_from = (caller(1))[3];
348 1         4 my $this_validator = "validation_for>$called_from";
349              
350             if (not exists($_validators->{ $this_validator })) {
351             local $Type::Tiny::AvoidCallbacks;
352 4         43 $_validators->{$this_validator} = validation_for(
353 4         17 params => $positional_templates,
354             name => $this_validator,
355 4 100       19 );
356 3         5 }
357 3         14 my $validator = $_validators->{ $this_validator };
358              
359             my @validated_values = eval { $validator->(@$data) };
360             if (my $error = $@) {
361             _sniff_it($error);
362 4         2612 }
363              
364 4         9 my %validated;
  4         66  
365 4 100       481 @validated{ @$positional_names } = @validated_values if @$positional_names;
366 2         16  
367             # store values in the their (scoped) variables
368             for my $to_store (keys %store_params) {
369 2         4 ${ $store_params{$to_store} } = $validated{$to_store};
370 2 50       9 }
371              
372             return wantarray ? (%validated) : lock_hash(%validated);
373 2         6 }
374 1         3  
  1         3  
375             =head2 The C<parameter()> method
376              
377 2 50       8 Returns a C<parameter_name>, C<validation_template> pair that can be used in the
378             C<parameters> argument hashref for
379             C<Params::ValidationCompiler::validadion_for()>
380              
381             =head3 Parameters
382              
383             Positional:
384              
385             =over
386              
387             =item 1. C<$name> (I<Required>)
388              
389             The name of this parameter (it must be a kind of identifier: C<< m{^\w+$} >>)
390              
391             =item 2. C<$required> (I<Optional>)
392              
393             One of C<< $class->Required >> or C<< $class->Optional >> but will default to
394             C<< $class->Required >>.
395              
396             =item 3. C<$extra> (I<Optional>)
397              
398             This optional HashRef can contain the fields supported by the C<params>
399             parameter of C<validation_for()>, even overriding the ones set by the C<<
400             $class->ValidationTemplates() >> for this C<$name> - although C<optional> is set
401             by the previous parameter in this sub.
402              
403             This parameter is mostly used for the extra feature to pass a lexically scoped
404             variable via L<store|/"the extra store attribute">.
405              
406             =back
407              
408             =head3 Responses
409              
410             =over
411              
412             =item B<Success>
413              
414             A list of C<$parameter_name> and C<$validation_template>.
415              
416              
417             (this_parm => { optional => 0, type => Str, store => \my $this_param })
418              
419              
420             =back
421              
422             =head3 NOTE on "Unknown" parameters
423              
424             Whenever C<< $self->parameter() >> is called with a parameter-name that doesn't
425             resolve to a template in the C<ValidationTemplates()> hash, a default "empty"
426             template is produced. This will mean that there will be no validation on that
427             value, although one could pass one as the third parameter:
428              
429             use Moo;
430             use Types::Standard qw( StrMatch );
431             with qw(
432             MyTemplates
433             MooX::Params::CompiledValidators
434             );
435              
436             sub show_user_info {
437             my $self = shift;
438             my $args = $self->validate_parameters(
439             {
440             $self->parameter(customer_id => $self->Required),
441             $self->parameter(
442             email => $self->Required,
443             { type => StrMatch[ qr{^ [-.\w]+ @ [-.\w]+ $}x ] },
444             ),
445             },
446             { @_ }
447             );
448             return {
449             customer => $args->{customer_id},
450             email => $args->{email},
451             };
452             }
453              
454             =cut
455              
456             my $self = shift;
457             my $validate_us = $_validators->{_parameter};
458             my ($name, $optional, $extra) = $validate_us->(@_);
459              
460             my $template = exists($self->ValidationTemplates->{$name})
461             ? $self->ValidationTemplates->{$name}
462 8     8 1 16 : { };
463 8         22  
464 8         222 my $final_template = {
465             %$template,
466             ($extra ? %$extra : ()),
467 8 50       149 optional => $optional,
468             };
469             return ($name => $final_template);
470 8 50       11060 }
471              
472             =begin private
473              
474             =head2 _sniff_it($message)
475 8         499  
476             Tailor made exception handler.
477              
478             =end private
479              
480             =cut
481              
482             my ($message) = @_;
483             my ($filename, $line) = (caller(1))[1, 2];
484             my $subroutine = (caller(2))[3];
485              
486             die sprintf('Error in %s (%s:%u): %s', $subroutine, $filename, $line, $message);
487             }
488              
489 4     4   12 use namespace::autoclean;
490 4         28 1;
491 4         37  
492             =head2 The extra C<store> attribute
493 4         27  
494             Both C<validate_parameters()> and C<validate_positional_parameters> support the
495             extra C<store> attribute in a validation template that should be a
496 2     2   3098 scalar-reference where we store the value after successful validation.
  2         4  
  2         21  
497              
498             One can pick and mix with validation templates:
499              
500             use Moo;
501             use Types::Standard qw( StrMatch );
502             with qw(
503             MyTemplates
504             MooX::Params::CompiledValidators
505             );
506              
507             sub show_user_info {
508             my $self = shift;
509             $self->validate_parameters(
510             {
511             $self->parameter(customer_id => $self->Required, {store => \my $customer_id),
512             email => {
513             type => StrMatch[ qr{^ [-.\w]+ @ [-.\w]+ $}x ],
514             optional => 0,
515             store => \my $email
516             },
517             },
518             { @_ }
519             );
520             return {
521             customer => $customer_id,
522             email => $email,
523             };
524             }
525              
526             One would call this as:
527              
528             my $user_info = $instance->show_user_info(
529             customer_id => 'Blah42',
530             email => 'blah42@some.tld',
531             );
532              
533             One could argue that using (lexical) variables -instead of addressing keys of a
534             locked hash- triggers the error caused by a typo at I<compile-time> rather than
535             at I<run-time>.
536              
537             B<NOTE>: In order to keep the scope of the variable, where the value is stored,
538             limited, the C<store> attribute should only be used from the per method override
539             option C<extra> for C<< $self->parameter() >>.
540              
541             =head1 AUTHOR
542              
543             E<copy> MMXXI - Abe Timmerman <abeltje@cpan.org>
544              
545             =head1 LICENSE
546              
547             This library is free software; you can redistribute it and/or modify
548             it under the same terms as Perl itself.
549              
550             This program is distributed in the hope that it will be useful,
551             but WITHOUT ANY WARRANTY; without even the implied warranty of
552             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
553              
554             =cut