File Coverage

lib/JSV/Compiler.pm
Criterion Covered Total %
statement 384 439 87.4
branch 133 186 71.5
condition 122 195 62.5
subroutine 36 36 100.0
pod 3 3 100.0
total 678 859 78.9


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