File Coverage

blib/lib/JSON/SchemaValidator.pm
Criterion Covered Total %
statement 136 453 30.0
branch 61 344 17.7
condition 16 60 26.6
subroutine 25 38 65.7
pod 0 3 0.0
total 238 898 26.5


line stmt bran cond sub pod time code
1             package JSON::SchemaValidator;
2              
3 3     3   235963 use strict;
  3         38  
  3         91  
4 3     3   17 use warnings;
  3         6  
  3         143  
5              
6             our $VERSION = '1.03';
7              
8 3     3   23 use B ();
  3         7  
  3         44  
9 3     3   1983 use Storable ();
  3         9455  
  3         99  
10             require Carp;
11 3     3   1762 use Time::Piece;
  3         34681  
  3         68  
12              
13 3     3   1569 use JSON::SchemaValidator::Result;
  3         9  
  3         108  
14 3     3   1248 use JSON::SchemaValidator::Pointer qw(pointer);
  3         11  
  3         19033  
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 95 my $class = shift;
45 1         3 my (%params) = @_;
46              
47 1         5 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         26 };
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 1     1 0 1176 my $self = shift;
127 1         3 my ($json, $schema) = @_;
128              
129 1         116 $schema = Storable::dclone($schema);
130              
131 1         7 my $context = {
132             root => $schema,
133             ids => {},
134             pointer => '#',
135             };
136              
137 1         4 $self->_collect_ids($context, $schema);
138              
139 1         3 my $result = $self->_validate($context, $json, $schema);
140              
141 1         6 return $result;
142             }
143              
144             sub _collect_ids {
145 2     2   5 my $self = shift;
146 2         5 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     6 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     4 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         5 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   2 my $self = shift;
231 1         6 my ($context, $schema) = @_;
232              
233 1 50       5 if (_is_object($schema)) {
    0          
234 1 50 33     5 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   3 my $self = shift;
293 1         3 my ($context, $json, $schema) = @_;
294              
295 1         3 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       4 if (_is_object($schema)) {
302 1 50       5 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       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 1 50       3 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         3 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       7 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       5 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         3 return $result;
358             }
359              
360             sub _validate_type {
361 1     1   3 my $self = shift;
362 1         3 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         3 my @results;
369 1         3 foreach my $type (@$types) {
370 1 50       3 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     7 if (@results && !grep { $_->is_success } @results) {
  1         3  
389 1 50       8 if (@results == 1) {
390 1         4 $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         7 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              
474             # Required only if a boolean, otherwise it's a list of required properties
475 0 0 0     0 if (exists $schema->{properties}->{$key}->{required}
476             && _is_boolean($schema->{properties}->{$key}->{required}))
477             {
478 0         0 push @required, $key;
479             }
480             }
481             }
482              
483 0 0       0 if (exists $schema->{dependencies}) {
484 0         0 foreach my $dependency (keys %{$schema->{dependencies}}) {
  0         0  
485 0 0       0 next unless exists $json->{$dependency};
486              
487 0 0       0 if (_is_array($schema->{dependencies}->{$dependency})) {
    0          
488 0         0 push @required, @{$schema->{dependencies}->{$dependency}};
  0         0  
489             }
490             elsif (_is_object($schema->{dependencies}->{$dependency})) {
491 0         0 my $dependency_schema = $schema->{dependencies}->{$dependency};
492              
493 0         0 foreach my $key (keys %$dependency_schema) {
494 0 0       0 if ($key eq 'required') {
495 0         0 push @required, @{$dependency_schema->{$key}};
  0         0  
496             }
497             else {
498 0         0 $schema->{$key} = $dependency_schema->{$key};
499             }
500             }
501             }
502             }
503             }
504              
505 0 0       0 if (defined(my $min_properties = $schema->{minProperties})) {
506 0 0       0 if (keys %$json < $min_properties) {
507             $result->add_error(
508             uri => $context->{pointer},
509 0         0 message => "Must have minimum " . $min_properties . ' property(ies)',
510             attribute => 'minProperties',
511             details => [$min_properties]
512             );
513             }
514             }
515              
516 0 0       0 if (defined(my $max_properties = $schema->{maxProperties})) {
517 0 0       0 if (keys %$json > $max_properties) {
518             $result->add_error(
519             uri => $context->{pointer},
520 0         0 message => "Must have maximum " . $max_properties . ' property(ies)',
521             attribute => 'maxProperties',
522             details => [$max_properties]
523             );
524             }
525             }
526              
527 0 0       0 if (@required) {
528 0         0 foreach my $name (@required) {
529 0 0       0 if (!exists $json->{$name}) {
530 0         0 $result->add_error(
531             uri => "$context->{pointer}/$name",
532             message => 'Required',
533             attribute => 'required',
534             details => ['(true)']
535             );
536             }
537             }
538             }
539              
540 0         0 my @additional_properties = grep { !exists $schema->{properties}->{$_} } keys %$json;
  0         0  
541              
542 0 0       0 if (exists $schema->{additionalProperties}) {
543 0 0 0     0 if (_is_boolean($schema->{additionalProperties}) && !$schema->{additionalProperties}) {
    0          
544 0         0 PROPERTY: foreach my $additional_property (@additional_properties) {
545 0 0       0 if (my $pattern_properties = $schema->{patternProperties}) {
546 0         0 foreach my $pattern_property (keys %$pattern_properties) {
547 0 0       0 next PROPERTY if $additional_property =~ m/$pattern_property/;
548             }
549             }
550              
551             $result->add_error(
552 0         0 uri => "$context->{pointer}/$additional_property",
553             message => 'Unknown property',
554             );
555             }
556             }
557             elsif (_is_object($schema->{additionalProperties})) {
558 0         0 ADDITIONAL_PROPERTY: foreach my $additional_property (@additional_properties) {
559              
560             # patternProperties overwrite additionalProperties
561 0 0       0 if (my $pattern_properties = $schema->{patternProperties}) {
562 0         0 foreach my $pattern_property (keys %$pattern_properties) {
563 0 0       0 next ADDITIONAL_PROPERTY if $additional_property =~ m/$pattern_property/;
564             }
565             }
566              
567             my $subresult = $self->_validate(
568             {%$context, pointer => "$context->{pointer}/$additional_property"},
569             $json->{$additional_property},
570             $schema->{additionalProperties}
571 0         0 );
572 0         0 $result->merge($subresult);
573             }
574             }
575             }
576              
577 0 0       0 if (my $properties = $schema->{properties}) {
578 0         0 foreach my $name (keys %$properties) {
579 0 0       0 if (exists $json->{$name}) {
580             my $subresult = $self->_validate({%$context, pointer => "$context->{pointer}/$name"},
581 0         0 $json->{$name}, $properties->{$name});
582 0         0 $result->merge($subresult);
583             }
584             }
585             }
586              
587 0 0       0 if (_is_object($schema->{patternProperties})) {
588 0         0 foreach my $pattern_property (keys %{$schema->{patternProperties}}) {
  0         0  
589 0         0 my @matched_properties = grep { m/$pattern_property/ } keys %$json;
  0         0  
590              
591 0         0 foreach my $property (@matched_properties) {
592             my $subresult = $self->_validate({%$context, pointer => "$context->{pointer}/$property"},
593 0         0 $json->{$property}, $schema->{patternProperties}->{$pattern_property});
594 0         0 $result->merge($subresult);
595             }
596             }
597             }
598              
599 0         0 return $result;
600             }
601              
602             sub _validate_array {
603 1     1   5 my $self = shift;
604 1         5 my ($context, $json, $schema) = @_;
605              
606 1         3 my $result = $self->_build_result(root => $context->{pointer});
607              
608 1 50       5 if (defined(my $min_items = $schema->{minItems})) {
609 0 0       0 if (@$json < $min_items) {
610             $result->add_error(
611             uri => $context->{pointer},
612 0         0 message => "Must have minimum " . $min_items . ' item(s)',
613             attribute => 'minItems',
614             details => [$min_items],
615             );
616             }
617             }
618              
619 1 50       6 if (defined(my $max_items = $schema->{maxItems})) {
620 0 0       0 if (@$json > $max_items) {
621             $result->add_error(
622             uri => $context->{pointer},
623 0         0 message => "Must have maximum " . $max_items . ' item(s)',
624             attribute => 'maxItems',
625             details => [$max_items],
626             );
627             }
628             }
629              
630 1 50       5 if (_is_array($schema->{items})) {
631 0         0 my $exp_length = @{$schema->{items}};
  0         0  
632 0         0 my $got_length = @$json;
633              
634 0         0 for (my $i = 0; $i < @{$schema->{items}}; $i++) {
  0         0  
635 0 0       0 last if @$json < $i + 1;
636              
637             my $subresult =
638 0         0 $self->_validate({%$context, pointer => "$context->{pointer}\[$i]"}, $json->[$i], $schema->{items}->[$i]);
639 0         0 $result->merge($subresult);
640             }
641              
642 0 0       0 if ($got_length > $exp_length) {
643 0 0       0 if (_is_boolean($schema->{additionalItems})) {
    0          
644 0 0       0 if (!$schema->{additionalItems}) {
645             $result->add_error(
646             uri => $context->{pointer},
647 0         0 message => "Must have exactly " . @{$schema->{items}} . ' item(s)',
648             attribute => 'additionalItems',
649 0         0 details => [scalar @{$schema->{items}}]
  0         0  
650              
651             );
652             }
653             }
654             elsif (_is_object($schema->{additionalItems})) {
655 0         0 for ($exp_length .. $got_length - 1) {
656             my $subresult = $self->_validate({%$context, pointer => "$context->{pointer}\[$_]"},
657 0         0 $json->[$_], $schema->{additionalItems});
658 0         0 $result->merge($subresult);
659             }
660             }
661             }
662             }
663              
664 1 50       7 if (_is_object($schema->{items})) {
665 0         0 for (my $i = 0; $i < @$json; $i++) {
666             my $subresult =
667 0         0 $self->_validate({%$context, pointer => "$context->{pointer}/$i"}, $json->[$i], $schema->{items});
668 0         0 $result->merge($subresult);
669             }
670             }
671              
672 1 50       6 if ($schema->{uniqueItems}) {
673 0         0 my $seen = {};
674 0         0 foreach my $el (@$json) {
675 0 0       0 my $hash = ref $el ? JSON::encode_json($el) : defined $el ? $el : 'null';
    0          
676              
677 0 0       0 if (exists $seen->{$hash}) {
678             $result->add_error(
679             uri => $context->{pointer},
680 0         0 message => "Must have unique items",
681             attribute => 'uniqueItems',
682             details => ['(true)']
683             );
684 0         0 last;
685             }
686 0         0 $seen->{$hash}++;
687             }
688             }
689              
690 1 50       9 if ($schema->{contains}) {
691 0 0       0 if (!@$json) {
692             $result->add_error(
693             uri => $context->{pointer},
694 0         0 message => "Must not be empty",
695             attribute => 'contains'
696             );
697             }
698             }
699              
700 1         3 return $result;
701             }
702              
703             sub _validate_string {
704 0     0   0 my $self = shift;
705 0         0 my ($context, $json, $schema) = @_;
706              
707 0         0 my $result = $self->_build_result(pointer => $context->{pointer});
708              
709 0 0       0 if (defined(my $max_length = $schema->{maxLength})) {
710 0 0       0 if (length($json) > $max_length) {
711             $result->add_error(
712             uri => $context->{pointer},
713 0         0 message => "Must have the maximum length of $max_length",
714             attribute => 'maxLength',
715             details => [$max_length]
716             );
717             }
718             }
719              
720 0 0       0 if (defined(my $min_length = $schema->{minLength})) {
721 0 0       0 if (length($json) < $min_length) {
722             $result->add_error(
723             uri => $context->{pointer},
724 0         0 message => "Must have the minimum length of $min_length",
725             attribute => 'minLength',
726             details => [$min_length]
727             );
728             }
729             }
730              
731 0 0       0 if (my $pattern = $schema->{pattern}) {
732 0 0       0 if ($json !~ m/$pattern/) {
733             $result->add_error(
734             uri => $context->{pointer},
735 0         0 message => "Must match pattern $pattern",
736             attribute => 'pattern',
737             details => ["$pattern"]
738             );
739             }
740             }
741              
742 0         0 return $result;
743             }
744              
745             sub _validate_number {
746 0     0   0 my $self = shift;
747 0         0 my ($context, $json, $schema) = @_;
748              
749 0         0 my $result = $self->_build_result(pointer => $context->{pointer});
750              
751 0 0       0 if (defined(my $minimum = $schema->{minimum})) {
752 0 0       0 if ($schema->{exclusiveMinimum}) {
753 0 0       0 if ($json <= $minimum) {
754             $result->add_error(
755             uri => $context->{pointer},
756 0         0 message => "Must be greater than or equals to $minimum",
757             attribute => 'minimum',
758             details => [$minimum]
759             );
760             }
761             }
762             else {
763 0 0       0 if ($json < $minimum) {
764             $result->add_error(
765             uri => $context->{pointer},
766 0         0 message => "Must be greater than $minimum",
767             attribute => 'minimum',
768             details => [$minimum]
769             );
770             }
771             }
772             }
773              
774 0 0       0 if (_is_number($schema->{exclusiveMaximum})) {
775 0         0 my $maximum = $schema->{exclusiveMaximum};
776              
777 0 0       0 if ($json >= $maximum) {
778             $result->add_error(
779             uri => $context->{pointer},
780 0         0 message => "Must be less than or equals to $maximum",
781             attribute => 'maximum',
782             details => [$maximum]
783             );
784             }
785             }
786              
787 0 0       0 if (defined(my $maximum = $schema->{maximum})) {
788 0 0       0 if ($schema->{exclusiveMaximum}) {
789 0 0       0 if ($json >= $maximum) {
790             $result->add_error(
791             uri => $context->{pointer},
792 0         0 message => "Must be less than or equals to $maximum",
793             attribute => 'maximum',
794             details => [$maximum]
795             );
796             }
797             }
798             else {
799 0 0       0 if ($json > $maximum) {
800             $result->add_error(
801             uri => $context->{pointer},
802 0         0 message => "Must be less than $maximum",
803             attribute => 'maximum',
804             details => [$maximum]
805             );
806             }
807             }
808             }
809              
810 0 0       0 if (defined(my $divisibleBy = $schema->{divisibleBy})) {
811 0 0       0 if (sprintf('%0.6f', $json) ne sprintf('%0.6f', int($json / $divisibleBy) * $divisibleBy)) {
812             $result->add_error(
813             uri => $context->{pointer},
814 0         0 message => "Must be divisible by $divisibleBy",
815             attribute => 'divisibleBy',
816             details => [$divisibleBy]
817             );
818             }
819             }
820              
821 0 0       0 if (defined(my $multipleOf = $schema->{multipleOf})) {
822 0 0       0 if (sprintf('%0.6f', $json) ne sprintf('%0.6f', int($json / $multipleOf) * $multipleOf)) {
823             $result->add_error(
824             uri => $context->{pointer},
825 0         0 message => "Must be multiple of by $multipleOf",
826             attribute => 'multipleOf',
827             details => [$multipleOf]
828             );
829             }
830             }
831              
832 0         0 return $result;
833             }
834              
835             sub _validate_enum {
836 0     0   0 my $self = shift;
837 0         0 my ($context, $json, $enum) = @_;
838              
839 0         0 my $result = $self->_build_result(pointer => $context->{pointer});
840              
841 0         0 my $set = {};
842 0         0 foreach my $el (@$enum) {
843 0 0       0 my $hash = ref $el ? JSON::encode_json($el) : $el;
844 0         0 $set->{$hash} = 1;
845             }
846              
847 0 0       0 my $hash = ref $json ? JSON::encode_json($json) : defined $json ? $json : 'null';
    0          
848              
849 0 0       0 if (!exists $set->{$hash}) {
850             $result->add_error(
851             uri => $context->{pointer},
852 0         0 message => "Must be one of",
853             attribute => 'enum',
854             details => [@$enum]
855             );
856             }
857              
858 0         0 return $result;
859             }
860              
861             sub _validate_const {
862 0     0   0 my $self = shift;
863 0         0 my ($context, $json, $const) = @_;
864              
865 0         0 my $result = $self->_build_result();
866              
867 0         0 my $exp_type = _type($const);
868              
869 0 0 0     0 if (_is_type($json, $exp_type) || ($exp_type eq 'integer' && _type($json) eq 'number')) {
      0        
870 0 0 0     0 if (_is_object($json) || _is_array($json)) {
    0          
    0          
    0          
871 0 0       0 if (JSON->new->utf8->canonical->encode($json) ne JSON->new->utf8->canonical->encode($const)) {
872             $result->add_error(
873             uri => $context->{pointer},
874 0         0 message => "Must be equal to const",
875             attribute => 'const',
876             );
877             }
878             }
879             elsif (_is_number($json)) {
880 0 0       0 if (sprintf('%0.6f', $const) ne sprintf('%0.6f', $json)) {
881             $result->add_error(
882             uri => $context->{pointer},
883 0         0 message => "Must be of equal to $const",
884             attribute => 'const',
885             details => [$const]
886             );
887             }
888             }
889             elsif (_is_string($json)) {
890 0 0       0 if ($json ne $const) {
891             $result->add_error(
892             uri => $context->{pointer},
893 0         0 message => "Must be of equal to $const",
894             attribute => 'const',
895             details => [$const]
896             );
897             }
898             }
899             elsif (_is_boolean($json)) {
900 0 0       0 if ($const != $json) {
901             $result->add_error(
902             uri => $context->{pointer},
903 0         0 message => "Must be of equal to $const",
904             attribute => 'const',
905             details => [$const]
906             );
907             }
908             }
909             }
910             else {
911             $result->add_error(
912             uri => $context->{pointer},
913 0         0 message => "Must be of type $exp_type",
914             attribute => 'const',
915             details => [$exp_type]
916             );
917             }
918              
919 0         0 return $result;
920             }
921              
922             sub _is_object {
923 23     23   38 my ($value) = @_;
924              
925 23   100     144 return defined $value && ref $value eq 'HASH';
926             }
927              
928             sub _is_array {
929 17     17   32 my ($value) = @_;
930              
931 17   100     79 return defined $value && ref $value eq 'ARRAY';
932             }
933              
934             sub _is_boolean {
935 13     13   22 my ($value) = @_;
936              
937 13   66     48 return defined $value && JSON::is_bool($value);
938             }
939              
940             sub _is_number {
941 11     11   18 my ($value) = @_;
942              
943 11 50       25 return 0 unless defined $value;
944 11 50       24 return 0 if ref $value;
945 11 50       24 return 0 if JSON::is_bool($value);
946              
947 11         57 my $b_obj = B::svref_2object(\$value);
948 11         26 my $flags = $b_obj->FLAGS;
949 11 100 66     54 return 1
950             if $flags & (B::SVp_IOK() | B::SVp_NOK())
951             && !($flags & B::SVp_POK());
952              
953 4         9 return 0;
954             }
955              
956             sub _is_integer {
957 10     10   17 my ($value) = @_;
958              
959 10 50       23 return 0 unless defined $value;
960 10 50       19 return 0 if ref $value;
961 10 50       22 return 0 if JSON::is_bool($value);
962              
963 10         59 my $b_obj = B::svref_2object(\$value);
964 10         45 my $flags = $b_obj->FLAGS;
965 10 100 66     47 return 1 if ($flags & B::SVp_IOK()) && !($flags & B::SVp_POK());
966              
967 6         18 return 0;
968             }
969              
970             sub _is_string {
971 3     3   8 my ($value) = @_;
972              
973 3 50       21 return 0 unless defined $value;
974 3 100       20 return 0 if ref $value;
975 2 50       5 return 0 if _is_boolean($value);
976 2 50       15 return 0 if _is_number($value);
977              
978 2         9 return 1;
979             }
980              
981             sub _is_null {
982 19     19   36 my ($value) = @_;
983              
984 19 100       70 return defined $value ? 0 : 1;
985             }
986              
987             sub _is_type {
988 10     10   31 my ($value, $type) = @_;
989              
990 10         22 my $real_type = _type($value);
991              
992 10 100       31 if ($type eq 'number') {
993 3 100       23 return 1 if $real_type eq 'integer';
994             }
995              
996 9         38 return $real_type eq $type;
997              
998 0 0       0 return _type($value) eq $type ? 1 : 0;
999             }
1000              
1001             sub _type {
1002 18     18   1246 my ($value) = @_;
1003              
1004 18 100       35 return 'null' if _is_null($value);
1005 16 100       45 return 'object' if _is_object($value);
1006 14 100       33 return 'array' if _is_array($value);
1007 11 100       27 return 'boolean' if _is_boolean($value);
1008 9 100       52 return 'integer' if _is_integer($value);
1009 6 100       11 return 'number' if _is_number($value);
1010 2 50       6 return 'string' if _is_string($value);
1011              
1012 0         0 Carp::croak("Unknown type");
1013             }
1014              
1015             sub _subschema {
1016 1     1   2 my ($schema) = @_;
1017              
1018 1         4 for (qw/allOf anyOf oneOf not/) {
1019 4 50       9 return $_ if $schema->{$_};
1020             }
1021              
1022 1         6 return;
1023             }
1024              
1025             sub _resolve_remote_ref {
1026 0     0   0 my $self = shift;
1027 0         0 my ($context, $ref) = @_;
1028              
1029 0         0 my ($url, $pointer) = $ref =~ m/^([^#]+)(#.*)?$/;
1030              
1031 0         0 my $schema;
1032              
1033 0 0       0 if (exists $context->{ids}->{$url}) {
    0          
1034 0         0 $schema = $context->{ids}->{$url};
1035             }
1036             elsif ($context->{remote_cache}->{$url}) {
1037 0         0 $schema = $context->{remote_cache}->{$url};
1038             }
1039             else {
1040 0         0 $schema = eval { $self->{fetcher}->($url) };
  0         0  
1041 0         0 $context->{remote_cache}->{$url} = $schema;
1042              
1043 0 0       0 if ($schema) {
1044 0   0     0 $schema->{id} //= $url;
1045              
1046 0         0 $self->_collect_ids($context, $schema);
1047             }
1048             }
1049              
1050 0 0 0     0 if ($schema && $pointer) {
1051 0         0 $schema = pointer($schema, $pointer);
1052             }
1053              
1054 0         0 return $schema;
1055             }
1056              
1057             sub _build_result {
1058 4     4   8 my $self = shift;
1059              
1060 4         14 return JSON::SchemaValidator::Result->new;
1061             }
1062              
1063             1;
1064             __END__