File Coverage

blib/lib/Data/Validate/WithYAML.pm
Criterion Covered Total %
statement 209 209 100.0
branch 110 110 100.0
condition 64 64 100.0
subroutine 29 29 100.0
pod 10 10 100.0
total 422 422 100.0


line stmt bran cond sub pod time code
1             package Data::Validate::WithYAML;
2              
3 32     32   2327130 use strict;
  32         490  
  32         981  
4 32     32   193 use warnings;
  32         65  
  32         855  
5              
6 32     32   171 use Carp;
  32         66  
  32         1902  
7 31     31   214 use Scalar::Util qw(looks_like_number);
  31         77  
  31         1506  
8 31     31   18174 use YAML::Tiny;
  31         178875  
  31         75627  
9              
10             # ABSTRACT: Validation framework that can be configured with YAML files
11              
12             our $VERSION = '0.19';
13             our $errstr = '';
14              
15              
16             sub new{
17 29     29 1 6131 my ($class,$filename,%args) = @_;
18            
19 29         89 my $self = {};
20 29         85 bless $self,$class;
21            
22 29         176 $self->{__optional__} = {};
23 29         96 $self->{__required__} = {};
24            
25 29         191 $self->_allow_subs( $args{allow_subs} );
26 29         177 $self->_no_steps( $args{no_steps} );
27 29 100       178 $self->_yaml_config( $filename ) or return undef;
28            
29 23         123 return $self;
30             }
31              
32             sub _optional {
33 181     181   885 my ($self) = @_;
34 181         532 $self->{__optional__};
35             }
36              
37             sub _required {
38 441     441   772 my ($self) = @_;
39 441         1601 $self->{__required__};
40             }
41              
42             sub _no_steps {
43 52     52   182 my ($self, $no_steps) = @_;
44 52 100       201 $self->{__no_steps__} = $no_steps if @_ == 2;;
45 52         164 $self->{__no_steps__};
46             }
47              
48              
49             sub set_optional {
50 3     3 1 1401 my ($self,$field) = @_;
51            
52 3         9 my $value = delete $self->_required->{$field};
53 3 100       158 if( $value ) {
54 1         4 $self->_optional->{$field} = $value;
55             }
56             }
57              
58              
59             sub set_required {
60 2     2 1 1377 my ($self,$field) = @_;
61            
62 2         7 my $value = delete $self->_optional->{$field};
63 2 100       8 if( $value ) {
64 1         4 $self->_required->{$field} = $value;
65             }
66             }
67              
68              
69             sub validate{
70 35     35 1 16845 my $self = shift;
71              
72 35         69 my ($part, %hash);
73              
74 35 100 100     198 if ( @_ && @_ % 2 == 0 ) {
75 3         11 %hash = @_;
76 3         6 $part = '';
77             }
78             else {
79 32         105 ($part, %hash) = @_;
80             }
81              
82 35         104 my @fieldnames = $self->fieldnames( $part );
83            
84 35         65 my %errors;
85             my %fields;
86 35         89 my $optional = $self->_optional;
87 35         80 my $required = $self->_required;
88            
89 35         72 for my $name ( @fieldnames ) {
90 111 100       305 $fields{$name} = $optional->{$name} if exists $optional->{$name};
91 111 100       292 $fields{$name} = $required->{$name} if exists $required->{$name};
92              
93 111 100       264 next if $fields{$name}->{no_validate};
94            
95 106         197 my $value = $hash{$name};
96            
97 106         192 my $depends_on = $fields{$name}->{depends_on};
98 106 100       196 if ( $depends_on ) {
99 26 100 100     124 if ( !exists $hash{$depends_on} && !$fields{$name}->{depends_lax} ) {
    100          
100 3         7 $errors{$name} = $self->message( $name );
101 3         7 next;
102             }
103             elsif ( defined $hash{$depends_on} ) {
104            
105 22         45 my $depends_on_value = $hash{$depends_on};
106 22   100     68 my $cases = $fields{$name}->{case} || {};
107            
108 22 100       63 $fields{$name} = $cases->{$depends_on_value} if $cases->{$depends_on_value};
109             }
110             }
111              
112 103   100     262 $fields{$name}->{type} ||= 'optional';
113 103         306 my $success = $self->check( $name, $hash{$name}, $fields{$name} );
114 103 100       284 if ( !$success ) {
115 25         80 $errors{$name} = $self->message( $name );
116             }
117             }
118            
119 35         205 return %errors;
120             }
121              
122              
123             sub fieldnames{
124 41     41 1 2816 my $self = shift;
125              
126 41         69 my ($step, %options);
127              
128 41 100 100     173 if ( @_ && @_ % 2 == 0 ) {
129 1         4 %options = @_;
130 1         3 $step = '';
131             }
132             else {
133 40         97 ($step, %options) = @_;
134             }
135              
136 41         78 my @names;
137 41 100       82 if( defined $step ){
138 38 100       61 @names = @{ $self->{fieldnames}->{$step} || [] };
  38         162  
139             }
140             else{
141 3         6 for my $step ( keys %{ $self->{fieldnames} } ){
  3         14  
142 6         12 push @names, @{ $self->{fieldnames}->{$step} };
  6         34  
143             }
144             }
145            
146 41 100       125 if ( $options{exclude} ) {
147 2         3 my %hash;
148 2         20 @hash{@names} = (1) x @names;
149            
150 2         5 delete @hash{ @{$options{exclude}} };
  2         6  
151            
152 2         10 @names = keys %hash;
153             }
154              
155 41         164 return @names;
156             }
157              
158              
159             sub errstr{
160 3     3 1 3487 my ($self) = @_;
161 3         21 return $errstr;
162             }
163              
164              
165             sub message {
166 30     30 1 833 my ($self,$field) = @_;
167            
168 30   100     65 my $subhash = $self->_required->{$field} || $self->_optional->{$field};
169 30         56 my $message = "";
170            
171 30 100       84 if ( $subhash->{message} ) {
172 24         82 $message = $subhash->{message};
173             }
174              
175 30         109 $message;
176             }
177              
178              
179             sub check_list {
180 6     6 1 1628 my ($self,$field,$values) = @_;
181            
182 6 100       18 return if !$values;
183 5 100       21 return if ref $values ne 'ARRAY';
184            
185 4         8 my @results;
186 4         7 for my $value ( @{$values} ) {
  4         11  
187 3 100       13 push @results, $self->check( $field, $value ) ? 1 : 0;
188             }
189            
190 3         10 return \@results;
191             }
192              
193              
194             sub check{
195 237     237 1 59472 my ($self,$field,$value,$definition) = @_;
196            
197 237         1307 my %dispatch = (
198             min => \&_min,
199             max => \&_max,
200             regex => \&_regex,
201             length => \&_length,
202             enum => \&_enum,
203             sub => \&_sub,
204             datatype => \&_datatype,
205             );
206            
207 237   100     774 my $subhash = $definition || $self->_required->{$field} || $self->_optional->{$field};
208              
209 237 100 100     1345 if(
    100 100        
      100        
      100        
      100        
      100        
210             ( $definition and $definition->{type} eq 'required' )
211             or ( !$definition and exists $self->_required->{$field} )
212             ){
213 197 100 100     805 return 0 unless defined $value and length $value;
214             }
215             elsif(
216             ( ( $definition and $definition->{type} eq 'optional' )
217             or ( !$definition and exists $self->_optional->{$field} ) )
218             and (not defined $value or not length $value) ){
219 18         75 return 1;
220             }
221            
222 203         351 my $bool = 1;
223            
224 203         648 for my $key( keys %$subhash ){
225 526 100       1331 if( exists $dispatch{$key} ){
    100          
226 293 100       729 unless($dispatch{$key}->($value,$subhash->{$key},$self)){
227 72         115 $bool = 0;
228 72         140 last;
229             }
230             }
231             elsif( $key eq 'plugin' ){
232 6 100       29 my $plugins = ref $subhash->{$key} ? $subhash->{$key} : [$subhash->{$key}];
233              
234 6         14 my $one_not_ok = 0;
235              
236 6         13 for my $plugin ( @{ $plugins } ) {
  6         18  
237 8         47 my ($name) = $plugin =~ m{([A-z0-9_:]+)};
238 8         27 my $module = 'Data::Validate::WithYAML::Plugin::' . $name;
239              
240 8     5   630 eval "use $module";
  5         1814  
  4         517  
  4         93  
241              
242 8 100 100     115 if( not $@ and $module->can('check') ){
243 5         18 my $local_bool = $module->check($value, $subhash);
244 5 100       52 $one_not_ok = 1 if !$local_bool;
245             }
246             else{
247 3         440 croak "Can't check with $module";
248             }
249             }
250              
251 3 100       13 $bool = $one_not_ok ? 0 : 1;
252             }
253             }
254            
255 197         749 return $bool;
256             }
257              
258              
259             sub fieldinfo {
260 3     3 1 22 my ($self, $field) = @_;
261              
262 3   100     7 my $info = $self->_required->{$field} || $self->_optional->{$field};
263 3 100       12 return if !$info;
264              
265 2         10 return $info;
266             }
267              
268             # read config file and parse required and optional fields
269             sub _yaml_config{
270 29     29   101 my ($self,$file) = @_;
271            
272 29 100 100     958 if ( ref $file and 'SCALAR' eq ref $file ) {
    100 100        
    100          
273             eval {
274 3         11 $self->{config} = YAML::Tiny->read_string( ${$file} );
  3         27  
275 2         2741 1;
276 3 100       14 } or do {
277 1         537 $errstr = $@;
278 1         9 return;
279             };
280             }
281             elsif(defined $file and -e $file){
282             eval {
283 22         162 $self->{config} = YAML::Tiny->read( $file );
284 21         283729 1;
285 22 100       81 } or do {
286 1         12793 $errstr = $@;
287 1         8 return;
288             };
289             }
290             elsif(defined $file){
291 3         14 $errstr = 'file does not exist';
292 3         33 return;
293             }
294             else {
295 1         3 $errstr = 'Need path to YAML file';
296 1         6 return;
297             }
298              
299 23 100       622 if ( $self->_no_steps ) {
300 2         12 $self->_add_fields( $self->{config}->[0], '' );
301             }
302             else {
303 21         57 for my $section(keys %{$self->{config}->[0]}){
  21         144  
304 31         95 my $sec_hash = $self->{config}->[0]->{$section};
305 31         108 $self->_add_fields( $sec_hash, $section );
306             }
307             }
308              
309 23         111 return $self->{config};
310             }
311              
312             sub _add_fields {
313 33     33   106 my ($self, $data, $section) = @_;
314              
315 33         142 for my $field( keys %$data ){
316 113 100 100     583 if(exists $data->{$field}->{type} and
    100          
317             $data->{$field}->{type} eq 'required'){
318 48         148 $self->_required->{$field} = $data->{$field};
319              
320 48 100       113 if( exists $self->_optional->{$field} ){
321 5         16 delete $self->_optional->{$field};
322             }
323             }
324             elsif( not exists $self->_required->{$field} ){
325 62         169 $self->_optional->{$field} = $data->{$field};
326             }
327              
328 113         188 push @{$self->{fieldnames}->{$section}}, $field;
  113         428  
329             }
330             }
331              
332             sub _min{
333 116     116   332 my ($value,$min) = @_;
334 116 100       414 return if !looks_like_number $value;
335 114         381 return $value >= $min;
336             }
337              
338             sub _max{
339 108     108   314 my ($value,$max) = @_;
340 108 100       288 return if !looks_like_number $value;
341 106         319 return $value <= $max;
342             }
343              
344             sub _regex{
345 22     22   49 my ($value,$regex) = @_;
346 22         690 my $re = qr/$regex/;
347 22         237 return ($value =~ $re);
348             }
349              
350             sub _length{
351 21     21   150 my ($value,$check) = @_;
352            
353 21 100       88 if($check =~ /,/){
354 20         150 my ($min,$max) = $check =~ /\s*(\d+)?\s*,\s*(\d+)?/;
355 20         43 my $bool = 1;
356 20 100 100     112 if(defined $min and length $value < $min){
357 6         11 $bool = 0;
358             }
359 20 100 100     76 if(defined $max and length $value > $max){
360 1         2 $bool = 0;
361             }
362 20         80 return $bool;
363             }
364             else{
365 1         7 return length $value == $check;
366             }
367             }
368              
369             sub _enum{
370 23     23   58 my ($value,$list) = @_;
371 23         53 return grep{ $_ eq $value }@$list;
  67         177  
372             }
373              
374             sub _sub {
375 4     4   14 my ($value,$sub,$self) = @_;
376 4         20 $_ = $value;
377            
378 4 100       12 croak "Can't use user defined sub unless it is allowed" if !$self->_allow_subs;
379            
380 2         167 return eval "$sub";
381             }
382              
383             sub _allow_subs {
384 33     33   147 my ($self,$value) = @_;
385            
386 33 100       171 $self->{__allow_subs} = $value if @_ == 2;
387 33         310 $self->{__allow_subs};
388             }
389              
390             sub _datatype {
391 36     36   180 my ($value, $type) = @_;
392              
393 36         76 $type = lc $type;
394              
395 36 100       98 if ( $type eq 'int' ) {
    100          
    100          
396 17 100       58 return if !looks_like_number $value;
397 15 100       103 return if $value !~ m{\A [+-]? \d+ (?:[eE]\d+)? \z}xms;
398 13         79 return 1;
399             }
400             elsif ( $type eq 'num' ) {
401 6 100       25 return if !looks_like_number $value;
402 4         13 return 1;
403             }
404             elsif ( $type eq 'positive_int' ) {
405 12 100       50 return if !looks_like_number $value;
406 9   100     21 return _datatype( $value, 'int' ) && $value > 0;
407             }
408              
409 1         12 croak "Unknown datatype $type";
410             }
411              
412             1;
413              
414             __END__