File Coverage

lib/JSV/Compilator.pm
Criterion Covered Total %
statement 321 386 83.1
branch 90 144 62.5
condition 60 115 52.1
subroutine 33 34 97.0
pod 3 3 100.0
total 507 682 74.3


line stmt bran cond sub pod time code
1             package JSV::Compilator;
2 8     8   828562 use strict;
  8         61  
  8         215  
3 8     8   44 use warnings;
  8         15  
  8         230  
4 8     8   1359 use Data::Walk;
  8         4449  
  8         419  
5 8     8   1798 use JSON;
  8         36231  
  8         56  
6 8     8   2309 use JSON::Pointer;
  8         44999  
  8         516  
7 8     8   4420 use Path::Tiny;
  8         74871  
  8         481  
8 8     8   66 use Carp;
  8         15  
  8         443  
9 8     8   3708 use Storable 'dclone';
  8         19568  
  8         475  
10 8     8   62 use Data::Dumper;
  8         26  
  8         392  
11 8     8   2718 use Regexp::Common('RE_ALL', 'Email::Address', 'URI', 'time');
  8         16582  
  8         31  
12 8     8   1273848 use Scalar::Util qw(looks_like_number blessed);
  8         17  
  8         29485  
13              
14             our $VERSION = "0.01";
15              
16             sub new {
17 8     8 1 636 my ($class, %args) = @_;
18 8         112 bless {
19             original_schema => {},
20             full_schema => {}
21             }, $class;
22             }
23              
24             sub load_schema {
25 24     24 1 35537 my ($self, $file) = @_;
26 24 50       79 if ('HASH' eq ref $file) {
27 24         72 $self->{original_schema} = $file;
28             } else {
29 0 0       0 croak "Unreadable file" if !-r $file;
30 0 0 0     0 if ($file =~ /\.yaml$/i || $file =~ /\.yml$/i) {
    0          
31 0         0 require YAML::XS;
32 0         0 $self->{original_schema} = YAML::XS::LoadFile($file);
33             } elsif ($file =~ /\.json/i) {
34 0         0 $self->{original_schema} = decode_json(path($file)->slurp_raw);
35             } else {
36 0         0 croak "Unknown file type: must be .json or .yaml";
37             }
38             }
39 24         67 return $self->_resolve_references;
40             }
41              
42             sub _resolve_references {
43 24     24   34 my $self = $_[0];
44 24         1262 $self->{full_schema} = dclone $self->{original_schema};
45             walkdepth(
46             +{
47             wanted => sub {
48 395 0 66 395   12321 if ( ref $_ eq "HASH"
      33        
      33        
49             && exists $_->{'$ref'}
50             && !ref $_->{'$ref'}
51             && keys %$_ == 1)
52             {
53 0         0 my $rv = $_->{'$ref'};
54 0         0 $rv =~ s/.*#//;
55 0         0 my $rp = JSON::Pointer->get($self->{full_schema}, $rv);
56 0 0       0 if ('HASH' eq ref $rp) {
57 0         0 %$_ = %$rp;
58             }
59              
60             }
61             },
62             },
63             $self->{full_schema}
64 24         219 );
65 24         347 return $self;
66             }
67              
68             sub compile {
69 26     26 1 197 my ($self, %opts) = @_;
70 26   50     133 local $self->{coersion} = $opts{coersion} // 0;
71 26   50     109 local $self->{to_json} = $opts{to_json} // 0;
72 26         66 $self->{required_modules} = {};
73 26   50     103 my $input_sym = $opts{input_symbole} // '$_[0]';
74 26         72 my $schema = _norm_schema($self->{full_schema});
75 26   66     90 my $type = $schema->{type} // _guess_schema_type($schema);
76 26   100     123 my $is_required = $opts{is_required} // $type eq 'object' || 0;
77 26         62 my $val_func = "_validate_$type";
78 26         95 my $val_expr = $self->$val_func($input_sym, $schema, "", $is_required);
79 26         108 return $val_expr;
80             }
81              
82             # type: six primitive types ("null", "boolean", "object", "array", "number", or "string"), or "integer"
83              
84             sub _sympt_to_path {
85 0     0   0 my ($sympt) = @_;
86 0         0 $sympt =~ s!^[^\{\}]+!/!;
87 0         0 $sympt =~ s![\{\}]+!/!g;
88 0         0 $sympt;
89             }
90              
91             sub _norm_schema {
92 211     211   257 my $shmpt = $_[0];
93             return +{
94 211 100       423 type => _guess_schema_type($shmpt),
95             const => $shmpt
96             } if 'HASH' ne ref $shmpt;
97 185         263 $shmpt;
98             }
99              
100             sub _guess_schema_type {
101 68     68   81 my $shmpt = $_[0];
102 68 100       171 if (my $class = blessed($shmpt)) {
103 26 50       95 if ($class =~ /bool/i) {
104 26         89 return 'boolean';
105             } else {
106 0         0 return 'object';
107             }
108             }
109 42 50       91 if ('HASH' ne ref $shmpt) {
110 0 0       0 return 'number' if looks_like_number($shmpt);
111 0         0 return 'string';
112             }
113 42 50       74 return $shmpt->{type} if $shmpt->{type};
114             return 'object'
115             if defined $shmpt->{additionalProperties}
116             or $shmpt->{patternProperties}
117             or $shmpt->{properties}
118             or defined $shmpt->{minProperties}
119 42 50 33     295 or defined $shmpt->{maxProperties};
      66        
      66        
      66        
120             return 'array'
121             if defined $shmpt->{additionalItems}
122             or defined $shmpt->{uniqueItems}
123             or $shmpt->{items}
124             or defined $shmpt->{minItems}
125 36 50 33     239 or defined $shmpt->{maxItems};
      33        
      33        
      33        
126             return 'number'
127             if defined $shmpt->{minimum}
128             or defined $shmpt->{maximum}
129             or defined $shmpt->{exclusiveMinimum}
130             or defined $shmpt->{exclusiveMaximum}
131 36 50 100     242 or defined $shmpt->{multipleOf};
      66        
      66        
      33        
132 32         75 return 'string';
133             }
134              
135             sub _quote_var {
136 77     77   98 my $s = $_[0];
137 77         242 my $d = Data::Dumper->new([$s]);
138 77         1558 $d->Terse(1);
139 77         382 my $qs = $d->Dump;
140 77 50       933 substr($qs, -1, 1, '') if substr($qs, -1, 1) eq "\n";
141 77         336 return $qs;
142             }
143              
144             #<<<
145             my %formats = (
146             'date-time' => $RE{time}{iso},
147             email => $RE{Email}{Address},
148             uri => $RE{URI},
149             hostname => '(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*'
150             . '(?:[a-zA-Z][-a-zA-Z0-9]*[a-zA-Z0-9]|[a-zA-Z])[.]?)',
151             ipv4 => $RE{net}{IPv4},
152             ipv6 => $RE{net}{IPv6},
153             );
154             #>>>
155              
156             sub _validate_null {
157 1     1   3 my ($self, $sympt, $schmptm, $path) = @_;
158 1         5 my @sp = split /->/, $sympt;
159 1         3 my $el = pop @sp;
160 1         3 my $sh = join "->", @sp;
161 1 50       5 my $ec = $sh ? "|| ('HASH' eq ref($sh) && !exists ($sympt))" : '';
162 1         6 return "push \@\$errors, \"$path must be null\" if defined($sympt) $ec;\n";
163             }
164              
165             sub _validate_boolean {
166 28     28   56 my ($self, $sympt, $schmpt, $path, $is_required) = @_;
167 28         43 $schmpt = _norm_schema($schmpt);
168 28         42 my $r = '';
169 28 50       53 if (defined $schmpt->{default}) {
170 0         0 my $val = _quote_var($schmpt->{default});
171 0         0 $r = "$sympt = $val if not defined $sympt;\n";
172             }
173 28         58 $r .= "if(defined($sympt)) {\n";
174 28         57 $r .= $self->_validate_schemas_array($sympt, $schmpt, $path);
175 28 100       54 if (defined $schmpt->{const}) {
176 26         41 $r .= " { no warnings 'uninitialized';\n";
177 26 100       71 my $not = $schmpt->{const} ? 'not' : "";
178 26         195 $r .= " push \@\$errors, \"$path must be \".($schmpt->{const}?'true':'false') if $not $sympt \n";
179 26         127 $r .= " }\n";
180             }
181 28 50       69 if ($self->{to_json}) {
    50          
182 0         0 $r = " $sympt = (($sympt)? \\1: \\0);\n";
183             } elsif ($self->{coersion}) {
184 0         0 $r = " $sympt = (($sympt)? 1: 0);\n";
185             }
186 28         40 $r .= "}\n";
187 28 100       41 if ($is_required) {
188 26         62 $r .= "else {\n";
189 26         38 $r .= " push \@\$errors, \"$path is required\";\n";
190 26         40 $r .= "}\n";
191             }
192 28         51 return $r;
193             }
194              
195             sub _validate_string {
196 60     60   136 my ($self, $sympt, $schmpt, $path, $is_required) = @_;
197 60         92 $schmpt = _norm_schema($schmpt);
198 60         86 my $r = '';
199 60 50       116 if (defined $schmpt->{default}) {
200 0         0 my $val = _quote_var($schmpt->{default});
201 0         0 $r = "$sympt = $val if not defined $sympt;\n";
202             }
203 60         123 $r .= "if(defined($sympt)) {\n";
204 60         138 $r .= $self->_validate_schemas_array($sympt, $schmpt, $path);
205 60 100       126 if (defined $schmpt->{maxLength}) {
206 2         6 $r .= " push \@\$errors, '$path length must be not greater than ";
207 2         9 $r .= "$schmpt->{maxLength}' if length($sympt) > $schmpt->{maxLength};\n";
208             }
209 60 100       121 if (defined $schmpt->{minLength}) {
210 2         6 $r .= " push \@\$errors, '$path length must be not less than ";
211 2         8 $r .= "$schmpt->{minLength}' if length($sympt) < $schmpt->{minLength};\n";
212             }
213 60 50       107 if (defined $schmpt->{const}) {
214 0         0 my $val = _quote_var($schmpt->{const});
215 0         0 $r .= " push \@\$errors, \"$path must be $schmpt->{const}\" if $sympt ne $val;\n";
216             }
217 60 100       121 if (defined $schmpt->{pattern}) {
218 6         8 my $pattern = $schmpt->{pattern};
219 6         14 $pattern =~ s/\\Q(.*?)\\E/quotemeta($1)/eg;
  0         0  
220 6         8 $pattern =~ s/\\Q(.*)$/quotemeta($1)/eg;
  0         0  
221 6         7 $pattern =~ s/"/\\"/g;
222 6         15 $pattern =~ s|/|\\/|g;
223 6         16 $r .= " push \@\$errors, \"$path does not match pattern\" if $sympt !~ /$pattern/;\n";
224             }
225 60 50 66     156 if ($schmpt->{enum} && 'ARRAY' eq ref($schmpt->{enum}) && @{$schmpt->{enum}}) {
  10   66     25  
226 10         12 my $can_list = join ", ", map {_quote_var($_)} @{$schmpt->{enum}};
  14         22  
  10         19  
227 10         22 $self->{required_modules}{'List::Util'}{none} = 1;
228 10         24 $r .= " push \@\$errors, \"$path must be on of $can_list\" if none {\$_ eq $sympt} ($can_list);\n";
229             }
230 60 100 66     183 if ($schmpt->{format} && $formats{$schmpt->{format}}) {
231 6         1214 $r .= " push \@\$errors, \"$path does not match format $schmpt->{format}\"";
232 6         15 $r .= " if $sympt !~ /^$formats{$schmpt->{format}}\$/;\n";
233             }
234 60 50 33     485 if ($self->{to_json} || $self->{coersion}) {
235 0         0 $r .= " $sympt = \"$sympt\";\n";
236             }
237 60         84 $r .= "}\n";
238 60 100       102 if ($is_required) {
239 32         39 $r .= "else {\n";
240 32         62 $r .= " push \@\$errors, \"$path is required\";\n";
241 32         41 $r .= "}\n";
242             }
243 60         161 return $r;
244             }
245              
246             sub _validate_any_number {
247 14     14   515 my ($self, $sympt, $schmpt, $path, $is_required, $re, $ntype) = @_;
248 14         214 $schmpt = _norm_schema($schmpt);
249 14         43 my $r = '';
250 14   100     52 $ntype ||= '';
251 14 50       37 if (defined $schmpt->{default}) {
252 0         0 my $val = _quote_var($schmpt->{default});
253 0         0 $r = "$sympt = $val if not defined $sympt;\n";
254             }
255 14         52 $r .= "if(defined($sympt)) {\n";
256 14         44 $r .= $self->_validate_schemas_array($sympt, $schmpt, $path);
257 14         26 $r .= " {\n";
258 14         58 $r .= " if($sympt !~ /^$re\$/){ push \@\$errors, '$path does not look like $ntype number'; last }\n";
259 14 100       1758 if (defined $schmpt->{minimum}) {
260 5         22 $r .= " push \@\$errors, '$path must be not less than $schmpt->{minimum}'";
261 5         16 $r .= " if $sympt < $schmpt->{minimum};\n";
262             }
263 14 100       43 if (defined $schmpt->{exclusiveMinimum}) {
264 1         5 $r .= " push \@\$errors, '$path must be greater than $schmpt->{exclusiveMinimum}'";
265 1         4 $r .= " if $sympt <= $schmpt->{exclusiveMinimum};\n";
266             }
267 14 100       32 if (defined $schmpt->{maximum}) {
268 3         11 $r .= " push \@\$errors, '$path must be not greater than $schmpt->{maximum}'";
269 3         9 $r .= " if $sympt > $schmpt->{maximum};\n";
270             }
271 14 50       37 if (defined $schmpt->{exclusiveMaximum}) {
272 0         0 $r .= " push \@\$errors, '$path must be less than $schmpt->{exclusiveMaximum}'";
273 0         0 $r .= " if $sympt >= $schmpt->{exclusiveMaximum};\n";
274             }
275 14 50       31 if (defined $schmpt->{const}) {
276 0         0 $r .= " push \@\$errors, '$path must be $schmpt->{const}' if $sympt != $schmpt->{const};\n";
277             }
278 14 50       39 if ($schmpt->{multipleOf}) {
279 0         0 $self->{required_modules}{'POSIX'}{floor} = 1;
280 0         0 $r .= " push \@\$errors, '$path must be multiple of $schmpt->{multipleOf}'";
281 0         0 $r .= " if $sympt / $schmpt->{multipleOf} != floor($sympt / $schmpt->{multipleOf});\n";
282             }
283 14 0 33     43 if ($schmpt->{enum} && 'ARRAY' eq ref($schmpt->{enum}) && @{$schmpt->{enum}}) {
  0   33     0  
284 0         0 my $can_list = join ", ", map {_quote_var($_)} @{$schmpt->{enum}};
  0         0  
  0         0  
285 0         0 $self->{required_modules}{'List::Util'}{none} = 1;
286 0         0 $r .= " push \@\$errors, '$path must be on of $can_list' if none {$_ == $sympt} ($can_list);\n";
287             }
288 14 50 33     42 if ($schmpt->{format} && $formats{$schmpt->{format}}) {
289 0         0 $r .= " push \@\$errors, '$path does not match format $schmpt->{format}'";
290 0         0 $r .= " if $sympt !~ /^$formats{$schmpt->{format}}\$/;\n";
291             }
292 14 50 33     72 if ($self->{to_json} || $self->{coersion}) {
293 0         0 $r .= " $sympt += 0;\n";
294             }
295 14         28 $r .= "} }\n";
296 14 50       28 if ($is_required) {
297 14         24 $r .= "else {\n";
298 14         33 $r .= " push \@\$errors, \"$path is required\";\n";
299 14         22 $r .= "}\n";
300             }
301 14         65 return $r;
302              
303             }
304              
305             sub _validate_number {
306 5     5   18 my ($self, $sympt, $schmpt, $path, $is_required) = @_;
307 5         31 return $self->_validate_any_number($sympt, $schmpt, $path, $is_required, $RE{num}{real});
308             }
309              
310             sub _validate_integer {
311 9     9   26 my ($self, $sympt, $schmpt, $path, $is_required) = @_;
312 9         72 return $self->_validate_any_number($sympt, $schmpt, $path, $is_required, $RE{num}{int}, "integer");
313             }
314              
315             sub _make_schemas_array {
316 26     26   69 my ($self, $schemas, $rpath, $type) = @_;
317 26 100       66 $schemas = [$schemas] if 'ARRAY' ne ref $schemas;
318 26         39 my @tfa;
319 26         33 for my $schm (@{$schemas}) {
  26         52  
320 57         98 my $subschm = _norm_schema($schm);
321 57   100     167 my $stype = $subschm->{type} // $type // _guess_schema_type($schm);
      66        
322 57         95 my $val_func = "_validate_$stype";
323 57         188 my $ivf = $self->$val_func("\$_[0]", $subschm, "$rpath", "required");
324 57         207 push @tfa, " sub {my \$errors = []; $ivf; \@\$errors == 0}\n";
325             }
326 26         175 return "(" . join(",\n", @tfa) . ")";
327             }
328              
329             sub _validate_all_of {
330 6     6   10 my ($self, $schmpt, $sympt, $rpath) = @_;
331 6         7 my $r = '';
332 6         15 $self->{required_modules}{'List::Util'}{notall} = 1;
333 6         16 $r .= " { my \@allOf = " . $self->_make_schemas_array($schmpt->{allOf}, $rpath, $schmpt->{type}) . ";\n";
334 6         16 $r .= " my \$stored_arg = ${sympt};\n";
335 6         15 $r .= " push \@\$errors, \"$rpath doesn't match all required schemas\" "
336             . "if notall { \$_->(\$stored_arg, \"$rpath\") } \@allOf;\n";
337 6         7 $r .= " }\n";
338 6         15 return $r;
339             }
340              
341             sub _validate_any_of {
342 5     5   11 my ($self, $schmpt, $sympt, $rpath) = @_;
343 5         9 my $r = '';
344 5         13 $self->{required_modules}{'List::Util'}{none} = 1;
345 5         23 $r .= " { my \@anyOf = " . $self->_make_schemas_array($schmpt->{anyOf}, $rpath, $schmpt->{type}) . ";\n";
346 5         14 $r .= " my \$stored_arg = ${sympt};\n";
347 5         14 $r .= " push \@\$errors, \"$rpath doesn't match any required schema\""
348             . " if none { \$_->(\$stored_arg, \"$rpath\") } \@anyOf;\n";
349 5         7 $r .= " }\n";
350 5         21 return $r;
351             }
352              
353             sub _validate_one_of {
354 10     10   21 my ($self, $schmpt, $sympt, $rpath) = @_;
355 10         15 my $r = '';
356 10         29 $r .= " { my \@oneOf = " . $self->_make_schemas_array($schmpt->{oneOf}, $rpath, $schmpt->{type}) . ";\n";
357 10         25 $r .= " my \$stored_arg = ${sympt};\n";
358 10         19 $r .= " my \$m = 0; for my \$t (\@oneOf) { ++\$m if \$t->(\$stored_arg, \"$rpath\"); last if \$m > 1; }\n";
359 10         19 $r .= " push \@\$errors, \"$rpath doesn't match exactly one required schema\" if \$m != 1;\n";
360 10         16 $r .= " }\n";
361 10         63 return $r;
362             }
363              
364             sub _validate_not_of {
365 5     5   12 my ($self, $schmpt, $sympt, $rpath) = @_;
366 5         7 my $r = '';
367 5         15 $self->{required_modules}{'List::Util'}{any} = 1;
368 5         19 $r .= " { my \@notOf = " . $self->_make_schemas_array($schmpt->{not}, $rpath, $schmpt->{type}) . ";\n";
369 5         13 $r .= " my \$stored_arg = ${sympt};\n";
370 5         13 $r .= " push \@\$errors, \"$rpath matches a schema when must not\" "
371             . " if any { \$_->(\$stored_arg, \"$rpath\") } \@notOf;\n";
372 5         6 $r .= " }\n";
373 5         10 return $r;
374             }
375              
376             sub _validate_schemas_array {
377 128     128   209 my ($self, $sympt, $schmpt, $path) = @_;
378 128 100       234 my $rpath = !$path ? "(object)" : $path;
379 128         153 my $r = '';
380 128 100       224 $r .= $self->_validate_any_of($schmpt, $sympt, $rpath) if defined $schmpt->{anyOf};
381 128 100       213 $r .= $self->_validate_all_of($schmpt, $sympt, $rpath) if defined $schmpt->{allOf};
382 128 100       242 $r .= $self->_validate_one_of($schmpt, $sympt, $rpath) if defined $schmpt->{oneOf};
383 128 100       214 $r .= $self->_validate_not_of($schmpt, $sympt, $rpath) if defined $schmpt->{not};
384 128         235 return $r;
385             }
386              
387             sub _validate_object {
388 23     23   62 my ($self, $sympt, $schmpt, $path, $is_required) = @_;
389 23         39 $schmpt = _norm_schema($schmpt);
390 23 100       55 my $rpath = !$path ? "(object)" : $path;
391 23 100       55 my $ppref = $path ? "$path/" : "";
392 23         30 my $r = '';
393 23 50       49 if ($schmpt->{default}) {
394 0         0 my $val = _quote_var($schmpt->{default});
395 0         0 $r = " $sympt = $val if not defined $sympt;\n";
396             }
397 23         55 $r .= "if('HASH' eq ref($sympt)) {\n";
398 23         61 $r .= $self->_validate_schemas_array($sympt, $schmpt, $path);
399 23 100 66     96 if ($schmpt->{properties} && 'HASH' eq ref $schmpt->{properties}) {
400 21         33 my %required;
401 21 100 66     75 if ($schmpt->{required} && 'ARRAY' eq ref $schmpt->{required}) {
402 19         26 %required = map {$_ => 1} @{$schmpt->{required}};
  31         78  
  19         38  
403             }
404 21         30 for my $k (keys %{$schmpt->{properties}}) {
  21         54  
405 41         58 my $type = 'string';
406 41 50       97 if ('HASH' eq ref $schmpt->{properties}{$k}) {
407 41   66     107 $type = $schmpt->{properties}{$k}{type} // _guess_schema_type($schmpt->{properties}{$k});
408             }
409 41         79 my $val_func = "_validate_$type";
410 41         76 my $qk = _quote_var($k);
411 41         230 $r .= $self->$val_func("${sympt}->{$qk}", $schmpt->{properties}{$k}, "$ppref$k", $required{$k});
412             }
413             }
414 23 50       67 if (defined $schmpt->{minProperties}) {
415 0         0 $schmpt->{minProperties} += 0;
416 0         0 $r .= " push \@\$errors, '$rpath must contain not less than $schmpt->{minProperties} properties'";
417 0         0 $r .= " if keys %{$sympt} < $schmpt->{minProperties};\n";
418             }
419 23 50       52 if (defined $schmpt->{maxProperties}) {
420 0         0 $schmpt->{maxProperties} += 0;
421 0         0 $r .= " push \@\$errors, '$rpath must contain not more than $schmpt->{maxProperties} properties'";
422 0         0 $r .= " if keys %{$sympt} > $schmpt->{minProperties};\n";
423             }
424 23         43 my @pt;
425 23 100       46 if (defined $schmpt->{patternProperties}) {
426 2         3 for my $pt (keys %{$schmpt->{patternProperties}}) {
  2         7  
427 2         3 my $type;
428 2   66     8 $type = $schmpt->{patternProperties}{$pt}{type} // _guess_schema_type($schmpt->{patternProperties}{$pt});
429 2         5 my $val_func = "_validate_$type";
430 2         7 (my $upt = $pt) =~ s/"/\\"/g;
431 2         4 $upt =~ s/\\Q(.*?)\\E/quotemeta($1)/eg;
  0         0  
432 2         4 $upt =~ s/\\Q(.*)$/quotemeta($1)/eg;
  0         0  
433 2         8 $upt =~ s|/|\\/|g;
434 2         4 push @pt, $upt;
435 2         8 my $ivf = $self->$val_func("\$_[0]", $schmpt->{patternProperties}{$pt}, "\$_[1]", "required");
436 2         7 $r .= " { my \@props = grep {/$upt/} keys %{${sympt}};";
437              
438 2 50 33     17 if ($schmpt->{properties} && 'HASH' eq ref $schmpt->{properties}) {
439 2         4 my %apr = map {_quote_var($_) => undef} keys %{$schmpt->{properties}};
  2         4  
  2         7  
440 2         9 $r .= " my %defined_props = (" . join(", ", map {$_ => "undef"} keys %apr) . ");\n";
  2         10  
441 2         7 $r .= " \@props = grep {!exists \$defined_props{\$_} } \@props;\n";
442             }
443 2         26 $r .= " my \$tf = sub { $ivf };\n";
444 2         13 $r .= " for my \$prop (\@props) {\n";
445 2         6 $r .= " \$tf->(${sympt}->{\$prop}, \"$ppref\${prop}\");\n";
446 2         4 $r .= " };\n";
447 2         5 $r .= " }\n";
448             }
449             }
450 23 100       50 if (defined $schmpt->{additionalProperties}) {
451 10 50 33     36 if (!ref($schmpt->{additionalProperties}) && !$schmpt->{additionalProperties}) {
452 10         12 my %apr;
453 10         16 $r .= " {\n";
454 10 50 33     37 if ($schmpt->{properties} && 'HASH' eq ref $schmpt->{properties}) {
455 10         17 %apr = map {_quote_var($_) => undef} keys %{$schmpt->{properties}};
  20         28  
  10         26  
456 10         29 $r .= " my %allowed_props = (" . join(", ", map {$_ => "undef"} keys %apr) . ");\n";
  20         50  
457 10         36 $r .= " my \@unallowed_props = grep {!exists \$allowed_props{\$_} } keys %{${sympt}};\n";
458 10 100       20 if (@pt) {
459 2         5 $r .= " \@unallowed_props = grep { " . join("&&", map {"!/$_/"} @pt) . " } \@unallowed_props;\n";
  2         9  
460             }
461 10         19 $r .= " push \@\$errors, \"$rpath contains not allowed properties: \@unallowed_props\" ";
462 10         14 $r .= " if \@unallowed_props;\n";
463             } else {
464 0         0 $r .= " push \@\$errors, \"$rpath can't contain properties\" if %{${sympt}};\n";
465             }
466 10         27 $r .= " }\n";
467             }
468             }
469 23         31 $r .= "}\n";
470 23 50       54 if ($is_required) {
471 23         46 $r .= "else {\n";
472 23         42 $r .= " push \@\$errors, \"$rpath is required\";\n";
473 23         30 $r .= "}\n";
474             }
475 23         103 return $r;
476             }
477              
478             sub _validate_array {
479 3     3   14 my ($self, $sympt, $schmpt, $path, $is_required) = @_;
480 3         9 $schmpt = _norm_schema($schmpt);
481 3 50       8 my $rpath = !$path ? "(object)" : $path;
482 3         5 my $r = '';
483 3 50       8 if ($schmpt->{default}) {
484 0         0 my $val = _quote_var($schmpt->{default});
485 0         0 $r = " $sympt = $val if not defined $sympt;\n";
486             }
487 3         9 $r .= "if('ARRAY' eq ref($sympt)) {\n";
488 3         8 $r .= $self->_validate_schemas_array($sympt, $schmpt, $path);
489 3 50       8 if (defined $schmpt->{minItems}) {
490 3         11 $r .= " push \@\$errors, '$path must contain not less than $schmpt->{minItems} items'";
491 3         10 $r .= " if \@{$sympt} < $schmpt->{minItems};\n";
492             }
493 3 50       7 if (defined $schmpt->{maxItems}) {
494 0         0 $r .= " push \@\$errors, '$path must contain not more than $schmpt->{maxItems} items'";
495 0         0 $r .= " if \@{$sympt} > $schmpt->{maxItems};\n";
496             }
497 3 50       8 if (defined $schmpt->{uniqueItems}) {
498 3         5 $r .= " { my %seen;\n";
499 3         11 $r .= " for (\@{$sympt}) {\n";
500 3         9 $r .= " if(\$seen{\$_}) { push \@\$errors, '$path must contain only unique items'; last }\n";
501 3         5 $r .= " \$seen{\$_} = 1;\n";
502 3         5 $r .= " };\n";
503 3         4 $r .= " }\n";
504             }
505 3 50       8 if ($schmpt->{items}) {
506 3   33     10 my $type = $schmpt->{items}{type} // _guess_schema_type($schmpt->{items});
507 3         5 my $val_func = "_validate_$type";
508 3         12 my $ivf = $self->$val_func("\$_[0]", $schmpt->{items}, "$path/[]", $is_required);
509 3         9 $r .= " { my \$tf = sub { $ivf };\n";
510 3         8 $r .= " \$tf->(\$_, \"$rpath\") for (\@{$sympt});\n";
511 3         5 $r .= " }\n";
512             }
513 3         6 $r .= "}\n";
514 3 50       8 if ($is_required) {
515 0 0       0 $path = "array" if $path eq "";
516 0         0 $r .= "else {\n";
517 0         0 $r .= " push \@\$errors, \"$path is required\";\n";
518 0         0 $r .= "}\n";
519             }
520 3         14 return $r;
521             }
522              
523             1;
524              
525             __END__