File Coverage

blib/lib/Data/Validate/WithYAML.pm
Criterion Covered Total %
statement 203 203 100.0
branch 104 104 100.0
condition 64 64 100.0
subroutine 29 29 100.0
pod 10 10 100.0
total 410 410 100.0


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