File Coverage

blib/lib/JSON/SchemaValidator.pm
Criterion Covered Total %
statement 186 461 40.3
branch 91 344 26.4
condition 18 60 30.0
subroutine 27 38 71.0
pod 0 3 0.0
total 322 906 35.5


line stmt bran cond sub pod time code
1             package JSON::SchemaValidator;
2              
3 3     3   231514 use strict;
  3         38  
  3         91  
4 3     3   15 use warnings;
  3         7  
  3         130  
5              
6             our $VERSION = '1.04';
7              
8 3     3   19 use B ();
  3         7  
  3         41  
9 3     3   1905 use Storable ();
  3         9132  
  3         101  
10             require Carp;
11 3     3   1695 use Time::Piece;
  3         32762  
  3         15  
12              
13 3     3   1549 use JSON::SchemaValidator::Result;
  3         9  
  3         109  
14 3     3   1230 use JSON::SchemaValidator::Pointer qw(pointer);
  3         7  
  3         18430  
15              
16             my $DATETIME_RE = qr/
17             ^
18             [0-9]{4}\-[0-9]{2}\-[0-9]{2}T[0-9]{2}
19             :
20             [0-9]{2}:[0-9]{2}
21             (?:\.[0-9]{1,6})?
22             (?:
23             Z
24             |
25             [-+][0-9]{2}:[0-9]{2}
26             )
27             $
28             /ix;
29              
30             my $HOSTNAME_RE = qr/
31             (?:
32             (?:[a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.
33             )*
34             (?:[a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])
35             /ix;
36              
37             my $EMAIL_RE = qr/
38             [a-z0-9\._\%\+!\$\&\*=\^\|\~#%\{\}\/\-]+
39             \@
40             $HOSTNAME_RE
41             /ix;
42              
43             sub new {
44 1     1 0 111 my $class = shift;
45 1         4 my (%params) = @_;
46              
47 1         2 my $self = {};
48 1         3 bless $self, $class;
49              
50             $self->{formats} = {
51             hostname => sub {
52 0     0   0 my ($hostname) = @_;
53              
54 0 0       0 return 0 if length $hostname > 255;
55              
56 0 0       0 return 0 unless $hostname =~ qr/^$HOSTNAME_RE$/;
57              
58 0 0       0 return 0 if grep { length > 63 } split /\./, $hostname;
  0         0  
59              
60 0         0 return 1;
61             },
62             email => sub {
63 0     0   0 my ($email) = @_;
64              
65 0 0       0 return 0 unless $email =~ m/^$EMAIL_RE$/;
66              
67 0         0 my ($username, $hostname) = split /@/, $email;
68              
69 0 0       0 return 0 if length $hostname > 255;
70 0 0       0 return 0 if grep { length > 63 } split /\./, $hostname;
  0         0  
71              
72 0         0 return 1;
73             },
74             ipv4 => sub {
75 0     0   0 my ($ipv4) = @_;
76              
77 0         0 my @parts = split m/\./, $ipv4;
78              
79 0 0 0     0 return unless @parts > 0 && @parts < 5;
80              
81 0         0 for my $part (@parts) {
82 0 0 0     0 return unless $part =~ m/^[0-9]+$/ && $part >= 0 && $part < 256;
      0        
83             }
84              
85 0 0       0 return unless $parts[-1] > 0;
86              
87 0         0 return 1;
88             },
89             ipv6 => sub {
90 0     0   0 my ($ipv6) = @_;
91              
92 0         0 my @parts = split m/\:/, $ipv6;
93              
94 0 0 0     0 return unless @parts > 0 && @parts < 9;
95              
96 0         0 for my $part (@parts) {
97 0 0       0 next if $part eq '';
98              
99 0 0       0 return unless $part =~ m/^[0-9a-f]{1,4}$/i;
100             }
101              
102 0         0 return 1;
103             },
104             'date-time' => sub {
105 0     0   0 my ($date_time) = @_;
106              
107 0 0       0 return unless $date_time =~ $DATETIME_RE;
108              
109 0         0 $date_time =~ s{\.[0-9]*}{};
110 0         0 $date_time =~ s{Z$}{+00:00}i;
111 0         0 $date_time =~ s{:([0-9]+)$}{$1}i;
112              
113 0 0       0 return unless eval { Time::Piece->strptime(uc($date_time), '%Y-%m-%dT%T%z') };
  0         0  
114              
115 0         0 return 1;
116             }
117 1         23 };
118 1         4 $self->{fetcher} = $params{fetcher};
119              
120 1         4 return $self;
121             }
122              
123 0     0 0 0 sub formats { shift->{formats} }
124              
125             sub validate {
126 2     2 0 4394 my $self = shift;
127 2         6 my ($json, $schema) = @_;
128              
129 2         233 $schema = Storable::dclone($schema);
130              
131 2         13 my $context = {
132             root => $schema,
133             ids => {},
134             pointer => '#',
135             };
136              
137 2         9 $self->_collect_ids($context, $schema);
138              
139 2         10 my $result = $self->_validate($context, $json, $schema);
140              
141 2         11 return $result;
142             }
143              
144             sub _collect_ids {
145 16     16   23 my $self = shift;
146 16         30 my ($context, $schema) = @_;
147              
148 16 100       26 if (_is_object($schema)) {
    100          
149 7         22 my $new_context = {%$context};
150              
151 7 50 33     19 if ($schema->{id} && _is_string($schema->{id})) {
152 0         0 my $base_url = $context->{base_url};
153 0         0 my $path = $context->{path};
154              
155 0         0 my $id = $schema->{id};
156              
157 0 0       0 if ($id =~ m/^http/) {
158 0         0 ($base_url) = $id =~ m/^([^#]+)/;
159 0         0 $path = undef;
160              
161 0 0       0 if ($base_url !~ m{/$}) {
162 0         0 ($path) = $base_url =~ m{([^\/]+)$};
163 0         0 $base_url =~ s{[^\/]+$}{};
164             }
165              
166 0         0 $base_url =~ s{/$}{};
167             }
168             else {
169 0 0       0 if ($id !~ m/^#/) {
    0          
170 0 0       0 if ($id =~ m{/$}) {
171 0         0 $base_url .= "/$id";
172 0         0 $base_url =~ s{/$}{};
173 0         0 $path = undef;
174              
175 0         0 $id = "$base_url/";
176             }
177             else {
178 0         0 $path = $id;
179              
180 0 0       0 if ($base_url) {
181 0         0 $id = "$base_url/$id";
182             }
183             }
184             }
185             elsif ($path) {
186 0         0 $id = "$path$id";
187              
188 0 0       0 if ($base_url) {
189 0         0 $id = "$base_url/$id";
190             }
191             }
192             }
193              
194 0         0 $context->{ids}->{$id} = $schema;
195              
196 0         0 $new_context->{base_url} = $base_url;
197 0         0 $new_context->{path} = $path;
198             }
199              
200 7 50 33     16 if ($schema->{'$ref'} && _is_string($schema->{'$ref'})) {
201 0         0 my $ref = $schema->{'$ref'};
202              
203 0 0       0 if ($ref !~ m/^http/) {
204 0 0       0 if ($ref =~ m/^#/) {
205 0 0       0 if (my $path = $new_context->{path}) {
206 0         0 $ref = "$path$ref";
207             }
208             }
209              
210 0 0       0 if (my $base_url = $new_context->{base_url}) {
211 0         0 $ref = "$base_url/$ref";
212             }
213              
214 0         0 $schema->{'$ref'} = $ref;
215             }
216             }
217              
218 7         17 foreach my $key (keys %$schema) {
219 10         28 $self->_collect_ids($new_context, $schema->{$key});
220             }
221             }
222             elsif (_is_array($schema)) {
223 3         7 foreach my $el (@$schema) {
224 4         13 $self->_collect_ids($context, $el);
225             }
226             }
227             }
228              
229             sub _resolve_refs {
230 7     7   11 my $self = shift;
231 7         11 my ($context, $schema) = @_;
232              
233 7 100       13 if (_is_object($schema)) {
    50          
234 6 50 33     19 if ($schema->{'$ref'} && _is_string($schema->{'$ref'})) {
235 0         0 my $ref = delete $schema->{'$ref'};
236              
237 0         0 my $subschema;
238 0 0       0 if (exists $context->{ids}->{$ref}) {
239 0         0 $subschema = $context->{ids}->{$ref};
240             }
241             else {
242 0 0       0 if ($ref !~ m/^http/) {
243 0 0       0 if ($ref =~ m/^#/) {
244 0 0       0 if ($context->{path}) {
245 0         0 $ref = "$context->{path}/$ref";
246             }
247             }
248              
249 0 0       0 if ($context->{base_url}) {
250 0         0 $ref = "$context->{base_url}/$ref";
251             }
252             }
253              
254 0 0       0 if (exists $context->{ids}->{$ref}) {
    0          
255 0         0 $subschema = $context->{ids}->{$ref};
256             }
257             elsif ($ref =~ m/^http/) {
258 0         0 $subschema = $self->_resolve_remote_ref($context, $ref);
259             }
260             else {
261 0         0 $subschema = pointer($context->{root}, $ref);
262             }
263             }
264              
265 0 0       0 if ($subschema) {
266 0         0 for my $key (keys %$schema) {
267 0 0       0 next if $key eq 'definitions';
268              
269 0         0 delete $schema->{$key};
270             }
271              
272 0         0 foreach my $key (keys %$subschema) {
273 0 0       0 next if $key eq 'id';
274              
275 0         0 $schema->{$key} = $subschema->{$key};
276             }
277              
278 0 0       0 if ($schema->{'$ref'}) {
279 0         0 $self->_resolve_refs($context, $schema);
280             }
281             }
282             }
283             }
284             elsif (_is_array($schema)) {
285 1         3 foreach my $el (@$schema) {
286 2         9 $self->_resolve_refs($context, $el);
287             }
288             }
289             }
290              
291             sub _validate {
292 4     4   8 my $self = shift;
293 4         8 my ($context, $json, $schema) = @_;
294              
295 4         8 my $pointer = $context->{pointer};
296              
297 4         11 my $result = $self->_build_result;
298              
299 4         13 $self->_resolve_refs($context, $schema);
300              
301 4 50       8 if (_is_object($schema)) {
302 4 100       11 if (my $types = $schema->{type}) {
303 2         7 my $subresult = $self->_validate_type($context, $json, $types);
304 2         6 $result->merge($subresult);
305             }
306              
307 4 50       13 if (my $enum = $schema->{enum}) {
308 0         0 my $subresult = $self->_validate_enum($context, $json, $enum);
309 0         0 $result->merge($subresult);
310             }
311              
312 4 50       10 if (exists $schema->{const}) {
313 0         0 my $subresult = $self->_validate_const($context, $json, $schema->{const});
314 0         0 $result->merge($subresult);
315             }
316             }
317              
318 4 100       10 if (_is_object($json)) {
    50          
    0          
    0          
319 3         9 my $subresult = $self->_validate_object($context, $json, $schema);
320 3         10 $result->merge($subresult);
321             }
322             elsif (_is_array($json)) {
323 1         5 my $subresult = $self->_validate_array($context, $json, $schema);
324 1         3 $result->merge($subresult);
325             }
326             elsif (_is_number($json)) {
327 0         0 my $subresult = $self->_validate_number($context, $json, $schema);
328 0         0 $result->merge($subresult);
329             }
330             elsif (_is_string($json)) {
331 0         0 my $subresult = $self->_validate_string($context, $json, $schema);
332 0         0 $result->merge($subresult);
333             }
334              
335 4 100       11 if (my $subschema_type = _subschema($schema)) {
336 1         5 $self->_resolve_refs($context, $schema->{$subschema_type});
337              
338 1         9 my $subresult = $self->_validate_subschemas($context, $json, $subschema_type, $schema->{$subschema_type});
339 1         3 $result->merge($subresult);
340             }
341              
342 4 50       10 if (_is_string($json)) {
343 0 0       0 if (my $format = $schema->{format}) {
344 0 0       0 if (my $cb = $self->{formats}->{$format}) {
345 0 0       0 if (!$cb->($json)) {
346 0         0 $result->add_error(
347             uri => $pointer,
348             message => 'Must be of format ' . $format,
349             attribute => 'format',
350             details => [$format]
351             );
352             }
353             }
354             }
355             }
356              
357 4         8 return $result;
358             }
359              
360             sub _validate_type {
361 2     2   3 my $self = shift;
362 2         5 my ($context, $json, $types) = @_;
363              
364 2         4 my $result = $self->_build_result;
365              
366 2 50       7 $types = [$types] unless ref $types eq 'ARRAY';
367              
368 2         4 my @results;
369 2         5 foreach my $type (@$types) {
370 2 50       5 if (_is_object($type)) {
    100          
371 0         0 my $subresult = $self->_validate($context, $json, $type);
372 0         0 push @results, $subresult;
373             }
374             elsif (!_is_type($json, $type)) {
375             push @results,
376             $self->_build_result->add_error(
377             uri => $context->{pointer},
378 1         3 message => 'Must be of type ' . $type,
379             attribute => 'type',
380             details => [$type]
381             );
382             }
383             else {
384 1         3 push @results, $self->_build_result;
385             }
386             }
387              
388 2 100 66     8 if (@results && !grep { $_->is_success } @results) {
  2         6  
389 1 50       5 if (@results == 1) {
390 1         5 $result->merge($results[0]);
391             }
392             else {
393             $result->add_error(
394             uri => $context->{pointer},
395 0         0 message => "Must be one of",
396             attribute => 'type',
397             );
398             }
399             }
400              
401 2         11 return $result;
402             }
403              
404             sub _validate_subschemas {
405 1     1   4 my $self = shift;
406 1         3 my ($context, $json, $type, $subschemas) = @_;
407              
408 1         3 my $result = $self->_build_result;
409              
410 1 50       4 $subschemas = [$subschemas] unless ref $subschemas eq 'ARRAY';
411              
412 1         2 my @subresults;
413 1         3 foreach my $subschema (@$subschemas) {
414 2         7 my $subresult = $self->_validate($context, $json, $subschema);
415              
416 2         5 push @subresults, $subresult;
417             }
418              
419 1         3 my @valid = grep { $_->is_success } @subresults;
  2         5  
420              
421 1 50       7 if ($type eq 'allOf') {
    50          
    50          
    0          
422 0 0       0 if (@valid != @subresults) {
423             $result->add_error(
424             uri => $context->{pointer},
425             message => "Must be all of",
426             attribute => 'allOf',
427 0         0 details => [map { @{$_->errors} } @subresults]
  0         0  
  0         0  
428             );
429             }
430             }
431             elsif ($type eq 'anyOf') {
432 0 0       0 if (!@valid) {
433             $result->add_error(
434             uri => $context->{pointer},
435             message => "Must be any of",
436             attribute => 'anyOf',
437 0         0 details => [map { @{$_->errors} } @subresults]
  0         0  
  0         0  
438             );
439             }
440             }
441             elsif ($type eq 'oneOf') {
442 1 50       3 if (@valid != 1) {
443             $result->add_error(
444             uri => $context->{pointer},
445             message => "Must be one of",
446             attribute => 'oneOf',
447 1         3 details => [map { @{$_->errors} } @subresults]
  2         2  
  2         5  
448             );
449             }
450             }
451             elsif ($type eq 'not') {
452 0 0       0 if (@valid) {
453             $result->add_error(
454             uri => $context->{pointer},
455             message => "Must not be of",
456             attribute => 'not',
457 0         0 details => [map { @{$_->errors} } @subresults]
  0         0  
  0         0  
458             );
459             }
460             }
461              
462 1         5 return $result;
463             }
464              
465             sub _validate_object {
466 3     3   6 my $self = shift;
467 3         5 my ($context, $json, $schema) = @_;
468              
469 3         97 $schema = Storable::dclone($schema);
470              
471 3         17 my $result = $self->_build_result(root => $context->{pointer});
472              
473 3 100       9 my @required = exists $schema->{required} ? @{$schema->{required}} : ();
  2         7  
474              
475 3 100       9 if (exists $schema->{properties}) {
476 1         2 foreach my $key (keys %{$schema->{properties}}) {
  1         5  
477              
478             # Required only if a boolean, otherwise it's a list of required properties
479 2 50 33     8 if (exists $schema->{properties}->{$key}->{required}
480             && _is_boolean($schema->{properties}->{$key}->{required}))
481             {
482 0         0 push @required, $key;
483             }
484             }
485             }
486              
487 3 50       8 if (exists $schema->{dependencies}) {
488 0         0 foreach my $dependency (keys %{$schema->{dependencies}}) {
  0         0  
489 0 0       0 next unless exists $json->{$dependency};
490              
491 0 0       0 if (_is_array($schema->{dependencies}->{$dependency})) {
    0          
492 0         0 push @required, @{$schema->{dependencies}->{$dependency}};
  0         0  
493             }
494             elsif (_is_object($schema->{dependencies}->{$dependency})) {
495 0         0 my $dependency_schema = $schema->{dependencies}->{$dependency};
496              
497 0         0 foreach my $key (keys %$dependency_schema) {
498 0 0       0 if ($key eq 'required') {
499 0         0 push @required, @{$dependency_schema->{$key}};
  0         0  
500             }
501             else {
502 0         0 $schema->{$key} = $dependency_schema->{$key};
503             }
504             }
505             }
506             }
507             }
508              
509 3 50       9 if (defined(my $min_properties = $schema->{minProperties})) {
510 0 0       0 if (keys %$json < $min_properties) {
511             $result->add_error(
512             uri => $context->{pointer},
513 0         0 message => "Must have minimum " . $min_properties . ' property(ies)',
514             attribute => 'minProperties',
515             details => [$min_properties]
516             );
517             }
518             }
519              
520 3 50       6 if (defined(my $max_properties = $schema->{maxProperties})) {
521 0 0       0 if (keys %$json > $max_properties) {
522             $result->add_error(
523             uri => $context->{pointer},
524 0         0 message => "Must have maximum " . $max_properties . ' property(ies)',
525             attribute => 'maxProperties',
526             details => [$max_properties]
527             );
528             }
529             }
530              
531 3 100       11 if (@required) {
532 2         5 foreach my $name (@required) {
533 2 50       6 if (!exists $json->{$name}) {
534 2         10 $result->add_error(
535             uri => "$context->{pointer}/$name",
536             message => 'Required',
537             attribute => 'required',
538             details => ['(true)']
539             );
540             }
541             }
542             }
543              
544 3         9 my @additional_properties = grep { !exists $schema->{properties}->{$_} } keys %$json;
  0         0  
545              
546 3 50       8 if (exists $schema->{additionalProperties}) {
547 0 0 0     0 if (_is_boolean($schema->{additionalProperties}) && !$schema->{additionalProperties}) {
    0          
548 0         0 PROPERTY: foreach my $additional_property (@additional_properties) {
549 0 0       0 if (my $pattern_properties = $schema->{patternProperties}) {
550 0         0 foreach my $pattern_property (keys %$pattern_properties) {
551 0 0       0 next PROPERTY if $additional_property =~ m/$pattern_property/;
552             }
553             }
554              
555             $result->add_error(
556 0         0 uri => "$context->{pointer}/$additional_property",
557             message => 'Unknown property',
558             );
559             }
560             }
561             elsif (_is_object($schema->{additionalProperties})) {
562 0         0 ADDITIONAL_PROPERTY: foreach my $additional_property (@additional_properties) {
563              
564             # patternProperties overwrite additionalProperties
565 0 0       0 if (my $pattern_properties = $schema->{patternProperties}) {
566 0         0 foreach my $pattern_property (keys %$pattern_properties) {
567 0 0       0 next ADDITIONAL_PROPERTY if $additional_property =~ m/$pattern_property/;
568             }
569             }
570              
571             my $subresult = $self->_validate(
572             {%$context, pointer => "$context->{pointer}/$additional_property"},
573             $json->{$additional_property},
574             $schema->{additionalProperties}
575 0         0 );
576 0         0 $result->merge($subresult);
577             }
578             }
579             }
580              
581 3 100       8 if (my $properties = $schema->{properties}) {
582 1         3 foreach my $name (keys %$properties) {
583 2 50       6 if (exists $json->{$name}) {
584             my $subresult = $self->_validate({%$context, pointer => "$context->{pointer}/$name"},
585 0         0 $json->{$name}, $properties->{$name});
586 0         0 $result->merge($subresult);
587             }
588             }
589             }
590              
591 3 50       11 if (_is_object($schema->{patternProperties})) {
592 0         0 foreach my $pattern_property (keys %{$schema->{patternProperties}}) {
  0         0  
593 0         0 my @matched_properties = grep { m/$pattern_property/ } keys %$json;
  0         0  
594              
595 0         0 foreach my $property (@matched_properties) {
596             my $subresult = $self->_validate({%$context, pointer => "$context->{pointer}/$property"},
597 0         0 $json->{$property}, $schema->{patternProperties}->{$pattern_property});
598 0         0 $result->merge($subresult);
599             }
600             }
601             }
602              
603 3         12 return $result;
604             }
605              
606             sub _validate_array {
607 1     1   2 my $self = shift;
608 1         3 my ($context, $json, $schema) = @_;
609              
610 1         4 my $result = $self->_build_result(root => $context->{pointer});
611              
612 1 50       5 if (defined(my $min_items = $schema->{minItems})) {
613 0 0       0 if (@$json < $min_items) {
614             $result->add_error(
615             uri => $context->{pointer},
616 0         0 message => "Must have minimum " . $min_items . ' item(s)',
617             attribute => 'minItems',
618             details => [$min_items],
619             );
620             }
621             }
622              
623 1 50       4 if (defined(my $max_items = $schema->{maxItems})) {
624 0 0       0 if (@$json > $max_items) {
625             $result->add_error(
626             uri => $context->{pointer},
627 0         0 message => "Must have maximum " . $max_items . ' item(s)',
628             attribute => 'maxItems',
629             details => [$max_items],
630             );
631             }
632             }
633              
634 1 50       5 if (_is_array($schema->{items})) {
635 0         0 my $exp_length = @{$schema->{items}};
  0         0  
636 0         0 my $got_length = @$json;
637              
638 0         0 for (my $i = 0; $i < @{$schema->{items}}; $i++) {
  0         0  
639 0 0       0 last if @$json < $i + 1;
640              
641             my $subresult =
642 0         0 $self->_validate({%$context, pointer => "$context->{pointer}\[$i]"}, $json->[$i], $schema->{items}->[$i]);
643 0         0 $result->merge($subresult);
644             }
645              
646 0 0       0 if ($got_length > $exp_length) {
647 0 0       0 if (_is_boolean($schema->{additionalItems})) {
    0          
648 0 0       0 if (!$schema->{additionalItems}) {
649             $result->add_error(
650             uri => $context->{pointer},
651 0         0 message => "Must have exactly " . @{$schema->{items}} . ' item(s)',
652             attribute => 'additionalItems',
653 0         0 details => [scalar @{$schema->{items}}]
  0         0  
654              
655             );
656             }
657             }
658             elsif (_is_object($schema->{additionalItems})) {
659 0         0 for ($exp_length .. $got_length - 1) {
660             my $subresult = $self->_validate({%$context, pointer => "$context->{pointer}\[$_]"},
661 0         0 $json->[$_], $schema->{additionalItems});
662 0         0 $result->merge($subresult);
663             }
664             }
665             }
666             }
667              
668 1 50       5 if (_is_object($schema->{items})) {
669 0         0 for (my $i = 0; $i < @$json; $i++) {
670             my $subresult =
671 0         0 $self->_validate({%$context, pointer => "$context->{pointer}/$i"}, $json->[$i], $schema->{items});
672 0         0 $result->merge($subresult);
673             }
674             }
675              
676 1 50       5 if ($schema->{uniqueItems}) {
677 0         0 my $seen = {};
678 0         0 foreach my $el (@$json) {
679 0 0       0 my $hash = ref $el ? JSON::encode_json($el) : defined $el ? $el : 'null';
    0          
680              
681 0 0       0 if (exists $seen->{$hash}) {
682             $result->add_error(
683             uri => $context->{pointer},
684 0         0 message => "Must have unique items",
685             attribute => 'uniqueItems',
686             details => ['(true)']
687             );
688 0         0 last;
689             }
690 0         0 $seen->{$hash}++;
691             }
692             }
693              
694 1 50       4 if ($schema->{contains}) {
695 0 0       0 if (!@$json) {
696             $result->add_error(
697             uri => $context->{pointer},
698 0         0 message => "Must not be empty",
699             attribute => 'contains'
700             );
701             }
702             }
703              
704 1         3 return $result;
705             }
706              
707             sub _validate_string {
708 0     0   0 my $self = shift;
709 0         0 my ($context, $json, $schema) = @_;
710              
711 0         0 my $result = $self->_build_result(pointer => $context->{pointer});
712              
713 0 0       0 if (defined(my $max_length = $schema->{maxLength})) {
714 0 0       0 if (length($json) > $max_length) {
715             $result->add_error(
716             uri => $context->{pointer},
717 0         0 message => "Must have the maximum length of $max_length",
718             attribute => 'maxLength',
719             details => [$max_length]
720             );
721             }
722             }
723              
724 0 0       0 if (defined(my $min_length = $schema->{minLength})) {
725 0 0       0 if (length($json) < $min_length) {
726             $result->add_error(
727             uri => $context->{pointer},
728 0         0 message => "Must have the minimum length of $min_length",
729             attribute => 'minLength',
730             details => [$min_length]
731             );
732             }
733             }
734              
735 0 0       0 if (my $pattern = $schema->{pattern}) {
736 0 0       0 if ($json !~ m/$pattern/) {
737             $result->add_error(
738             uri => $context->{pointer},
739 0         0 message => "Must match pattern $pattern",
740             attribute => 'pattern',
741             details => ["$pattern"]
742             );
743             }
744             }
745              
746 0         0 return $result;
747             }
748              
749             sub _validate_number {
750 0     0   0 my $self = shift;
751 0         0 my ($context, $json, $schema) = @_;
752              
753 0         0 my $result = $self->_build_result(pointer => $context->{pointer});
754              
755 0 0       0 if (defined(my $minimum = $schema->{minimum})) {
756 0 0       0 if ($schema->{exclusiveMinimum}) {
757 0 0       0 if ($json <= $minimum) {
758             $result->add_error(
759             uri => $context->{pointer},
760 0         0 message => "Must be greater than or equals to $minimum",
761             attribute => 'minimum',
762             details => [$minimum]
763             );
764             }
765             }
766             else {
767 0 0       0 if ($json < $minimum) {
768             $result->add_error(
769             uri => $context->{pointer},
770 0         0 message => "Must be greater than $minimum",
771             attribute => 'minimum',
772             details => [$minimum]
773             );
774             }
775             }
776             }
777              
778 0 0       0 if (_is_number($schema->{exclusiveMaximum})) {
779 0         0 my $maximum = $schema->{exclusiveMaximum};
780              
781 0 0       0 if ($json >= $maximum) {
782             $result->add_error(
783             uri => $context->{pointer},
784 0         0 message => "Must be less than or equals to $maximum",
785             attribute => 'maximum',
786             details => [$maximum]
787             );
788             }
789             }
790              
791 0 0       0 if (defined(my $maximum = $schema->{maximum})) {
792 0 0       0 if ($schema->{exclusiveMaximum}) {
793 0 0       0 if ($json >= $maximum) {
794             $result->add_error(
795             uri => $context->{pointer},
796 0         0 message => "Must be less than or equals to $maximum",
797             attribute => 'maximum',
798             details => [$maximum]
799             );
800             }
801             }
802             else {
803 0 0       0 if ($json > $maximum) {
804             $result->add_error(
805             uri => $context->{pointer},
806 0         0 message => "Must be less than $maximum",
807             attribute => 'maximum',
808             details => [$maximum]
809             );
810             }
811             }
812             }
813              
814 0 0       0 if (defined(my $divisibleBy = $schema->{divisibleBy})) {
815 0 0       0 if (sprintf('%0.6f', $json) ne sprintf('%0.6f', int($json / $divisibleBy) * $divisibleBy)) {
816             $result->add_error(
817             uri => $context->{pointer},
818 0         0 message => "Must be divisible by $divisibleBy",
819             attribute => 'divisibleBy',
820             details => [$divisibleBy]
821             );
822             }
823             }
824              
825 0 0       0 if (defined(my $multipleOf = $schema->{multipleOf})) {
826 0 0       0 if (sprintf('%0.6f', $json) ne sprintf('%0.6f', int($json / $multipleOf) * $multipleOf)) {
827             $result->add_error(
828             uri => $context->{pointer},
829 0         0 message => "Must be multiple of by $multipleOf",
830             attribute => 'multipleOf',
831             details => [$multipleOf]
832             );
833             }
834             }
835              
836 0         0 return $result;
837             }
838              
839             sub _validate_enum {
840 0     0   0 my $self = shift;
841 0         0 my ($context, $json, $enum) = @_;
842              
843 0         0 my $result = $self->_build_result(pointer => $context->{pointer});
844              
845 0         0 my $set = {};
846 0         0 foreach my $el (@$enum) {
847 0 0       0 my $hash = ref $el ? JSON::encode_json($el) : $el;
848 0         0 $set->{$hash} = 1;
849             }
850              
851 0 0       0 my $hash = ref $json ? JSON::encode_json($json) : defined $json ? $json : 'null';
    0          
852              
853 0 0       0 if (!exists $set->{$hash}) {
854             $result->add_error(
855             uri => $context->{pointer},
856 0         0 message => "Must be one of",
857             attribute => 'enum',
858             details => [@$enum]
859             );
860             }
861              
862 0         0 return $result;
863             }
864              
865             sub _validate_const {
866 0     0   0 my $self = shift;
867 0         0 my ($context, $json, $const) = @_;
868              
869 0         0 my $result = $self->_build_result();
870              
871 0         0 my $exp_type = _type($const);
872              
873 0 0 0     0 if (_is_type($json, $exp_type) || ($exp_type eq 'integer' && _type($json) eq 'number')) {
      0        
874 0 0 0     0 if (_is_object($json) || _is_array($json)) {
    0          
    0          
    0          
875 0 0       0 if (JSON->new->utf8->canonical->encode($json) ne JSON->new->utf8->canonical->encode($const)) {
876             $result->add_error(
877             uri => $context->{pointer},
878 0         0 message => "Must be equal to const",
879             attribute => 'const',
880             );
881             }
882             }
883             elsif (_is_number($json)) {
884 0 0       0 if (sprintf('%0.6f', $const) ne sprintf('%0.6f', $json)) {
885             $result->add_error(
886             uri => $context->{pointer},
887 0         0 message => "Must be of equal to $const",
888             attribute => 'const',
889             details => [$const]
890             );
891             }
892             }
893             elsif (_is_string($json)) {
894 0 0       0 if ($json ne $const) {
895             $result->add_error(
896             uri => $context->{pointer},
897 0         0 message => "Must be of equal to $const",
898             attribute => 'const',
899             details => [$const]
900             );
901             }
902             }
903             elsif (_is_boolean($json)) {
904 0 0       0 if ($const != $json) {
905             $result->add_error(
906             uri => $context->{pointer},
907 0         0 message => "Must be of equal to $const",
908             attribute => 'const',
909             details => [$const]
910             );
911             }
912             }
913             }
914             else {
915             $result->add_error(
916             uri => $context->{pointer},
917 0         0 message => "Must be of type $exp_type",
918             attribute => 'const',
919             details => [$exp_type]
920             );
921             }
922              
923 0         0 return $result;
924             }
925              
926             sub _is_object {
927 54     54   91 my ($value) = @_;
928              
929 54   100     249 return defined $value && ref $value eq 'HASH';
930             }
931              
932             sub _is_array {
933 26     26   44 my ($value) = @_;
934              
935 26   100     127 return defined $value && ref $value eq 'ARRAY';
936             }
937              
938             sub _is_boolean {
939 13     13   21 my ($value) = @_;
940              
941 13   66     46 return defined $value && JSON::is_bool($value);
942             }
943              
944             sub _is_number {
945 11     11   21 my ($value) = @_;
946              
947 11 50       26 return 0 unless defined $value;
948 11 50       21 return 0 if ref $value;
949 11 50       22 return 0 if JSON::is_bool($value);
950              
951 11         61 my $b_obj = B::svref_2object(\$value);
952 11         24 my $flags = $b_obj->FLAGS;
953 11 100 66     55 return 1
954             if $flags & (B::SVp_IOK() | B::SVp_NOK())
955             && !($flags & B::SVp_POK());
956              
957 4         12 return 0;
958             }
959              
960             sub _is_integer {
961 10     10   20 my ($value) = @_;
962              
963 10 50       23 return 0 unless defined $value;
964 10 50       19 return 0 if ref $value;
965 10 50       20 return 0 if JSON::is_bool($value);
966              
967 10         61 my $b_obj = B::svref_2object(\$value);
968 10         37 my $flags = $b_obj->FLAGS;
969 10 100 66     44 return 1 if ($flags & B::SVp_IOK()) && !($flags & B::SVp_POK());
970              
971 6         14 return 0;
972             }
973              
974             sub _is_string {
975 6     6   10 my ($value) = @_;
976              
977 6 50       25 return 0 unless defined $value;
978 6 100       30 return 0 if ref $value;
979 2 50       5 return 0 if _is_boolean($value);
980 2 50       14 return 0 if _is_number($value);
981              
982 2         8 return 1;
983             }
984              
985             sub _is_null {
986 20     20   35 my ($value) = @_;
987              
988 20 100       73 return defined $value ? 0 : 1;
989             }
990              
991             sub _is_type {
992 11     11   31 my ($value, $type) = @_;
993              
994 11         26 my $real_type = _type($value);
995              
996 11 100       35 if ($type eq 'number') {
997 3 100       10 return 1 if $real_type eq 'integer';
998             }
999              
1000 10         40 return $real_type eq $type;
1001              
1002 0 0       0 return _type($value) eq $type ? 1 : 0;
1003             }
1004              
1005             sub _type {
1006 19     19   1238 my ($value) = @_;
1007              
1008 19 100       44 return 'null' if _is_null($value);
1009 17 100       42 return 'object' if _is_object($value);
1010 14 100       38 return 'array' if _is_array($value);
1011 11 100       28 return 'boolean' if _is_boolean($value);
1012 9 100       51 return 'integer' if _is_integer($value);
1013 6 100       13 return 'number' if _is_number($value);
1014 2 50       6 return 'string' if _is_string($value);
1015              
1016 0         0 Carp::croak("Unknown type");
1017             }
1018              
1019             sub _subschema {
1020 4     4   8 my ($schema) = @_;
1021              
1022 4         8 for (qw/allOf anyOf oneOf not/) {
1023 15 100       33 return $_ if $schema->{$_};
1024             }
1025              
1026 3         7 return;
1027             }
1028              
1029             sub _resolve_remote_ref {
1030 0     0   0 my $self = shift;
1031 0         0 my ($context, $ref) = @_;
1032              
1033 0         0 my ($url, $pointer) = $ref =~ m/^([^#]+)(#.*)?$/;
1034              
1035 0         0 my $schema;
1036              
1037 0 0       0 if (exists $context->{ids}->{$url}) {
    0          
1038 0         0 $schema = $context->{ids}->{$url};
1039             }
1040             elsif ($context->{remote_cache}->{$url}) {
1041 0         0 $schema = $context->{remote_cache}->{$url};
1042             }
1043             else {
1044 0         0 $schema = eval { $self->{fetcher}->($url) };
  0         0  
1045 0         0 $context->{remote_cache}->{$url} = $schema;
1046              
1047 0 0       0 if ($schema) {
1048 0   0     0 $schema->{id} //= $url;
1049              
1050 0         0 $self->_collect_ids($context, $schema);
1051             }
1052             }
1053              
1054 0 0 0     0 if ($schema && $pointer) {
1055 0         0 $schema = pointer($schema, $pointer);
1056             }
1057              
1058 0         0 return $schema;
1059             }
1060              
1061             sub _build_result {
1062 13     13   19 my $self = shift;
1063              
1064 13         42 return JSON::SchemaValidator::Result->new;
1065             }
1066              
1067             1;
1068             __END__