File Coverage

blib/lib/Data/Validator.pm
Criterion Covered Total %
statement 140 142 98.5
branch 64 66 96.9
condition 6 8 75.0
subroutine 15 15 100.0
pod 4 9 44.4
total 229 240 95.4


line stmt bran cond sub pod time code
1             package Data::Validator;
2 20     20   850297 use 5.008_001;
  20         85  
  20         1522  
3 20     20   26662 use Mouse;
  20         1048457  
  20         188  
4 20     20   14790 use Mouse::Util::TypeConstraints ();
  20         56  
  20         354  
5 20     20   114 use Mouse::Util ();
  20         42  
  20         313  
6 20     20   107 use Carp ();
  20         40  
  20         2025  
7              
8             our $VERSION = '1.07';
9              
10             *_isa_tc = \&Mouse::Util::TypeConstraints::find_or_create_isa_type_constraint;
11             *_does_tc = \&Mouse::Util::TypeConstraints::find_or_create_does_type_constraint;
12              
13             has rules => (
14             is => 'ro',
15             isa => 'ArrayRef',
16             required => 1,
17             );
18              
19 20     20   114 no Mouse;
  20         39  
  20         105  
20              
21             my %rule_attrs = map { $_ => undef }qw(
22             isa does coerce
23             default optional
24             xor
25             documentation
26             );
27              
28             sub BUILDARGS {
29 35     35 1 21006 my($class, @mapping) = @_;
30              
31 35         83 my %xor;
32              
33             my @rules;
34 35         223 while(my($name, $rule_ref) = splice @mapping, 0, 2) {
35 57         97 my %rule;
36 57 100       251 if(!Mouse::Util::TypeConstraints::HashRef($rule_ref)) {
37 22         81 %rule = (isa => $rule_ref);
38             }
39             else {
40 35         45 %rule = %{$rule_ref}
  35         129  
41             }
42              
43             # validate the rule
44 57         101 my $used = 0;
45 57         272 foreach my $attr(keys %rule_attrs) {
46 399 100       849 exists($rule{$attr}) and $used++;
47             }
48 57 100       276 if($used < keys %rule) {
49 1         6 my @unknowns = grep { not exists $rule_attrs{$_} } sort keys %rule;
  2         8  
50 1         10 Carp::croak("Wrong definition for '$name':"
51             . ' Unknown attributes: '
52             . Mouse::Util::quoted_english_list(@unknowns) );
53             }
54              
55             # setup the rule
56 56 100       183 if(defined $rule{xor}) {
57 2         7 my @xors = Mouse::Util::TypeConstraints::ArrayRef($rule{xor})
58 4 100       29 ? @{$rule{xor}}
59             : ($rule{xor});
60 4         16 $xor{$name} = $rule{xor} = \@xors;
61             }
62              
63 56 100       167 if(defined $rule{isa}) {
64 35         209 $rule{type} = _isa_tc(delete $rule{isa});
65             }
66 56 100       745 if(defined $rule{does}) {
67 1 50       242 defined($rule{type})
68             and Carp::croak("Wrong definition for '$name':"
69             . q{ You cannot use 'isa' and 'does' together});
70 0         0 $rule{type} = _does_tc(delete $rule{does});
71             }
72              
73 55 100 100     359 if(defined $rule{type} && not defined $rule{coerce}) {
74 33         167 $rule{coerce} = $rule{type}->has_coercion;
75             }
76              
77 55         114 $rule{name} = $name;
78              
79 55         270 push @rules, \%rule;
80             }
81              
82             # to check xor first and only once, move xor configuration into front rules
83 33 100       138 if(%xor) {
84 4         10 my %byname = map { $_->{name} => $_ } @rules;
  13         41  
85 4         22 while(my($this, $others) = each %xor) {
86 4         22 foreach my $other_name(@{$others}) {
  4         13  
87 8   66     209 my $other_rule = $byname{$other_name}
88             || Carp::croak("Wrong definition for '$this':"
89             . " Unknown parameter name '$other_name'"
90             . " specified as exclusive-or");
91              
92 7   50     9 push @{$other_rule->{xor} ||= []}, $this;
  7         54  
93             }
94             }
95             }
96              
97 32         409 return { rules => \@rules };
98             }
99              
100             sub with {
101 20     20 1 64 my($self, @roles) = @_;
102 20         48 foreach my $role(@roles) {
103 27 50       166 next if ref $role;
104 27         186 $role = Mouse::Util::load_first_existing_class(
105             __PACKAGE__ . '::Role::' . $role,
106             $role,
107             );
108             }
109 20         1562 Mouse::Util::apply_all_roles($self, @roles);
110 20         57498 return $self;
111             }
112              
113             sub find_rule {
114 3     3 1 1279 my($self, $name) = @_;
115 3         5 foreach my $rule(@{$self->rules}) {
  3         13  
116 3 100       22 return $rule if $rule->{name} eq $name;
117             }
118 1         5 return undef;
119             }
120              
121             sub validate {
122 109     109 1 115695 my $self = shift;
123 109         612 my $args = $self->initialize(@_);
124              
125 109         217 my %skip;
126             my @errors;
127 0         0 my @missing;
128 109         165 my $nargs = scalar keys %{$args};
  109         329  
129 109         169 my $used = 0;
130 109         330 my $rules = $self->rules;
131 109         176 RULE: foreach my $rule(@{ $rules }) {
  109         342  
132 205         405 my $name = $rule->{name};
133 205 100       596 next RULE if exists $skip{$name};
134              
135 192 100       870 if(exists $args->{$name}) {
    100          
    100          
136              
137 123 100       406 if(exists $rule->{type}) {
138 97         417 my $err = $self->apply_type_constraint($rule, $args, $name);
139 97 100       791 if($err) {
140 22         82 push @errors, $self->make_error(
141             type => 'InvalidValue',
142             message => $err,
143             name => $name,
144             );
145 22         77 next RULE;
146             }
147             }
148              
149 101 100       305 if($rule->{xor}) {
150             # checks conflicts with exclusive arguments
151 10         18 foreach my $other_name( @{ $rule->{xor} } ) {
  10         25  
152 18 100       48 if(exists $args->{$other_name}) {
153 4         34 push @errors, $self->make_error(
154             type => 'ExclusiveParameter',
155             message => "Exclusive parameters passed together:"
156             . " '$name' v.s. '$other_name'",
157             name => $name,
158             conflict=> $other_name,
159             );
160             }
161 18         52 $skip{$other_name}++;
162             }
163             }
164 101         461 $used++;
165             }
166             elsif(exists $rule->{default}) {
167 28         50 my $default = $rule->{default};
168 28 100       174 $args->{$name} = Mouse::Util::TypeConstraints::CodeRef($default)
169             ? $default->($self, $rule, $args)
170             : $default;
171             }
172             elsif(!$rule->{optional}) {
173 35         101 push @missing, $rule;
174             }
175             }
176              
177              
178 109 100       353 if(@missing) {
179 27         60 MISSING: foreach my $rule(@missing) {
180 35         76 my $name = $rule->{name};
181 35 100       147 next if exists $skip{$name};
182              
183 32         52 my @xors;
184 32 100       128 if($rule->{xor}) {
185 4         7 foreach my $other_name(@{$rule->{xor}}) {
  4         12  
186 4 100       22 next MISSING if exists $args->{$other_name};
187 1         4 push @xors, $other_name;
188             }
189             }
190 29 100       169 my $real_missing = @xors
191             ? sprintf(q{'%s' (or %s)},
192             $name, Mouse::Util::quoted_english_list(@xors) )
193             : sprintf(q{'%s'}, $name);
194 29         206 push @errors, $self->make_error(
195             type => 'MissingParameter',
196             message => "Missing parameter: $real_missing",
197             name => $name,
198             );
199             }
200             }
201              
202              
203 109 100       306 if($used < $nargs) {
204 37         189 my %unknowns = $self->unknown_parameters($rules, $args);
205 37 100       141 if(keys %unknowns) {
206 7         42 foreach my $name( sort keys %unknowns ) {
207 11         51 push @errors, $self->make_error(
208             type => 'UnknownParameter',
209             message => "Unknown parameter: '$name'",
210             name => $name,
211             );
212             }
213             }
214             }
215              
216             # make it restricted
217 109         273 &Internals::SvREADONLY($args, 1);
218              
219 109 100       483 if(@errors) {
220 50         183 $args = $self->found_errors($args, @errors);
221             }
222              
223 64         285 return $args;
224             }
225              
226             __PACKAGE__->meta->add_method( initialize => \&Mouse::Object::BUILDARGS );
227              
228             sub unknown_parameters {
229 37     37 0 74 my($self, $rules, $args) = @_;
230 37         61 my %knowns = map { $_->{name} => undef } @{$rules};
  61         236  
  37         79  
231 73 100       276 return map {
232 37         91 !exists $knowns{$_}
233             ? ($_ => delete $args->{$_})
234             : ()
235 37         75 } keys %{$args};
236             }
237              
238             sub found_errors {
239 45     45 0 104 my($self, $args, @errors) = @_;
240 45         89 my $msg = '';
241 45         81 foreach my $e(@errors) {
242 60         192 $msg .= $e->{message} . "\n";
243             }
244 45         204 $self->throw_error($msg . '... found');
245             }
246              
247             sub make_error {
248 66     66 0 260 my($self, %e) = @_;
249 66         231 return \%e;
250             }
251              
252             sub throw_error {
253 44     44 0 77 my($self, $message) = @_;
254 44         184 local $Carp::CarpLevel = $Carp::CarpLevel + 2; # &throw_error + &validate
255 44         7143 confess($message);
256             }
257              
258             sub apply_type_constraint {
259 97     97 0 173 my($self, $rule, $args, $name) = @_;
260 97         168 my $tc = $rule->{type};
261 97 100       657 return '' if $tc->check($args->{$name});
262              
263 23 100       87 if($rule->{coerce}) {
264 2         13 my $value = $tc->coerce($args->{$name});
265 2 100       101 if($tc->check($value)) {
266 1         3 $args->{$name} = $value; # commit
267 1         3 return '';
268             }
269             }
270              
271 22         166 return "Invalid value for '$rule->{name}': "
272             . $tc->get_message($args->{$name});
273             }
274              
275             __PACKAGE__->meta->make_immutable;
276             __END__