File Coverage

blib/lib/JSON/SchemaValidator.pm
Criterion Covered Total %
statement 136 452 30.0
branch 61 344 17.7
condition 16 57 28.0
subroutine 25 38 65.7
pod 0 3 0.0
total 238 894 26.6


line stmt bran cond sub pod time code
1             package JSON::SchemaValidator;
2              
3 3     3   235983 use strict;
  3         33  
  3         87  
4 3     3   17 use warnings;
  3         6  
  3         144  
5              
6             our $VERSION = '1.02';
7              
8 3     3   21 use B ();
  3         5  
  3         44  
9 3     3   1972 use Storable ();
  3         9297  
  3         99  
10             require Carp;
11 3     3   1745 use Time::Piece;
  3         33335  
  3         13  
12              
13 3     3   1552 use JSON::SchemaValidator::Result;
  3         9  
  3         108  
14 3     3   1250 use JSON::SchemaValidator::Pointer qw(pointer);
  3         7  
  3         18332  
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 91 my $class = shift;
45 1         3 my (%params) = @_;
46              
47 1         3 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         25 };
118 1         3 $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 1     1 0 1065 my $self = shift;
127 1         3 my ($json, $schema) = @_;
128              
129 1         117 $schema = Storable::dclone($schema);
130              
131 1         8 my $context = {
132             root => $schema,
133             ids => {},
134             pointer => '#',
135             };
136              
137 1         4 $self->_collect_ids($context, $schema);
138              
139 1         4 my $result = $self->_validate($context, $json, $schema);
140              
141 1         4 return $result;
142             }
143              
144             sub _collect_ids {
145 2     2   4 my $self = shift;
146 2         4 my ($context, $schema) = @_;
147              
148 2 100       5 if (_is_object($schema)) {
    50          
149 1         5 my $new_context = {%$context};
150              
151 1 50 33     5 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 1 50 33     5 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 1         4 foreach my $key (keys %$schema) {
219 1         5 $self->_collect_ids($new_context, $schema->{$key});
220             }
221             }
222             elsif (_is_array($schema)) {
223 0         0 foreach my $el (@$schema) {
224 0         0 $self->_collect_ids($context, $el);
225             }
226             }
227             }
228              
229             sub _resolve_refs {
230 1     1   12 my $self = shift;
231 1         4 my ($context, $schema) = @_;
232              
233 1 50       3 if (_is_object($schema)) {
    0          
234 1 50 33     4 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 0         0 foreach my $el (@$schema) {
286 0         0 $self->_resolve_refs($context, $el);
287             }
288             }
289             }
290              
291             sub _validate {
292 1     1   2 my $self = shift;
293 1         4 my ($context, $json, $schema) = @_;
294              
295 1         2 my $pointer = $context->{pointer};
296              
297 1         3 my $result = $self->_build_result;
298              
299 1         5 $self->_resolve_refs($context, $schema);
300              
301 1 50       3 if (_is_object($schema)) {
302 1 50       7 if (my $types = $schema->{type}) {
303 1         6 my $subresult = $self->_validate_type($context, $json, $types);
304 1         4 $result->merge($subresult);
305             }
306              
307 1 50       9 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 1 50       4 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 1 50       3 if (_is_object($json)) {
    50          
    0          
    0          
319 0         0 my $subresult = $self->_validate_object($context, $json, $schema);
320 0         0 $result->merge($subresult);
321             }
322             elsif (_is_array($json)) {
323 1         4 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 1 50       4 if (my $subschema_type = _subschema($schema)) {
336 0         0 $self->_resolve_refs($context, $schema->{$subschema_type});
337              
338 0         0 my $subresult = $self->_validate_subschemas($context, $json, $subschema_type, $schema->{$subschema_type});
339 0         0 $result->merge($subresult);
340             }
341              
342 1 50       3 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 1         9 return $result;
358             }
359              
360             sub _validate_type {
361 1     1   4 my $self = shift;
362 1         6 my ($context, $json, $types) = @_;
363              
364 1         2 my $result = $self->_build_result;
365              
366 1 50       4 $types = [$types] unless ref $types eq 'ARRAY';
367              
368 1         2 my @results;
369 1         3 foreach my $type (@$types) {
370 1 50       2 if (_is_object($type)) {
    50          
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         4 message => 'Must be of type ' . $type,
379             attribute => 'type',
380             details => [$type]
381             );
382             }
383             else {
384 0         0 push @results, $self->_build_result;
385             }
386             }
387              
388 1 50 33     5 if (@results && !grep { $_->is_success } @results) {
  1         4  
389 1 50       5 if (@results == 1) {
390 1         3 $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 1         8 return $result;
402             }
403              
404             sub _validate_subschemas {
405 0     0   0 my $self = shift;
406 0         0 my ($context, $json, $type, $subschemas) = @_;
407              
408 0         0 my $result = $self->_build_result;
409              
410 0 0       0 $subschemas = [$subschemas] unless ref $subschemas eq 'ARRAY';
411              
412 0         0 my @subresults;
413 0         0 foreach my $subschema (@$subschemas) {
414 0         0 my $subresult = $self->_validate($context, $json, $subschema);
415              
416 0         0 push @subresults, $subresult;
417             }
418              
419 0         0 my @valid = grep { $_->is_success } @subresults;
  0         0  
420              
421 0 0       0 if ($type eq 'allOf') {
    0          
    0          
    0          
422 0 0       0 if (@valid != @subresults) {
423             $result->add_error(
424             uri => $context->{pointer},
425 0         0 message => "Must be all of",
426             attribute => 'allOf',
427             );
428             }
429             }
430             elsif ($type eq 'anyOf') {
431 0 0       0 if (!@valid) {
432             $result->add_error(
433             uri => $context->{pointer},
434 0         0 message => "Must be any of",
435             attribute => 'anyOf',
436             );
437             }
438             }
439             elsif ($type eq 'oneOf') {
440 0 0       0 if (@valid != 1) {
441             $result->add_error(
442             uri => $context->{pointer},
443 0         0 message => "Must be one of",
444             attribute => 'oneOf',
445             );
446             }
447             }
448             elsif ($type eq 'not') {
449 0 0       0 if (@valid) {
450             $result->add_error(
451             uri => $context->{pointer},
452 0         0 message => "Must not be of",
453             attribute => 'not',
454             );
455             }
456             }
457              
458 0         0 return $result;
459             }
460              
461             sub _validate_object {
462 0     0   0 my $self = shift;
463 0         0 my ($context, $json, $schema) = @_;
464              
465 0         0 $schema = Storable::dclone($schema);
466              
467 0         0 my $result = $self->_build_result(root => $context->{pointer});
468              
469 0 0       0 my @required = exists $schema->{required} ? @{$schema->{required}} : ();
  0         0  
470              
471 0 0       0 if (exists $schema->{properties}) {
472 0         0 foreach my $key (keys %{$schema->{properties}}) {
  0         0  
473 0 0       0 push @required, $key if $schema->{properties}->{$key}->{required};
474             }
475             }
476              
477 0 0       0 if (exists $schema->{dependencies}) {
478 0         0 foreach my $dependency (keys %{$schema->{dependencies}}) {
  0         0  
479 0 0       0 next unless exists $json->{$dependency};
480              
481 0 0       0 if (_is_array($schema->{dependencies}->{$dependency})) {
    0          
482 0         0 push @required, @{$schema->{dependencies}->{$dependency}};
  0         0  
483             }
484             elsif (_is_object($schema->{dependencies}->{$dependency})) {
485 0         0 my $dependency_schema = $schema->{dependencies}->{$dependency};
486              
487 0         0 foreach my $key (keys %$dependency_schema) {
488 0 0       0 if ($key eq 'required') {
489 0         0 push @required, @{$dependency_schema->{$key}};
  0         0  
490             }
491             else {
492 0         0 $schema->{$key} = $dependency_schema->{$key};
493             }
494             }
495             }
496             }
497             }
498              
499 0 0       0 if (defined(my $min_properties = $schema->{minProperties})) {
500 0 0       0 if (keys %$json < $min_properties) {
501             $result->add_error(
502             uri => $context->{pointer},
503 0         0 message => "Must have minimum " . $min_properties . ' property(ies)',
504             attribute => 'minProperties',
505             details => [$min_properties]
506             );
507             }
508             }
509              
510 0 0       0 if (defined(my $max_properties = $schema->{maxProperties})) {
511 0 0       0 if (keys %$json > $max_properties) {
512             $result->add_error(
513             uri => $context->{pointer},
514 0         0 message => "Must have maximum " . $max_properties . ' property(ies)',
515             attribute => 'maxProperties',
516             details => [$max_properties]
517             );
518             }
519             }
520              
521 0 0       0 if (@required) {
522 0         0 foreach my $name (@required) {
523 0 0       0 if (!exists $json->{$name}) {
524 0         0 $result->add_error(
525             uri => "$context->{pointer}/$name",
526             message => 'Required',
527             attribute => 'required',
528             details => ['(true)']
529             );
530             }
531             }
532             }
533              
534 0         0 my @additional_properties = grep { !exists $schema->{properties}->{$_} } keys %$json;
  0         0  
535              
536 0 0       0 if (exists $schema->{additionalProperties}) {
537 0 0 0     0 if (_is_boolean($schema->{additionalProperties}) && !$schema->{additionalProperties}) {
    0          
538 0         0 PROPERTY: foreach my $additional_property (@additional_properties) {
539 0 0       0 if (my $pattern_properties = $schema->{patternProperties}) {
540 0         0 foreach my $pattern_property (keys %$pattern_properties) {
541 0 0       0 next PROPERTY if $additional_property =~ m/$pattern_property/;
542             }
543             }
544              
545             $result->add_error(
546 0         0 uri => "$context->{pointer}/$additional_property",
547             message => 'Unknown property',
548             );
549             }
550             }
551             elsif (_is_object($schema->{additionalProperties})) {
552 0         0 ADDITIONAL_PROPERTY: foreach my $additional_property (@additional_properties) {
553              
554             # patternProperties overwrite additionalProperties
555 0 0       0 if (my $pattern_properties = $schema->{patternProperties}) {
556 0         0 foreach my $pattern_property (keys %$pattern_properties) {
557 0 0       0 next ADDITIONAL_PROPERTY if $additional_property =~ m/$pattern_property/;
558             }
559             }
560              
561             my $subresult = $self->_validate(
562             {%$context, pointer => "$context->{pointer}/$additional_property"},
563             $json->{$additional_property},
564             $schema->{additionalProperties}
565 0         0 );
566 0         0 $result->merge($subresult);
567             }
568             }
569             }
570              
571 0 0       0 if (my $properties = $schema->{properties}) {
572 0         0 foreach my $name (keys %$properties) {
573 0 0       0 if (exists $json->{$name}) {
574             my $subresult = $self->_validate({%$context, pointer => "$context->{pointer}/$name"},
575 0         0 $json->{$name}, $properties->{$name});
576 0         0 $result->merge($subresult);
577             }
578             }
579             }
580              
581 0 0       0 if (_is_object($schema->{patternProperties})) {
582 0         0 foreach my $pattern_property (keys %{$schema->{patternProperties}}) {
  0         0  
583 0         0 my @matched_properties = grep { m/$pattern_property/ } keys %$json;
  0         0  
584              
585 0         0 foreach my $property (@matched_properties) {
586             my $subresult = $self->_validate({%$context, pointer => "$context->{pointer}/$property"},
587 0         0 $json->{$property}, $schema->{patternProperties}->{$pattern_property});
588 0         0 $result->merge($subresult);
589             }
590             }
591             }
592              
593 0         0 return $result;
594             }
595              
596             sub _validate_array {
597 1     1   2 my $self = shift;
598 1         3 my ($context, $json, $schema) = @_;
599              
600 1         4 my $result = $self->_build_result(root => $context->{pointer});
601              
602 1 50       4 if (defined(my $min_items = $schema->{minItems})) {
603 0 0       0 if (@$json < $min_items) {
604             $result->add_error(
605             uri => $context->{pointer},
606 0         0 message => "Must have minimum " . $min_items . ' item(s)',
607             attribute => 'minItems',
608             details => [$min_items],
609             );
610             }
611             }
612              
613 1 50       3 if (defined(my $max_items = $schema->{maxItems})) {
614 0 0       0 if (@$json > $max_items) {
615             $result->add_error(
616             uri => $context->{pointer},
617 0         0 message => "Must have maximum " . $max_items . ' item(s)',
618             attribute => 'maxItems',
619             details => [$max_items],
620             );
621             }
622             }
623              
624 1 50       4 if (_is_array($schema->{items})) {
625 0         0 my $exp_length = @{$schema->{items}};
  0         0  
626 0         0 my $got_length = @$json;
627              
628 0         0 for (my $i = 0; $i < @{$schema->{items}}; $i++) {
  0         0  
629 0 0       0 last if @$json < $i + 1;
630              
631             my $subresult =
632 0         0 $self->_validate({%$context, pointer => "$context->{pointer}\[$i]"}, $json->[$i], $schema->{items}->[$i]);
633 0         0 $result->merge($subresult);
634             }
635              
636 0 0       0 if ($got_length > $exp_length) {
637 0 0       0 if (_is_boolean($schema->{additionalItems})) {
    0          
638 0 0       0 if (!$schema->{additionalItems}) {
639             $result->add_error(
640             uri => $context->{pointer},
641 0         0 message => "Must have exactly " . @{$schema->{items}} . ' item(s)',
642             attribute => 'additionalItems',
643 0         0 details => [scalar @{$schema->{items}}]
  0         0  
644              
645             );
646             }
647             }
648             elsif (_is_object($schema->{additionalItems})) {
649 0         0 for ($exp_length .. $got_length - 1) {
650             my $subresult = $self->_validate({%$context, pointer => "$context->{pointer}\[$_]"},
651 0         0 $json->[$_], $schema->{additionalItems});
652 0         0 $result->merge($subresult);
653             }
654             }
655             }
656             }
657              
658 1 50       6 if (_is_object($schema->{items})) {
659 0         0 for (my $i = 0; $i < @$json; $i++) {
660             my $subresult =
661 0         0 $self->_validate({%$context, pointer => "$context->{pointer}/$i"}, $json->[$i], $schema->{items});
662 0         0 $result->merge($subresult);
663             }
664             }
665              
666 1 50       4 if ($schema->{uniqueItems}) {
667 0         0 my $seen = {};
668 0         0 foreach my $el (@$json) {
669 0 0       0 my $hash = ref $el ? JSON::encode_json($el) : defined $el ? $el : 'null';
    0          
670              
671 0 0       0 if (exists $seen->{$hash}) {
672             $result->add_error(
673             uri => $context->{pointer},
674 0         0 message => "Must have unique items",
675             attribute => 'uniqueItems',
676             details => ['(true)']
677             );
678 0         0 last;
679             }
680 0         0 $seen->{$hash}++;
681             }
682             }
683              
684 1 50       3 if ($schema->{contains}) {
685 0 0       0 if (!@$json) {
686             $result->add_error(
687             uri => $context->{pointer},
688 0         0 message => "Must not be empty",
689             attribute => 'contains'
690             );
691             }
692             }
693              
694 1         3 return $result;
695             }
696              
697             sub _validate_string {
698 0     0   0 my $self = shift;
699 0         0 my ($context, $json, $schema) = @_;
700              
701 0         0 my $result = $self->_build_result(pointer => $context->{pointer});
702              
703 0 0       0 if (defined(my $max_length = $schema->{maxLength})) {
704 0 0       0 if (length($json) > $max_length) {
705             $result->add_error(
706             uri => $context->{pointer},
707 0         0 message => "Must have the maximum length of $max_length",
708             attribute => 'maxLength',
709             details => [$max_length]
710             );
711             }
712             }
713              
714 0 0       0 if (defined(my $min_length = $schema->{minLength})) {
715 0 0       0 if (length($json) < $min_length) {
716             $result->add_error(
717             uri => $context->{pointer},
718 0         0 message => "Must have the minimum length of $min_length",
719             attribute => 'minLength',
720             details => [$min_length]
721             );
722             }
723             }
724              
725 0 0       0 if (my $pattern = $schema->{pattern}) {
726 0 0       0 if ($json !~ m/$pattern/) {
727             $result->add_error(
728             uri => $context->{pointer},
729 0         0 message => "Must match pattern $pattern",
730             attribute => 'pattern',
731             details => ["$pattern"]
732             );
733             }
734             }
735              
736 0         0 return $result;
737             }
738              
739             sub _validate_number {
740 0     0   0 my $self = shift;
741 0         0 my ($context, $json, $schema) = @_;
742              
743 0         0 my $result = $self->_build_result(pointer => $context->{pointer});
744              
745 0 0       0 if (defined(my $minimum = $schema->{minimum})) {
746 0 0       0 if ($schema->{exclusiveMinimum}) {
747 0 0       0 if ($json <= $minimum) {
748             $result->add_error(
749             uri => $context->{pointer},
750 0         0 message => "Must be greater than or equals to $minimum",
751             attribute => 'minimum',
752             details => [$minimum]
753             );
754             }
755             }
756             else {
757 0 0       0 if ($json < $minimum) {
758             $result->add_error(
759             uri => $context->{pointer},
760 0         0 message => "Must be greater than $minimum",
761             attribute => 'minimum',
762             details => [$minimum]
763             );
764             }
765             }
766             }
767              
768 0 0       0 if (_is_number($schema->{exclusiveMaximum})) {
769 0         0 my $maximum = $schema->{exclusiveMaximum};
770              
771 0 0       0 if ($json >= $maximum) {
772             $result->add_error(
773             uri => $context->{pointer},
774 0         0 message => "Must be less than or equals to $maximum",
775             attribute => 'maximum',
776             details => [$maximum]
777             );
778             }
779             }
780              
781 0 0       0 if (defined(my $maximum = $schema->{maximum})) {
782 0 0       0 if ($schema->{exclusiveMaximum}) {
783 0 0       0 if ($json >= $maximum) {
784             $result->add_error(
785             uri => $context->{pointer},
786 0         0 message => "Must be less than or equals to $maximum",
787             attribute => 'maximum',
788             details => [$maximum]
789             );
790             }
791             }
792             else {
793 0 0       0 if ($json > $maximum) {
794             $result->add_error(
795             uri => $context->{pointer},
796 0         0 message => "Must be less than $maximum",
797             attribute => 'maximum',
798             details => [$maximum]
799             );
800             }
801             }
802             }
803              
804 0 0       0 if (defined(my $divisibleBy = $schema->{divisibleBy})) {
805 0 0       0 if (sprintf('%0.6f', $json) ne sprintf('%0.6f', int($json / $divisibleBy) * $divisibleBy)) {
806             $result->add_error(
807             uri => $context->{pointer},
808 0         0 message => "Must be divisible by $divisibleBy",
809             attribute => 'divisibleBy',
810             details => [$divisibleBy]
811             );
812             }
813             }
814              
815 0 0       0 if (defined(my $multipleOf = $schema->{multipleOf})) {
816 0 0       0 if (sprintf('%0.6f', $json) ne sprintf('%0.6f', int($json / $multipleOf) * $multipleOf)) {
817             $result->add_error(
818             uri => $context->{pointer},
819 0         0 message => "Must be multiple of by $multipleOf",
820             attribute => 'multipleOf',
821             details => [$multipleOf]
822             );
823             }
824             }
825              
826 0         0 return $result;
827             }
828              
829             sub _validate_enum {
830 0     0   0 my $self = shift;
831 0         0 my ($context, $json, $enum) = @_;
832              
833 0         0 my $result = $self->_build_result(pointer => $context->{pointer});
834              
835 0         0 my $set = {};
836 0         0 foreach my $el (@$enum) {
837 0 0       0 my $hash = ref $el ? JSON::encode_json($el) : $el;
838 0         0 $set->{$hash} = 1;
839             }
840              
841 0 0       0 my $hash = ref $json ? JSON::encode_json($json) : defined $json ? $json : 'null';
    0          
842              
843 0 0       0 if (!exists $set->{$hash}) {
844             $result->add_error(
845             uri => $context->{pointer},
846 0         0 message => "Must be one of",
847             attribute => 'enum',
848             details => [@$enum]
849             );
850             }
851              
852 0         0 return $result;
853             }
854              
855             sub _validate_const {
856 0     0   0 my $self = shift;
857 0         0 my ($context, $json, $const) = @_;
858              
859 0         0 my $result = $self->_build_result();
860              
861 0         0 my $exp_type = _type($const);
862              
863 0 0 0     0 if (_is_type($json, $exp_type) || ($exp_type eq 'integer' && _type($json) eq 'number')) {
      0        
864 0 0 0     0 if (_is_object($json) || _is_array($json)) {
    0          
    0          
    0          
865 0 0       0 if (JSON->new->utf8->canonical->encode($json) ne JSON->new->utf8->canonical->encode($const)) {
866             $result->add_error(
867             uri => $context->{pointer},
868 0         0 message => "Must be equal to const",
869             attribute => 'const',
870             );
871             }
872             }
873             elsif (_is_number($json)) {
874 0 0       0 if (sprintf('%0.6f', $const) ne sprintf('%0.6f', $json)) {
875             $result->add_error(
876             uri => $context->{pointer},
877 0         0 message => "Must be of equal to $const",
878             attribute => 'const',
879             details => [$const]
880             );
881             }
882             }
883             elsif (_is_string($json)) {
884 0 0       0 if ($json ne $const) {
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_boolean($json)) {
894 0 0       0 if ($const != $json) {
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             }
904             else {
905             $result->add_error(
906             uri => $context->{pointer},
907 0         0 message => "Must be of type $exp_type",
908             attribute => 'const',
909             details => [$exp_type]
910             );
911             }
912              
913 0         0 return $result;
914             }
915              
916             sub _is_object {
917 23     23   51 my ($value) = @_;
918              
919 23   100     140 return defined $value && ref $value eq 'HASH';
920             }
921              
922             sub _is_array {
923 17     17   33 my ($value) = @_;
924              
925 17   100     84 return defined $value && ref $value eq 'ARRAY';
926             }
927              
928             sub _is_boolean {
929 13     13   23 my ($value) = @_;
930              
931 13   66     48 return defined $value && JSON::is_bool($value);
932             }
933              
934             sub _is_number {
935 11     11   22 my ($value) = @_;
936              
937 11 50       24 return 0 unless defined $value;
938 11 50       23 return 0 if ref $value;
939 11 50       22 return 0 if JSON::is_bool($value);
940              
941 11         61 my $b_obj = B::svref_2object(\$value);
942 11         29 my $flags = $b_obj->FLAGS;
943 11 100 66     59 return 1
944             if $flags & (B::SVp_IOK() | B::SVp_NOK())
945             && !($flags & B::SVp_POK());
946              
947 4         11 return 0;
948             }
949              
950             sub _is_integer {
951 10     10   17 my ($value) = @_;
952              
953 10 50       25 return 0 unless defined $value;
954 10 50       21 return 0 if ref $value;
955 10 50       21 return 0 if JSON::is_bool($value);
956              
957 10         64 my $b_obj = B::svref_2object(\$value);
958 10         39 my $flags = $b_obj->FLAGS;
959 10 100 66     48 return 1 if ($flags & B::SVp_IOK()) && !($flags & B::SVp_POK());
960              
961 6         15 return 0;
962             }
963              
964             sub _is_string {
965 3     3   6 my ($value) = @_;
966              
967 3 50       23 return 0 unless defined $value;
968 3 100       24 return 0 if ref $value;
969 2 50       5 return 0 if _is_boolean($value);
970 2 50       14 return 0 if _is_number($value);
971              
972 2         8 return 1;
973             }
974              
975             sub _is_null {
976 19     19   37 my ($value) = @_;
977              
978 19 100       74 return defined $value ? 0 : 1;
979             }
980              
981             sub _is_type {
982 10     10   29 my ($value, $type) = @_;
983              
984 10         24 my $real_type = _type($value);
985              
986 10 100       30 if ($type eq 'number') {
987 3 100       11 return 1 if $real_type eq 'integer';
988             }
989              
990 9         38 return $real_type eq $type;
991              
992 0 0       0 return _type($value) eq $type ? 1 : 0;
993             }
994              
995             sub _type {
996 18     18   1204 my ($value) = @_;
997              
998 18 100       38 return 'null' if _is_null($value);
999 16 100       37 return 'object' if _is_object($value);
1000 14 100       36 return 'array' if _is_array($value);
1001 11 100       25 return 'boolean' if _is_boolean($value);
1002 9 100       55 return 'integer' if _is_integer($value);
1003 6 100       13 return 'number' if _is_number($value);
1004 2 50       7 return 'string' if _is_string($value);
1005              
1006 0         0 Carp::croak("Unknown type");
1007             }
1008              
1009             sub _subschema {
1010 1     1   3 my ($schema) = @_;
1011              
1012 1         3 for (qw/allOf anyOf oneOf not/) {
1013 4 50       10 return $_ if $schema->{$_};
1014             }
1015              
1016 1         4 return;
1017             }
1018              
1019             sub _resolve_remote_ref {
1020 0     0   0 my $self = shift;
1021 0         0 my ($context, $ref) = @_;
1022              
1023 0         0 my ($url, $pointer) = $ref =~ m/^([^#]+)(#.*)?$/;
1024              
1025 0         0 my $schema;
1026              
1027 0 0       0 if (exists $context->{ids}->{$url}) {
    0          
1028 0         0 $schema = $context->{ids}->{$url};
1029             }
1030             elsif ($context->{remote_cache}->{$url}) {
1031 0         0 $schema = $context->{remote_cache}->{$url};
1032             }
1033             else {
1034 0         0 $schema = eval { $self->{fetcher}->($url) };
  0         0  
1035 0         0 $context->{remote_cache}->{$url} = $schema;
1036              
1037 0 0       0 if ($schema) {
1038 0   0     0 $schema->{id} //= $url;
1039              
1040 0         0 $self->_collect_ids($context, $schema);
1041             }
1042             }
1043              
1044 0 0 0     0 if ($schema && $pointer) {
1045 0         0 $schema = pointer($schema, $pointer);
1046             }
1047              
1048 0         0 return $schema;
1049             }
1050              
1051             sub _build_result {
1052 4     4   8 my $self = shift;
1053              
1054 4         16 return JSON::SchemaValidator::Result->new;
1055             }
1056              
1057             1;
1058             __END__