File Coverage

blib/lib/MooX/Params/CompiledValidators.pm
Criterion Covered Total %
statement 73 74 98.6
branch 17 22 77.2
condition 4 6 66.6
subroutine 10 11 90.9
pod 5 5 100.0
total 109 118 92.3


line stmt bran cond sub pod time code
1             use Moo::Role;
2 2     2   408098  
  2         10  
  2         13  
3             our $VERSION = '0.05';
4              
5             use Hash::Util 'lock_hash';
6 2     2   1536 use Params::ValidationCompiler 'validation_for';
  2         4577  
  2         11  
7 2     2   881 use Types::Standard qw( StrMatch Enum HashRef ArrayRef );
  2         37207  
  2         91  
8 2     2   12  
  2         6  
  2         11  
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 8608 =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         8 # 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         77  
225 4 100 66     27 my $called_from = (caller(1))[3];
226             my $this_validator = "validation_for>$called_from";
227              
228             if (not exists($_validators->{ $this_validator })) {
229 4         32 $_validators->{$this_validator} = validation_for(
230 4         15 params => $templates,
231             name => $this_validator,
232 4 100       13 );
233 3         12 }
234             my $validator = $_validators->{ $this_validator };
235              
236             my %validated = eval { $validator->(%$values) };
237             if (my $error = $@) {
238 4         3103 _sniff_it($error);
239             }
240 4         15  
  4         72  
241 4 100       8815 # store values in the their (scoped) variables
242 2         13 for my $to_store (keys %store_params) {
243             ${ $store_params{$to_store} } = $validated{$to_store};
244             }
245              
246 2         6 return wantarray ? (%validated) : lock_hash(%validated);
247 1         14 }
  1         5  
248              
249             =head2 The C<validate_positional_parameters()> method
250 2 50       9  
251             Like C<< $instance->validate_parameters() >>, but now the pairs of I<name>,
252             I<validation-template> are passed in an arrayref, that is split into lists of
253             the names and templates. The parameters passed -as an array- will be validated
254             against the templates-list, and the validated results are combined back into
255             a hash with name/value pairs. This makes the programming interface almost the
256             same for both named-parameters and positional-parameters.
257              
258             Returns a (locked) hashref with validated parameters or C<die()>s trying...
259              
260             Given:
261              
262             use Moo;
263             with 'MooX::Params::CompiledValidators';
264              
265             sub show_user_info {
266             my $self = shift;
267             my $args = $self->validate_positional_parameters(
268             [
269             customer_id => { type => Str, optional => 0 },
270             username => { type => Str, optional => 0 },
271             ],
272             \@_
273             );
274             return {
275             customer => $args->{customer_id},
276             username => $args->{username},
277             };
278             }
279              
280             One would call this as:
281              
282             my $user_info = $instance->show_user_info('Blah42', 'blah42');
283              
284             =head3 Parameters
285              
286             Positional:
287              
288             =over
289              
290             =item 1. C<$validation_templates>
291              
292             A arrayref with pairs of parameter-names and L<validation templates>.
293              
294              
295             =item 2. C<$values>
296              
297             A arrayref with the actual values that need to be validated.
298              
299             =back
300              
301             =head3 Responses
302              
303             =over
304              
305             =item B<Success> (list context)
306              
307             A list that can be coerced into a hash.
308              
309             =item B<Success> (scalar context)
310              
311             A locked hashref.
312              
313             =item B<Error>
314              
315             Anything L<Params::ValidationCompiler> will throw for invalid values.
316              
317             =back
318              
319             =cut
320              
321             my $self = shift;
322             my $validate_us = $_validators->{_validate_positional_parameters};
323             my ($templates, $data) = $validate_us->(@_);
324              
325             my ($positional_templates, $positional_names);
326 4     4 1 10 my @names_and_templates = @$templates;
327 4         9 while (@names_and_templates) {
328 4         88 my ($pname, $ptemplate) = splice(@names_and_templates, 0, 2);
329             push @$positional_templates, $ptemplate;
330 4         65 push @$positional_names, $pname;
331 4         10 }
332 4         12  
333 4         12 # remember where to store values in (scoped) variables
334 4         11 # should we die() if that value is not a SCALAR-Ref?
335 4         14 my %store_params;
336             for my $i (0 .. $#{$positional_templates}) {
337             if ( exists($positional_templates->[$i]{store})
338             and ref($positional_templates->[$i]{store}) eq 'SCALAR')
339             {
340 4         5 $store_params{ $positional_names->[$i] } = delete(
341 4         7 $positional_templates->[$i]{store}
  4         13  
342 4 100 66     19 );
343             }
344             }
345              
346             my $called_from = (caller(1))[3];
347 1         5 my $this_validator = "validation_for>$called_from";
348              
349             if (not exists($_validators->{ $this_validator })) {
350             $_validators->{$this_validator} = validation_for(
351 4         35 params => $positional_templates,
352 4         15 name => $this_validator,
353             );
354 4 100       12 }
355 3         10 my $validator = $_validators->{ $this_validator };
356              
357             my @validated_values = eval { $validator->(@$data) };
358             if (my $error = $@) {
359             _sniff_it($error);
360 4         2369 }
361              
362 4         8 my %validated;
  4         67  
363 4 100       317 @validated{ @$positional_names } = @validated_values if @$positional_names;
364 2         13  
365             # store values in the their (scoped) variables
366             for my $to_store (keys %store_params) {
367 2         4 ${ $store_params{$to_store} } = $validated{$to_store};
368 2 50       9 }
369              
370             return wantarray ? (%validated) : lock_hash(%validated);
371 2         5 }
372 1         2  
  1         3  
373             =head2 The C<parameter()> method
374              
375 2 50       9 Returns a C<parameter_name>, C<validation_template> pair that can be used in the
376             C<parameters> argument hashref for
377             C<Params::ValidationCompiler::validadion_for()>
378              
379             =head3 Parameters
380              
381             Positional:
382              
383             =over
384              
385             =item 1. C<$name> (I<Required>)
386              
387             The name of this parameter (it must be a kind of identifier: C<< m{^\w+$} >>)
388              
389             =item 2. C<$required> (I<Optional>)
390              
391             One of C<< $class->Required >> or C<< $class->Optional >> but will default to
392             C<< $class->Required >>.
393              
394             =item 3. C<$extra> (I<Optional>)
395              
396             This optional HashRef can contain the fields supported by the C<params>
397             parameter of C<validation_for()>, even overriding the ones set by the C<<
398             $class->ValidationTemplates() >> for this C<$name> - although C<optional> is set
399             by the previous parameter in this sub.
400              
401             This parameter is mostly used for the extra feature to pass a lexically scoped
402             variable via L<store|/"the extra store attribute">.
403              
404             =back
405              
406             =head3 Responses
407              
408             =over
409              
410             =item B<Success>
411              
412             A list of C<$parameter_name> and C<$validation_template>.
413              
414              
415             (this_parm => { optional => 0, type => Str, store => \my $this_param })
416              
417              
418             =back
419              
420             =head3 NOTE on "Unknown" parameters
421              
422             Whenever C<< $self->parameter() >> is called with a parameter-name that doesn't
423             resolve to a template in the C<ValidationTemplates()> hash, a default "empty"
424             template is produced. This will mean that there will be no validation on that
425             value, although one could pass one as the third parameter:
426              
427             use Moo;
428             use Types::Standard qw( StrMatch );
429             with qw(
430             MyTemplates
431             MooX::Params::CompiledValidators
432             );
433              
434             sub show_user_info {
435             my $self = shift;
436             my $args = $self->validate_parameters(
437             {
438             $self->parameter(customer_id => $self->Required),
439             $self->parameter(
440             email => $self->Required,
441             { type => StrMatch[ qr{^ [-.\w]+ @ [-.\w]+ $}x ] },
442             ),
443             },
444             { @_ }
445             );
446             return {
447             customer => $args->{customer_id},
448             email => $args->{email},
449             };
450             }
451              
452             =cut
453              
454             my $self = shift;
455             my $validate_us = $_validators->{_parameter};
456             my ($name, $optional, $extra) = $validate_us->(@_);
457              
458             my $template = exists($self->ValidationTemplates->{$name})
459             ? $self->ValidationTemplates->{$name}
460 8     8 1 17 : { };
461 8         15  
462 8         176 my $final_template = {
463             %$template,
464             ($extra ? %$extra : ()),
465 8 50       130 optional => $optional,
466             };
467             return ($name => $final_template);
468 8 50       10055 }
469              
470             =begin private
471              
472             =head2 _sniff_it($message)
473 8         416  
474             Tailor made exception handler.
475              
476             =end private
477              
478             =cut
479              
480             my ($message) = @_;
481             my ($filename, $line) = (caller(1))[1, 2];
482             my $subroutine = (caller(2))[3];
483              
484             die sprintf('Error in %s (%s:%u): %s', $subroutine, $filename, $line, $message);
485             }
486              
487 4     4   10 use namespace::autoclean;
488 4         20 1;
489 4         25  
490             =head2 The extra C<store> attribute
491 4         22  
492             Both C<validate_parameters()> and C<validate_positional_parameters> support the
493             extra C<store> attribute in a validation template that should be a
494 2     2   3144 scalar-reference where we store the value after successful validation.
  2         4  
  2         18  
495              
496             One can pick and mix with validation templates:
497              
498             use Moo;
499             use Types::Standard qw( StrMatch );
500             with qw(
501             MyTemplates
502             MooX::Params::CompiledValidators
503             );
504              
505             sub show_user_info {
506             my $self = shift;
507             $self->validate_parameters(
508             {
509             $self->parameter(customer_id => $self->Required, {store => \my $customer_id),
510             email => {
511             type => StrMatch[ qr{^ [-.\w]+ @ [-.\w]+ $}x ],
512             optional => 0,
513             store => \my $email
514             },
515             },
516             { @_ }
517             );
518             return {
519             customer => $customer_id,
520             email => $email,
521             };
522             }
523              
524             One would call this as:
525              
526             my $user_info = $instance->show_user_info(
527             customer_id => 'Blah42',
528             email => 'blah42@some.tld',
529             );
530              
531             One could argue that using (lexical) variables -instead of addressing keys of a
532             locked hash- triggers the error caused by a typo at I<compile-time> rather than
533             at I<run-time>.
534              
535             B<NOTE>: In order to keep the scope of the variable, where the value is stored,
536             limited, the C<store> attribute should only be used from the per method override
537             option C<extra> for C<< $self->parameter() >>.
538              
539             =head1 AUTHOR
540              
541             E<copy> MMXXI - Abe Timmerman <abeltje@cpan.org>
542              
543             =head1 LICENSE
544              
545             This library is free software; you can redistribute it and/or modify
546             it under the same terms as Perl itself.
547              
548             This program is distributed in the hope that it will be useful,
549             but WITHOUT ANY WARRANTY; without even the implied warranty of
550             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
551              
552             =cut