JSON-Validator/lib/JSON/Validator/Schema/Draft4.pm
package JSON::Validator::Schema::Draft4;
use Mojo::Base 'JSON::Validator::Schema';
use JSON::Validator::Util qw(E data_checksum data_type is_type);
use List::Util 'uniq';
has id => sub {
my $data = shift->data;
return is_type($data, 'HASH') ? $data->{id} || '' : '';
};
has specification => 'http://json-schema.org/draft-04/schema#';
sub _build_formats {
return {
'date-time' => JSON::Validator::Formats->can('check_date_time'),
'email' => JSON::Validator::Formats->can('check_email'),
'hostname' => JSON::Validator::Formats->can('check_hostname'),
'ipv4' => JSON::Validator::Formats->can('check_ipv4'),
'ipv6' => JSON::Validator::Formats->can('check_ipv6'),
'regex' => JSON::Validator::Formats->can('check_regex'),
'uri' => JSON::Validator::Formats->can('check_uri'),
};
}
sub _validate_number_max {
my ($self, $value, $state, $expected) = @_;
return unless defined(my $cmp_with = $state->{schema}{maximum});
my $key = $state->{schema}{exclusiveMaximum} ? 'ex_maximum' : 'maximum';
return if $key eq 'maximum' ? $value <= $cmp_with : $value < $cmp_with;
return E $state->{path}, [$expected => $key => $value, $cmp_with];
}
sub _validate_number_min {
my ($self, $value, $state, $expected) = @_;
return unless defined(my $cmp_with = $state->{schema}{minimum});
my $key = $state->{schema}{exclusiveMinimum} ? 'ex_minimum' : 'minimum';
return if $key eq 'minimum' ? $value >= $cmp_with : $value > $cmp_with;
return E $state->{path}, [$expected => $key => $value, $cmp_with];
}
sub _validate_type_array {
my ($self, $data, $state) = @_;
return E $state->{path}, [array => type => data_type $data] if ref $data ne 'ARRAY';
return (
$self->_validate_type_array_min_max($_[1], $state),
$self->_validate_type_array_unique($_[1], $state),
$self->_validate_type_array_items($_[1], $state),
);
}
sub _validate_type_array_items {
my ($self, $data, $state) = @_;
my ($path, $schema) = @$state{qw(path schema)};
my @errors;
if (ref $schema->{items} eq 'ARRAY') {
my $additional_items = $schema->{additionalItems} // {};
my @rules = @{$schema->{items}};
if ($additional_items) {
push @rules, $additional_items while @rules < @$data;
}
if (@rules >= @$data) {
for my $i (0 .. @$data - 1) {
push @errors, $self->_validate($data->[$i], $self->_state($state, path => [@$path, $i], schema => $rules[$i]));
}
}
elsif (!$additional_items) {
push @errors, E $path, [array => additionalItems => int(@$data), int(@rules)];
}
}
elsif (exists $schema->{items}) {
for my $i (0 .. @$data - 1) {
push @errors,
$self->_validate($data->[$i], $self->_state($state, path => [@$path, $i], schema => $schema->{items}));
}
}
return @errors;
}
sub _validate_type_array_min_max {
my ($self, $data, $state) = @_;
my @errors;
if (defined $state->{schema}{minItems} and $state->{schema}{minItems} > @$data) {
push @errors, E $state->{path}, [array => minItems => int(@$data), $state->{schema}{minItems}];
}
if (defined $state->{schema}{maxItems} and $state->{schema}{maxItems} < @$data) {
push @errors, E $state->{path}, [array => maxItems => int(@$data), $state->{schema}{maxItems}];
}
return @errors;
}
sub _validate_type_array_unique {
my ($self, $data, $state) = @_;
return unless $state->{schema}{uniqueItems};
my (@errors, %uniq);
for (@$data) {
next if !$uniq{data_checksum($_)}++;
push @errors, E $state->{path}, [array => 'uniqueItems'];
last;
}
return @errors;
}
sub _validate_type_object {
my ($self, $data, $state) = @_;
return E $state->{path}, [object => type => data_type $data] if ref $data ne 'HASH';
return (
$self->_validate_type_object_min_max($_[1], $state),
$self->_validate_type_object_dependencies($_[1], $state),
$self->_validate_type_object_properties($_[1], $state),
);
}
sub _validate_type_object_min_max {
my ($self, $data, $state) = @_;
my @errors;
my @dkeys = keys %$data;
if (defined $state->{schema}{maxProperties} and $state->{schema}{maxProperties} < @dkeys) {
push @errors, E $state->{path}, [object => maxProperties => int(@dkeys), $state->{schema}{maxProperties}];
}
if (defined $state->{schema}{minProperties} and $state->{schema}{minProperties} > @dkeys) {
push @errors, E $state->{path}, [object => minProperties => int(@dkeys), $state->{schema}{minProperties}];
}
return @errors;
}
sub _validate_type_object_dependencies {
my ($self, $data, $state) = @_;
my $dependencies = $state->{schema}{dependencies} || {};
my @errors;
for my $k (keys %$dependencies) {
next if not exists $data->{$k};
if (ref $dependencies->{$k} eq 'ARRAY') {
push @errors,
map { E [@{$state->{path}}, $_], [object => dependencies => $k] }
grep { !exists $data->{$_} } @{$dependencies->{$k}};
}
else {
push @errors, $self->_validate($data, $self->_state($state, schema => $dependencies->{$k}));
}
}
return @errors;
}
sub _validate_type_object_properties {
my ($self, $data, $state) = @_;
my ($path, $schema, @errors, %rules) = @$state{qw(path schema)};
my $defaults = $self->{coerce}{defaults};
my @dkeys = keys %$data;
if (my $properties = $schema->{properties}) {
if ($defaults) {
push @{$rules{$_}}, $properties->{$_} for keys %$properties;
}
else {
defined $properties->{$_} && push @{$rules{$_}}, $properties->{$_} for @dkeys;
}
}
for my $p (keys %{$schema->{patternProperties} || {}}) {
my $r = $schema->{patternProperties}{$p};
push @{$rules{$_}}, $r for grep /$p/, @dkeys;
}
my $additional = exists $schema->{additionalProperties} ? $schema->{additionalProperties} : {};
if ($additional) {
$additional = {} unless is_type $additional, 'HASH';
$rules{$_} ||= [$additional] for @dkeys;
}
elsif (my @k = grep { !$rules{$_} } @dkeys) {
local $" = ', ';
return E $path, [object => additionalProperties => join ', ', sort @k];
}
for my $k (uniq @{$schema->{required} || []}) {
next if exists $data->{$k};
push @errors, E [@$path, $k], [object => 'required'];
delete $rules{$k};
}
for my $k (keys %rules) {
for my $r (@{$rules{$k}}) {
next if !exists $data->{$k} and !$defaults;
my $s2 = $self->_state($state, path => [@$path, $k], schema => $r);
if ($defaults and ref $s2->{schema} eq 'HASH' and exists $s2->{schema}{default} and !exists $data->{$k}) {
$data->{$k} = $s2->{schema}{default};
}
next if !exists $data->{$k};
my @e = $self->_validate($data->{$k}, $s2);
push @errors, @e;
next if @e or !is_type $r, 'HASH';
push @errors, $self->_validate_type_enum($data->{$k}, $s2) if $r->{enum};
push @errors, $self->_validate_type_const($data->{$k}, $s2) if $r->{const};
}
}
return @errors;
}
1;
=encoding utf8
=head1 NAME
JSON::Validator::Schema::Draft4 - JSON-Schema Draft 4
=head1 SYNOPSIS
See L<JSON::Validator::Schema/SYNOPSIS>.
=head1 DESCRIPTION
This class represents
L<https://json-schema.org/specification-links.html#draft-4>.
=head1 ATTRIBUTES
=head2 specification
my $str = $schema->specification;
my $schema = $schema->specification($str);
Defaults to "L<http://json-schema.org/draft-04/schema#>".
=head1 SEE ALSO
L<JSON::Validator::Schema>.
=cut