File Coverage

blib/lib/Data/FormValidator/Multi.pm
Criterion Covered Total %
statement 66 68 97.0
branch 17 22 77.2
condition 5 7 71.4
subroutine 12 12 100.0
pod 8 8 100.0
total 108 117 92.3


line stmt bran cond sub pod time code
1 3     3   15095 use warnings;
  3         24  
  3         95  
2 3     3   18 use strict;
  3         6  
  3         75  
3              
4 3     3   1409 use Data::FormValidator::Multi::Results;
  3         11  
  3         39  
5              
6             package Data::FormValidator::Multi;
7 3     3   126 use base qw(Data::FormValidator);
  3         9  
  3         2236  
8              
9             our $VERSION = '0.002';
10              
11             =encoding utf8
12              
13             =head1 NAME
14              
15             Data::FormValidator::Multi - Check multidimensional data structures with
16             Data::FormValidator
17              
18             =head1 SYNOPSIS
19              
20             use Data::FormValidator::Multi;
21              
22             # a hash that has hashes and arrays as some of its values
23             my $data = { ... };
24              
25             # a Data::FormValidator profile
26             my $main_profile = { ... };
27              
28             # Data::FormValidator profiles for hashes and arrays in $data
29             my $meta_profile = { ... };
30             my $timezones_profile = { ... };
31            
32             # create the validator
33             my $dfv = Data::FormValidator::Multi->new({
34             profile => $main_profile,
35             subprofiles => {
36             meta => $meta_profile,
37             timezones => $timezones_profile,
38             }
39             });
40            
41             # run the check on the data
42             my $results = $dfv->check( $data );
43            
44             # call ->to_json on the results object to get a data structure of errors
45             use Data::Dumper;
46             print Data::Dumper->Dump([$results->to_json], ['results_as_json']);
47              
48             =head1 DESCRIPTION
49              
50             Data::FormValidator feels like the sanest data validator to me other than now
51             that I use angular a lot I'm POSTing complex data structures now instead of
52             CGI key=value pairs.
53              
54             This module provides the ability to validate complex data structures and keep
55             the same DFV Csuccess ) { ... }> pattern during validation.
56              
57             DFVM can validate arbitrarily nested hash and array data structures. For arrays
58             the specified profile is applied to each element in the array.
59              
60             After a data structure is Ced, C will return false if a
61             single bit of data in the data structure is invalid. From there C can
62             be called on the C<$result> to get a data structure that has invalid fields as
63             keys and the errors for that field as its value. What I do with this return
64             value is pass it back to an angular controller so that, with of the magic
65             of two way binding, error messages and indicators magically light up.
66              
67             =head1 EXAMPLE
68              
69             Here is a complete example of validating a hash that has a hash and an array
70             as some of its values. See the F
71             test for an example of validating an arbitrarily deep data structure.
72              
73             use warnings;
74             use strict;
75            
76             use Data::FormValidator::Multi;
77            
78             # the data we are validating. Note the negative $data->{dashboard} and
79             # $data->{timezones}[1]{id} fields, and the commented out hash entries.
80             # These are the invalid / missing fields that DFVM will report on
81             my $data = {
82             dashboard => -23,
83             name => 'FooBar',
84             meta => {
85             foo => 'Foo',
86             # bar => 'Bar',
87             bazz => 'Bazz',
88             },
89             timezones => [
90             {
91             id => 999,
92             name => 'Home',
93             zone => 'America/New_York',
94             date => '01/01',
95             time => '23:59'
96             },
97             {
98             id => -111,
99             name => 'L. A.',
100             zone => 'America/Los_Angeles',
101             # date => '01/01',
102             time => '20:59'
103             }
104             ]
105             };
106            
107             # profile for the fields in the top level of the input data
108             my $main_profile = {
109             required => [qw(name dashboard timezones )],
110             optional => [qw(meta)],
111             constraint_methods => {
112             dashboard => [
113             {
114             name => 'not_positive',
115             constraint_method => sub {
116             my ($dfv, $val) = @_;
117             return $val =~ /\A\d+\z/;
118             }
119             }
120             ]
121             },
122             msgs => {
123             format => '%s',
124             invalid => 'FIELD IS INVALID',
125             missing => 'FIELD IS REQUIRED',
126             constraints => {
127             not_positive => 'MUST BE POSITIVE'
128             }
129             }
130             };
131            
132             # profile for the data in the $data->{meta} hash
133             my $meta_profile = {
134             required => [qw( foo bar bazz )],
135             msgs => {
136             format => '%s',
137             invalid => 'FIELD IS INVALID',
138             missing => 'FIELD IS REQUIRED',
139             }
140             };
141            
142             # profile for the data in the $data->{timezones} array
143             my $timezones_profile = {
144             required => [ qw( id zone name date time ) ],
145             constraint_methods => {
146             id => [
147             {
148             name => 'not_positive',
149             constraint_method => sub {
150             my ($dfv, $val) = @_;
151             return $val =~ /\A\d+\z/;
152             }
153             }
154             ]
155             },
156             msgs => {
157             format => '%s',
158             invalid => 'FIELD IS INVALID',
159             missing => 'FIELD IS REQUIRED',
160             constraints => {
161             not_positive => 'MUST BE POSITIVE'
162             }
163             }
164             };
165            
166             # create a profile, passing the top level profile under the 'profile' key, and
167             # the profiles for the $data->{meta} hash and $data->{timezones} array as a
168             # hash under the 'subprofiles' key.
169             my $dfv = Data::FormValidator::Multi->new({
170             profile => $main_profile,
171             subprofiles => {
172             meta => $meta_profile,
173             timezones => $timezones_profile,
174             }
175             });
176            
177             # run the check on the data
178             my $results = $dfv->check( $data );
179            
180             use Data::Dumper;
181             print Data::Dumper->Dump([$results->to_json], ['results_as_json']);
182              
183             outputs:
184              
185             $results_as_json = {
186             'meta' => {
187             'bar' => 'FIELD IS REQUIRED'
188             },
189             'timezones' => [
190             undef,
191             {
192             'id' => 'MUST BE POSITIVE',
193             'date' => 'FIELD IS REQUIRED'
194             }
195             ],
196             'dashboard' => 'MUST BE POSITIVE'
197             };
198              
199             =head1 METHODS
200              
201             =head2 check
202              
203             If given an array, loop over them, do a DFVM check on each element, and return
204             an array blessed as a DFV::Multi::Results object.
205              
206             Otherwise, call in to parent check method (plain Data::FormValidator) for
207             validation of the data and return the DFV::Multi::Results object.
208              
209             =cut
210              
211             sub check {
212 8     8 1 12225 my($self, $datas, $profile) = @_;
213              
214 8         18 my $results = [];
215 8 100       32 if ( ref $datas eq 'ARRAY' ) {
216 1         3 foreach my $data ( @$datas ) {
217 3         12 my $element_results = (ref $self)->new->check( $data, $self->{profiles}{profile} );
218 3         12 push @$results => $element_results;
219             }
220             } else {
221 7   66     57 $results = $self->SUPER::check( $datas, $self->{profiles}{profile} || $profile );
222             }
223              
224 8         5446 bless $results => 'Data::FormValidator::Multi::Results';
225              
226 8 100       50 $self->check_nested( $results ) unless ref $datas eq 'ARRAY';
227              
228 8         52 return $results;
229             }
230              
231             =head2 check_nested
232              
233             =cut
234              
235             sub check_nested {
236 7     7 1 17 my($self, $results) = @_;
237              
238 7   100     37 my $profiles = $self->{profiles}{subprofiles} || {};
239 7         23 foreach my $field ( keys %$profiles ) {
240 7         45 $self->check_nested_for( $field => $results );
241             }
242             }
243              
244             =head2 check_nested_for
245              
246             =cut
247              
248             sub check_nested_for {
249 7     7 1 18 my($self, $field, $results) = @_;
250              
251 7         14 my $profile = $self->{profiles}{subprofiles}{$field};
252              
253 7 100       19 if ( $profile->{subprofiles} ) {
254 2         19 $self->has_nested_profiles( $profile, $field, $results );
255             } else {
256 5         13 $self->no_nested_profiles( $profile, $field, $results );
257             }
258              
259             }
260              
261             =head2 has_nested_profiles
262              
263             =cut
264              
265             sub has_nested_profiles {
266 2     2 1 7 my($self, $profile, $field, $results) = @_;
267              
268 2 50       8 if ( my $data = $results->valid($field) ) { # data can be an array or hash
269 2         29 my $nested_results = (ref $self)->new( $profile )->check( $data );
270              
271 2 50       11 if ( ! $nested_results->success ) {
272 2         7 $self->move_from_valid_to_objects( $field, $results, $nested_results );
273             }
274             }
275             }
276              
277             =head2 no_nested_profiles
278              
279             =cut
280              
281             sub no_nested_profiles {
282 5     5 1 15 my($self, $profile, $field, $results) = @_;
283              
284 5 50       23 if ( my $datas = $results->valid($field) ) { # data can be an array or hash
285              
286 5 100       183 if ( ref $datas eq 'HASH' ) {
    50          
287 2         10 $self->handle_hash_input( $profile, $field, $results, $datas );
288             } elsif ( ref $datas eq 'ARRAY' ) {
289 3         13 $self->handle_array_input( $profile, $field, $results, $datas );
290             } else {
291 0         0 die 'dont know how to process $datas';
292             }
293              
294             }
295              
296             }
297              
298             =head2 handle_hash_input
299              
300             =cut
301              
302             sub handle_hash_input {
303 2     2 1 6 my($self, $profile, $field, $results, $datas) = @_;
304              
305 2         15 my $nested_results = Data::FormValidator::Multi::Results->new( $profile, $datas );
306              
307 2 50       687 if ( $nested_results->success ) {
308 2         55 $results->valid( $field => $nested_results );
309             } else {
310 0         0 $self->move_from_valid_to_objects( $field, $results, $nested_results );
311             }
312             }
313              
314             =head2 handle_array_input
315              
316             =cut
317              
318             sub handle_array_input {
319 3     3 1 10 my($self, $profile, $field, $results, $datas) = @_;
320              
321             # $DB::single = 1;
322              
323             # set up some local state to handle error condition
324 3         6 my $error = {};
325 3         10 my $errors = $error->{errors} = [];
326 3         10 $error->{total} = $error->{count} = 0;
327 3         19 my $all_results = [];
328              
329 3         10 foreach my $data ( @$datas ) {
330 6         52 $error->{total}++;
331 6         14 push @$errors => undef; # array element gets replaced if there is an error
332              
333 6         20 my $nested_results = Data::FormValidator::Multi::Results->new( $profile, $data );
334              
335 6         4028 push @$all_results => $nested_results;
336 6 100       20 if ( ! $nested_results->success ) {
337 2         26 $error->{count}++;
338 2         6 pop @$errors;
339 2         7 push @$errors => $nested_results;
340             }
341             }
342              
343 3 100       35 if ( ! $error->{count} ) {
344 1         4 $results->valid( $field => $all_results );
345             } else {
346 2         12 $self->move_from_valid_to_objects( $field, $results, $errors )
347             }
348             }
349              
350             =head2 move_from_valid_to_objects
351              
352             =cut
353              
354             sub move_from_valid_to_objects {
355 4     4 1 11 my($self, $field, $results, $field_results) = @_;
356              
357 4         11 delete $results->{valid}{$field};
358              
359 4   50     25 $results->{objects} ||= {};
360 4         42 $results->{objects}{$field} = $field_results;
361             }
362              
363             =head1 SEE ALSO
364              
365             =over 4
366              
367             =item *
368              
369             L
370              
371             =back
372              
373             =head1 AUTHOR
374              
375             Todd Wade
376              
377             =head1 COPYRIGHT AND LICENSE
378              
379             This software is copyright (c) 2016 by Todd Wade.
380              
381             This is free software; you can redistribute it and/or modify it under
382             the same terms as the Perl 5 programming language system itself.
383              
384             =cut
385              
386             1;