File Coverage

blib/lib/Brannigan/Tree.pm
Criterion Covered Total %
statement 161 171 94.1
branch 118 128 92.1
condition 64 78 82.0
subroutine 12 12 100.0
pod 4 4 100.0
total 359 393 91.3


line stmt bran cond sub pod time code
1             package Brannigan::Tree;
2              
3             our $VERSION = "1.1";
4             $VERSION = eval $VERSION;
5              
6 3     3   30 use strict;
  3         6  
  3         98  
7 3     3   17 use warnings;
  3         7  
  3         77  
8 3     3   2064 use Brannigan::Validations;
  3         6  
  3         9217  
9              
10             =head1 NAME
11              
12             Brannigan::Tree - A Brannigan validation/parsing scheme tree, possibly built from a series of inherited schemes.
13              
14             =head1 VERSION
15              
16             version 1.1
17              
18             =head1 DESCRIPTION
19              
20             This module is used internally by L. Basically, a tree is a
21             validation/parsing scheme in its "final", workable structure, taking
22             any inherited schemes into account. The actual validation and parsing
23             of input is done by this module.
24              
25             =head1 CONSTRUCTOR
26              
27             =head2 new( $scheme | @schemes )
28              
29             Creates a new Brannigan::Tree instance from one or more schemes.
30              
31             =cut
32              
33             sub new {
34 14     14 1 27 my $class = shift;
35              
36 14         192 return bless $class->_merge_trees(@_), $class;
37             }
38              
39             =head1 OBJECT METHODS
40              
41             =head2 process( \%params )
42              
43             Validates and parses the hash-ref of input parameters. Returns a hash-ref
44             of the parsed input, possibly containing a '_rejects' hash-ref with a list
45             of failed validations for each failed parameter.
46              
47             =cut
48              
49             sub process {
50 9     9 1 16 my ($self, $params) = @_;
51              
52             # validate the data
53 9         19 my $data = {};
54              
55 9         33 my $rejects = $self->validate($params, $self->{params});
56 9 100       34 $data->{_rejects} = $rejects if $rejects;
57              
58 9         70 my $prs = $self->parse($params, $self->{params}, $self->{groups});
59 9         47 foreach (sort keys %$prs) {
60 60         127 $data->{$_} = $prs->{$_};
61             }
62              
63 9         107 return $data;
64             }
65              
66             =head2 validate( \%params )
67              
68             Validates the hash-ref of input parameters and returns a hash-ref of rejects
69             (i.e. failed validation methods) for each parameter.
70              
71             =cut
72              
73             sub validate {
74 38     38 1 52 my ($self, $params, $rules) = @_;
75              
76 38         36 my $rejects;
77              
78             # go over all the parameters and validate them
79 38         148 foreach (sort keys %$params) {
80             # find references to this parameter, first in regexes, then direct
81             # give preference to the direct references
82 136         154 my @references;
83 136 100       308 push(@references, $rules->{_all}) if $rules->{_all};
84 136         539 foreach my $param (sort keys %$rules) {
85 854 100       2171 next unless $param =~ m!^/([^/]+)/$!;
86 116         1748 my $re = qr/$1/;
87 116 100       795 push(@references, $rules->{$param}) if m/$re/;
88             }
89 136 100       429 push(@references, $rules->{$_}) if $rules->{$_};
90              
91 136         346 my $rj = $self->_validate_param($_, $params->{$_}, $self->_merge_trees(@references));
92              
93 136 100       595 $rejects->{$_} = $rj if $rj;
94             }
95              
96             # find required parameters that aren't there
97 38         156 foreach (sort keys %$rules) {
98 167 100       306 next if $_ eq '_all';
99 158 100       368 next if m!^/[^/]+/$!;
100 136 100 100     621 $rejects->{$_} = ['required(1)'] if $rules->{$_}->{required} && (!defined $params->{$_} || $params->{$_} eq '');
      66        
101             }
102              
103 38         94 return $rejects;
104             }
105              
106             =head2 parse( \%params, \%param_rules, [\%group_rules] )
107              
108             Receives a hash-ref of parameters, a hash-ref of parameter rules (this is
109             the 'params' part of a scheme) and optionally a hash-ref of group rules
110             (this is the 'groups' part of a scheme), parses the parameters according
111             to these rules and returns a hash-ref of all the parameters after parsing.
112              
113             =cut
114              
115             sub parse {
116 56     56 1 90 my ($self, $params, $param_rules, $group_rules) = @_;
117              
118 56         74 my $data;
119              
120             # fill-in missing parameters with default values, if defined
121 56         203 foreach (sort keys %$param_rules) {
122 187 100       608 next if m!^/[^/]+/$!;
123 165 100 100     802 next unless !defined $params->{$_} || $params->{$_} eq '';
124              
125             # is there a default value/method?
126 34 100 100     196 if (exists $param_rules->{$_}->{default} && ref $param_rules->{$_}->{default} eq 'CODE') {
    100          
127 3         13 $data->{$_} = $param_rules->{$_}->{default}->();
128             } elsif (exists $param_rules->{$_}->{default}) {
129 3         12 $data->{$_} = $param_rules->{$_}->{default};
130             }
131             }
132              
133             # parse the data
134 56         208 foreach (sort keys %$params) {
135             # ignore undefined or empty values
136 154 100 100     789 next if !defined $params->{$_} || $params->{$_} eq '';
137            
138             # is there a reference to this parameter in the scheme?
139 149         170 my @refs;
140 149         546 foreach my $p (sort keys %$param_rules) {
141 841 100       2079 next unless $p =~ m!^/([^/]+)/$!;
142 110         1491 my $re = qr/$1/;
143 110 100       672 next unless m/$re/;
144 49         157 push(@refs, $param_rules->{$p});
145             }
146 149 100       490 push(@refs, $param_rules->{$_}) if $param_rules->{$_};
147            
148 149 100 66     411 next if scalar @refs == 0 && $self->{ignore_missing};
149 147 100 66     667 unless (scalar @refs && $self->{ignore_missing}) {
150             # pass the parameter as is
151 2         5 $data->{$_} = $params->{$_};
152 2         7 next;
153             }
154              
155             # is this a hash-ref or an array-ref or just a scalar?
156 145 100       477 if (ref $params->{$_} eq 'HASH') {
    100          
157 29         76 my $pd = $self->parse($params->{$_}, $self->_merge_trees(@refs)->{keys});
158 29         118 foreach my $k (sort keys %$pd) {
159 83         301 $data->{$_}->{$k} = $pd->{$k};
160             }
161             } elsif (ref $params->{$_} eq 'ARRAY') {
162 9         11 foreach my $val (@{$params->{$_}}) {
  9         24  
163             # we need to parse this value with the rules
164             # in the 'values' key
165 18         65 my $pd = $self->parse({ param => $val }, { param => $self->_merge_trees(@refs)->{values} });
166 18         54 push(@{$data->{$_}}, $pd->{param});
  18         89  
167             }
168             } else {
169             # is there a parsing method?
170             # first see if there's one in a regex
171 107         109 my $parse;
172 107         225 my @data = ($params->{$_});
173 107         392 foreach my $r (sort keys %$param_rules) {
174 553 100       1414 next unless $r =~ m!^/([^/]+)/$!;
175 69         748 my $re = qr/$1/;
176            
177 69         346 my @matches = (m/$re/);
178 69 100       199 next unless scalar @matches > 0;
179 43         71 push(@data, @matches);
180              
181 43 100       184 $parse = $param_rules->{$r}->{parse} if $param_rules->{$r}->{parse};
182             }
183 107 100       369 $parse = $param_rules->{$_}->{parse} if $param_rules->{$_}->{parse};
184              
185             # make sure if we have a parse method that is indeed a subroutine
186 107 100 66     381 if ($parse && ref $parse eq 'CODE') {
187 17         59 my $parsed = $parse->(@data);
188 17         187 foreach my $k (sort keys %$parsed) {
189 17 50       65 if (ref $parsed->{$k} eq 'HASH') {
    50          
190 0         0 foreach my $sk (sort keys %{$parsed->{$k}}) {
  0         0  
191 0         0 $data->{$k}->{$sk} = $parsed->{$k}->{$sk};
192             }
193             } elsif (ref $parsed->{$k} eq 'ARRAY') {
194 0         0 push(@{$data->{$k}}, @{$parsed->{$k}});
  0         0  
  0         0  
195             } else {
196 17         111 $data->{$k} = $parsed->{$k};
197             }
198             }
199             } else {
200             # just pass as-is
201 90         380 $data->{$_} = $params->{$_};
202             }
203             }
204             }
205              
206             # parse group data
207 56 100       162 if ($group_rules) {
208 7         26 foreach (sort keys %$group_rules) {
209 11         12 my @data;
210            
211             # do we have a list of parameters, or a regular expression?
212 11 100       42 if (exists $group_rules->{$_}->{params}) {
    50          
213 8         9 foreach my $p (@{$group_rules->{$_}->{params}}) {
  8         21  
214 20         57 push(@data, $data->{$p});
215             }
216             } elsif (exists $group_rules->{$_}->{regex}) {
217 3         26 my ($re) = ($group_rules->{$_}->{regex} =~ m!^/([^/]+)/$!);
218 3 50       14 next unless $re;
219 3         17 $re = qr/$re/;
220 3         24 foreach my $p (sort keys %$data) {
221 39 100       137 next unless $p =~ m/$re/;
222 12         23 push(@data, $data->{$p});
223             }
224             } else {
225             # we have nothing in this group
226 0         0 next;
227             }
228            
229             # parse the data
230 11         49 my $parsed = $group_rules->{$_}->{parse}->(@data);
231 11         105 foreach my $k (sort keys %$parsed) {
232 8 100       30 if (ref $parsed->{$k} eq 'ARRAY') {
    50          
233 3         6 push(@{$data->{$k}}, @{$parsed->{$k}});
  3         8  
  3         22  
234             } elsif (ref $parsed->{$k} eq 'HASH') {
235 0         0 foreach my $sk (sort keys %{$parsed->{$k}}) {
  0         0  
236 0         0 $data->{$k}->{$sk} = $parsed->{$k}->{$sk};
237             }
238             } else {
239 5         25 $data->{$k} = $parsed->{$k};
240             }
241             }
242             }
243             }
244              
245 56         124 return $data;
246             }
247              
248             #############################
249             ##### INTERNAL METHODS ######
250             #############################
251              
252             # _validate_param( $param, $value, \%validations )
253             # ------------------------------------------------
254             # Receives the name of a parameter, its value, and a hash-ref of validations
255             # to assert against. Returns a list of validations that failed for this
256             # parameter. Depending on the type of the parameter (either scalar, hash
257             # or array), this method will call one of the following three methods.
258              
259             sub _validate_param {
260 154     154   278 my ($self, $param, $value, $validations) = @_;
261              
262             # is there any reference to this parameter in the scheme?
263 154 100       294 return unless $validations;
264              
265             # is this parameter required? if not, and it has no value
266             # (either undef or an empty string), then don't bother checking
267             # any validations. If yes, and it has no value, do the same.
268 152 100 100     590 return if !$validations->{required} && (!defined $value || $value eq '');
      66        
269 150 100 100     611 return ['required(1)'] if $validations->{required} && (!defined $value || $value eq '');
      66        
270              
271             # is this parameter forbidden? if yes, and it has a value,
272             # don't bother checking any other validations.
273 147 50 66     353 return ['forbidden(1)'] if $validations->{forbidden} && defined $value && $value ne '';
      66        
274              
275             # is this a scalar, array or hash parameter?
276 146 100       377 if ($validations->{hash}) {
    100          
277 29         62 return $self->_validate_hash($param, $value, $validations);
278             } elsif ($validations->{array}) {
279 11         27 return $self->_validate_array($param, $value, $validations);
280             } else {
281 106         229 return $self->_validate_scalar($param, $value, $validations);
282             }
283             }
284              
285             # _validate_scalar( $param, $value, \%validations, [$type] )
286             # ----------------------------------------------------------
287             # Receives the name of a parameter, its value, and a hash-ref of validations
288             # to assert against. Returns a list of all failed validations for this
289             # parameter. If the parameter is a child of a hash/array parameter, then
290             # C<$type> must be provided with either 'hash' or 'array'.
291              
292             sub _validate_scalar {
293 144     144   230 my ($self, $param, $value, $validations, $type) = @_;
294              
295 144         147 my @rejects;
296              
297             # get all validations we need to perform
298 144         476 foreach my $v (sort keys %$validations) {
299             # skip the parse method and the default value
300 329 100 66     1362 next if $v eq 'parse' || $v eq 'default';
301 306 100 100     847 next if $type && $type eq 'array' && $v eq 'values';
      100        
302 297 100 100     923 next if $type && $type eq 'hash' && $v eq 'keys';
      100        
303              
304             # get the data we're passing to the validation method
305 268 100       718 my @data = ref $validations->{$v} eq 'ARRAY' ? @{$validations->{$v}} : ($validations->{$v});
  69         165  
306            
307             # which validation method are we gonna use?
308             # custom ones have preference
309 268 100 66     1754 if ($v eq 'validate' && ref $validations->{$v} eq 'CODE') {
    100 100        
      66        
310             # this is an "inline" validation method, invoke it
311 18 100       53 push(@rejects, $v) unless $validations->{$v}->($value, @data);
312             } elsif (exists $self->{_custom_validations} && exists $self->{_custom_validations}->{$v} && ref $self->{_custom_validations}->{$v} eq 'CODE') {
313             # this is a cross-scheme custom validation method
314 9 100       32 push(@rejects, $v.'('.join(', ', @data).')') unless $self->{_custom_validations}->{$v}->($value, @data);
315             } else {
316             # we're using a built-in validation method
317 241 100       896 push(@rejects, $v.'('.join(', ', @data).')') unless Brannigan::Validations->$v($value, @data);
318             }
319             }
320              
321 144 100       826 return scalar @rejects ? [@rejects] : undef;
322             }
323              
324             # _validate_array( $param, $value, \%validations )
325             # ------------------------------------------------
326             # Receives the name of an array parameter, its value, and a hash-ref of validations
327             # to assert against. Returns a list of validations that failed for this
328             # parameter.
329              
330             sub _validate_array {
331 11     11   20 my ($self, $param, $value, $validations) = @_;
332              
333             # if this isn't an array, don't bother checking any other validation method
334 11 100       34 return { _self => ['array(1)'] } unless ref $value eq 'ARRAY';
335              
336             # invoke validations on the parameter itself
337 9         12 my $rejects = {};
338 9         21 my $_self = $self->_validate_scalar($param, $value, $validations, 'array');
339 9 50       19 $rejects->{_self} = $_self if $_self;
340              
341             # invoke validations on the values of the array
342 9         12 my $i = 0;
343 9         20 foreach (@$value) {
344 18         67 my $rj = $self->_validate_param("${param}[$i]", $_, $validations->{values});
345 18 100       50 $rejects->{$i} = $rj if $rj;
346 18         37 $i++;
347             }
348              
349 9 100       42 return scalar keys %$rejects ? $rejects : undef;
350             }
351              
352             # _validate_hash( $param, $value, \%validations )
353             # -----------------------------------------------
354             # Receives the name of a hash parameter, its value, and a hash-ref of validations
355             # to assert against. Returns a list of validations that failed for this
356             # parameter.
357              
358             sub _validate_hash {
359 29     29   46 my ($self, $param, $value, $validations) = @_;
360              
361             # if this isn't a hash, don't bother checking any other validation method
362 29 50       64 return { _self => ['hash(1)'] } unless ref $value eq 'HASH';
363              
364             # invoke validations on the parameter itself
365 29         43 my $rejects = {};
366 29         63 my $_self = $self->_validate_scalar($param, $value, $validations, 'hash');
367 29 50       66 $rejects->{_self} = $_self if $_self;
368              
369             # invoke validations on the keys of the hash (a.k.a mini-params)
370 29         80 my $hr = $self->validate($value, $validations->{keys});
371              
372 29         93 foreach (sort keys %$hr) {
373 38         86 $rejects->{$_} = $hr->{$_};
374             }
375              
376 29 100       145 return scalar keys %$rejects ? $rejects : undef;
377             }
378              
379             # _merge_trees( @trees )
380             # ----------------------
381             # Merges two or more hash-refs of validation/parsing trees and returns the
382             # resulting tree. The merge is performed in order, so trees later in the
383             # array (i.e. on the right) "tramp" the trees on the left.
384              
385             sub _merge_trees {
386 219     219   282 my $class = shift;
387              
388 219 100 66     922 return unless scalar @_ && (ref $_[0] eq 'HASH' || ref $_[0] eq 'Brannigan::Tree');
      66        
389              
390             # the leftmost tree is the starting tree
391 217         251 my $tree = shift;
392 217         809 my %tree = %$tree;
393              
394             # now for the merging business
395 217         499 foreach (@_) {
396 88 50       190 next unless ref $_ eq 'HASH';
397              
398 88         279 foreach my $k (sort keys %$_) {
399 81 100       184 if (ref $_->{$k} eq 'HASH') {
400 28 100       86 unless (exists $tree{$k}) {
401 6         29 $tree{$k} = $_->{$k};
402             } else {
403 22         78 $tree{$k} = $class->_merge_trees($tree{$k}, $_->{$k});
404             }
405             } else {
406 53 100 66     280 if ($k eq 'forbidden' && $_->{$k}) {
    100 100        
407             # remove required, if there was such a rule
408 2         5 delete $tree{'required'};
409             } elsif ($k eq 'required' && $_->{$k}) {
410             # remove forbidden, if there was such a rule
411 6         13 delete $tree{'forbidden'};
412             }
413 53         193 $tree{$k} = $_->{$k};
414             }
415             }
416             }
417              
418 217         820 return \%tree;
419             }
420              
421             =head1 SEE ALSO
422              
423             L, L.
424              
425             =head1 AUTHOR
426              
427             Ido Perlmuter, C<< >>
428              
429             =head1 BUGS
430              
431             Please report any bugs or feature requests to C, or through
432             the web interface at L. I will be notified, and then you'll
433             automatically be notified of progress on your bug as I make changes.
434              
435             =head1 SUPPORT
436              
437             You can find documentation for this module with the perldoc command.
438              
439             perldoc Brannigan::Tree
440              
441             You can also look for information at:
442              
443             =over 4
444              
445             =item * RT: CPAN's request tracker
446              
447             L
448              
449             =item * AnnoCPAN: Annotated CPAN documentation
450              
451             L
452              
453             =item * CPAN Ratings
454              
455             L
456              
457             =item * Search CPAN
458              
459             L
460              
461             =back
462              
463             =head1 LICENSE AND COPYRIGHT
464              
465             Copyright 2010-2013 Ido Perlmuter.
466              
467             This program is free software; you can redistribute it and/or modify it
468             under the terms of either: the GNU General Public License as published
469             by the Free Software Foundation; or the Artistic License.
470              
471             See http://dev.perl.org/licenses/ for more information.
472              
473             =cut
474              
475             1;