File Coverage

blib/lib/Brannigan/Tree.pm
Criterion Covered Total %
statement 158 168 94.0
branch 117 128 91.4
condition 61 78 78.2
subroutine 12 12 100.0
pod 4 4 100.0
total 352 390 90.2


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