package JSONSchema::Validator::Constraints::Draft4;
# ABSTRACT: JSON Schema Draft4 specification constraints
use strict;
use warnings;
use Scalar::Util 'weaken';
use URI;
use Carp 'croak';
use JSONSchema::Validator::Error 'error';
use JSONSchema::Validator::JSONPointer 'json_pointer';
use JSONSchema::Validator::Util qw(serialize unbool round is_type detect_type);
use JSONSchema::Validator::Format qw(
validate_date_time validate_date validate_time
validate_email validate_hostname
validate_idn_email
validate_ipv4 validate_ipv6
validate_uuid
validate_byte
validate_int32 validate_int64
validate_float validate_double
validate_regex
validate_json_pointer validate_relative_json_pointer
validate_uri validate_uri_reference
validate_iri validate_iri_reference
validate_uri_template
);
use constant FORMAT_VALIDATIONS => {
'date-time' => ['string', \&validate_date_time],
'date' => ['string', \&validate_date],
'time' => ['string', \&validate_time],
'email' => ['string', \&validate_email],
'idn-email' => ['string', \&validate_idn_email],
'hostname' => ['string', \&validate_hostname],
'ipv4' => ['string', \&validate_ipv4],
'ipv6' => ['string', \&validate_ipv6],
'uuid' => ['string', \&validate_uuid],
'byte' => ['string', \&validate_byte],
'int32' => ['integer', \&validate_int32],
'int64' => ['integer', \&validate_int64],
'float' => ['number', \&validate_float],
'double' => ['number', \&validate_double],
'regex' => ['string', \&validate_regex],
'json-pointer' => ['string', \&validate_json_pointer],
'relative-json-pointer' => ['string', \&validate_relative_json_pointer],
'uri' => ['string', \&validate_uri],
'uri-reference' => ['string', \&validate_uri_reference],
'iri' => ['string', \&validate_iri],
'iri-reference' => ['string', \&validate_iri_reference],
'uri-template' => ['string', \&validate_uri_template]
};
sub new {
my ($class, %params) = @_;
my $validator = $params{validator} or croak 'validator is required';
my $strict = $params{strict} // 1;
weaken($validator);
my $self = {
validator => $validator,
errors => [],
strict => $strict
};
bless $self, $class;
return $self;
}
sub validator { shift->{validator} }
sub strict { shift->{strict} }
# params: $self, $value, $type, $strict
sub check_type {
return is_type($_[1], $_[2], $_[3] // $_[0]->strict);
}
sub type {
my ($self, $instance, $types, $schema, $instance_path, $schema_path, $data) = @_;
my @types = ref $types ? @$types : ($types);
return 1 if grep { $self->check_type($instance, $_) } @types;
my $actual_type = detect_type($instance);
push @{$data->{errors}}, error(
message => "type mismatch (expecting any of (@types), found: $actual_type)",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub minimum {
my ($self, $instance, $minimum, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'number');
return 1 if $instance >= $minimum;
push @{$data->{errors}}, error(
message => "${instance} is less than ${minimum}",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub maximum {
my ($self, $instance, $maximum, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'number');
return 1 if $instance <= $maximum;
push @{$data->{errors}}, error(
message => "${instance} is greater than ${maximum}",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub exclusiveMaximum {
my ($self, $instance, $exclusiveMaximum, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'number');
return 1 unless exists $schema->{maximum};
my $maximum = $schema->{maximum};
my $res = $self->maximum($instance, $maximum, $schema, $instance_path, $schema_path, $data);
return 0 unless $res;
return 1 unless $exclusiveMaximum;
return 1 if $instance != $maximum;
push @{$data->{errors}}, error(
message => "${instance} is equal to ${maximum}",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub exclusiveMinimum {
my ($self, $instance, $exclusiveMinimum, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'number');
return 1 unless exists $schema->{minimum};
my $minimum = $schema->{minimum};
my $res = $self->minimum($instance, $minimum, $schema, $instance_path, $schema_path, $data);
return 0 unless $res;
return 1 unless $exclusiveMinimum;
return 1 if $instance != $minimum;
push @{$data->{errors}}, error(
message => "${instance} is equal to ${minimum}",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub minItems {
my ($self, $instance, $min, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'array');
return 1 if scalar(@$instance) >= $min;
push @{$data->{errors}}, error(
message => "minItems (>= ${min}) constraint violated",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub maxItems {
my ($self, $instance, $max, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'array');
return 1 if scalar(@$instance) <= $max;
push @{$data->{errors}}, error(
message => "maxItems (<= ${max}) constraint violated",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub minLength {
my ($self, $instance, $min, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'string');
return 1 if length $instance >= $min;
push @{$data->{errors}}, error(
message => "minLength (>= ${min}) constraint violated",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub maxLength {
my ($self, $instance, $max, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'string');
return 1 if length $instance <= $max;
push @{$data->{errors}}, error(
message => "maxLength (<= ${max}) constraint violated",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub dependencies {
my ($self, $instance, $dependencies, $schema, $instance_path, $schema_path, $data) = @_;
# ignore non-object
return 1 unless $self->check_type($instance, 'object');
my $result = 1;
for my $prop (keys %$dependencies) {
next unless exists $instance->{$prop};
my $dep = $dependencies->{$prop};
my $spath = json_pointer->append($schema_path, $prop);
# need strict check beacase of schema check
if ($self->check_type($dep, 'array', 1)) {
for my $idx (0 .. $#{$dep}) {
my $p = $dep->[$idx];
next if exists $instance->{$p};
push @{$data->{errors}}, error(
message => "dependencies constraint violated: property $p is ommited",
instance_path => $instance_path,
schema_path => json_pointer->append($spath, $idx)
);
$result = 0;
}
} else {
# $dep is object or boolean (starting draft 6 boolean is valid schema)
my $r = $self->validator->_validate_schema($instance, $dep, $instance_path, $spath, $data);
$result = 0 unless $r;
}
}
return $result;
}
sub additionalItems {
my ($self, $instance, $additionalItems, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'array');
# need strict check beacase of schema check
return 1 if $self->check_type($schema->{items} // {}, 'object', 1);
my $len_items = scalar @{$schema->{items}};
# need strict check beacase of schema check
if ($self->check_type($additionalItems, 'boolean', 1)) {
return 1 if $additionalItems;
if (scalar @$instance > $len_items) {
push @{$data->{errors}}, error(
message => 'additionalItems constraint violated',
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
return 1;
}
# additionalItems is object
my $result = 1;
my @items_last_part = @$instance[$len_items .. $#{$instance}];
for my $index (0 .. $#items_last_part) {
my $item = $items_last_part[$index];
my $ipath = json_pointer->append($instance_path, $len_items + $index);
my $r = $self->validator->_validate_schema($item, $additionalItems, $ipath, $schema_path, $data);
$result = 0 unless $r;
}
return $result;
}
sub additionalProperties {
my ($self, $instance, $addProps, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'object');
my $patterns = join '|', keys %{$schema->{patternProperties} // {}};
my @extra_props;
for my $p (keys %$instance) {
next if $schema->{properties} && exists $schema->{properties}{$p};
next if $patterns && $p =~ m/$patterns/u;
push @extra_props, $p;
}
return 1 unless @extra_props;
# need strict check beacase of schema check
if ($self->check_type($addProps, 'object', 1)) {
my $result = 1;
for my $p (@extra_props) {
my $ipath = json_pointer->append($instance_path, $p);
my $r = $self->validator->_validate_schema($instance->{$p}, $addProps, $ipath, $schema_path, $data);
$result = 0 unless $r;
}
return $result;
}
# addProps is boolean
return 1 if $addProps;
push @{$data->{errors}}, error(
message => 'additionalProperties constraint violated; properties: ' . join(', ', @extra_props),
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub allOf {
my ($self, $instance, $allOf, $schema, $instance_path, $schema_path, $data) = @_;
my $result = 1;
for my $idx (0 .. $#{$allOf}) {
my $subschema = $allOf->[$idx];
my $spath = json_pointer->append($schema_path, $idx);
my $r = $self->validator->_validate_schema($instance, $subschema, $instance_path, $spath, $data);
$result = 0 unless $r;
}
return $result;
}
sub anyOf {
my ($self, $instance, $anyOf, $schema, $instance_path, $schema_path, $data) = @_;
my $errors = $data->{errors};
my $local_errors = [];
my $result = 0;
for my $idx (0 .. $#$anyOf) {
$data->{errors} = [];
my $spath = json_pointer->append($schema_path, $idx);
$result = $self->validator->_validate_schema($instance, $anyOf->[$idx], $instance_path, $spath, $data);
unless ($result) {
push @{$local_errors}, error(
message => qq'${idx} part of "anyOf" has errors',
context => $data->{errors},
instance_path => $instance_path,
schema_path => $spath
);
}
last if $result;
}
$data->{errors} = $errors;
return 1 if $result;
push @{$data->{errors}}, error(
message => 'instance does not satisfy any schema of "anyOf"',
context => $local_errors,
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub oneOf {
my ($self, $instance, $oneOf, $schema, $instance_path, $schema_path, $data) = @_;
my $errors = $data->{errors};
my ($local_errors, $valid_schemas) = ([], []);
my $num = 0;
for my $idx (0 .. $#$oneOf) {
$data->{errors} = [];
my $spath = json_pointer->append($schema_path, $idx);
my $r = $self->validator->_validate_schema($instance, $oneOf->[$idx], $instance_path, $spath, $data);
if ($r) {
push @{$valid_schemas}, $spath;
} else {
push @{$local_errors}, error(
message => qq'${idx} part of "oneOf" has errors',
context => $data->{errors},
instance_path => $instance_path,
schema_path => $spath
);
}
++$num if $r;
}
$data->{errors} = $errors;
return 1 if $num == 1;
if ($num > 1) {
push @{$data->{errors}}, error(
message => 'instance is valid under more than one schema of "oneOf": ' . join(' ', @$valid_schemas),
instance_path => $instance_path,
schema_path => $schema_path
);
} else {
push @{$data->{errors}}, error(
message => 'instance is not valid under any of given schemas of "oneOf"',
context => $local_errors,
instance_path => $instance_path,
schema_path => $schema_path
);
}
return 0;
}
sub enum {
my ($self, $instance, $enum, $schema, $instance_path, $schema_path, $data) = @_;
my $result = 0;
for my $e (@$enum) {
# schema must have strict check
if ($self->check_type($e, 'boolean', 1)) {
$result = $self->check_type($instance, 'boolean')
? unbool($instance) eq unbool($e)
: 0
} elsif ($self->check_type($e, 'object', 1) || $self->check_type($e, 'array', 1)) {
$result = $self->check_type($instance, 'object') ||
$self->check_type($instance, 'array')
? serialize($instance) eq serialize($e)
: 0;
} elsif ($self->check_type($e, 'number', 1)) {
$result = $self->check_type($instance, 'number')
? $e == $instance
: 0;
} elsif (defined $e && defined $instance) {
$result = $e eq $instance;
} elsif (!defined $e && !defined $instance) {
$result = 1;
} else {
$result = 0;
}
last if $result;
}
return 1 if $result;
push @{$data->{errors}}, error(
message => "instance is not of enums",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub items {
my ($self, $instance, $items, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'array');
my $result = 1;
if ($self->check_type($items, 'array', 1)) {
my $min = $#{$items} > $#{$instance} ? $#{$instance} : $#{$items};
for my $i (0 .. $min) {
my $item = $instance->[$i];
my $subschema = $items->[$i];
my $spath = json_pointer->append($schema_path, $i);
my $ipath = json_pointer->append($instance_path, $i);
my $r = $self->validator->_validate_schema($item, $subschema, $ipath, $spath, $data);
$result = 0 unless $r;
}
} else {
# items is object
for my $i (0 .. $#{$instance}) {
my $item = $instance->[$i];
my $ipath = json_pointer->append($instance_path, $i);
my $r = $self->validator->_validate_schema($item, $items, $ipath, $schema_path, $data);
$result = 0 unless $r;
}
}
return $result;
}
sub format {
my ($self, $instance, $format, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless exists FORMAT_VALIDATIONS->{$format};
my ($type, $checker) = @{FORMAT_VALIDATIONS->{$format}};
return 1 unless $self->check_type($instance, $type);
my $result = $checker->($instance);
return 1 if $result;
push @{$data->{errors}}, error(
message => "instance is not $format",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub maxProperties {
my ($self, $instance, $maxProperties, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'object');
return 1 if scalar(keys %$instance) <= $maxProperties;
push @{$data->{errors}}, error(
message => "instance has more than $maxProperties properties",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub minProperties {
my ($self, $instance, $minProperties, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'object');
return 1 if scalar(keys %$instance) >= $minProperties;
push @{$data->{errors}}, error(
message => "instance has less than $minProperties properties",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub multipleOf {
my ($self, $instance, $multipleOf, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'number');
my $result = 1;
my $div = $instance / $multipleOf;
$result = 0 if $div == 'Inf' || int($div) != $div;
return 1 if $result;
push @{$data->{errors}}, error(
message => "instance is not multiple of $multipleOf",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub not {
my ($self, $instance, $not, $schema, $instance_path, $schema_path, $data) = @_;
my $errors = $data->{errors};
$data->{errors} = [];
# not is schema
my $result = $self->validator->_validate_schema($instance, $not, $instance_path, $schema_path, $data);
$data->{errors} = $errors;
return 1 unless $result;
push @{$data->{errors}}, error(
message => 'instance satisfies the schema defined in \"not\" keyword',
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub pattern {
my ($self, $instance, $pattern, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'string');
return 1 if $instance =~ m/$pattern/u;
push @{$data->{errors}}, error(
message => "instance does not match $pattern",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub patternProperties {
my ($self, $instance, $patternProperties, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'object');
my $result = 1;
for my $pattern (keys %$patternProperties) {
my $subschema = $patternProperties->{$pattern};
my $spath = json_pointer->append($schema_path, $pattern);
for my $k (keys %$instance) {
my $v = $instance->{$k};
if ($k =~ m/$pattern/u) {
my $ipath = json_pointer->append($instance_path, $k);
my $r = $self->validator->_validate_schema($v, $subschema, $ipath, $spath, $data);
$result = 0 unless $r;
}
}
}
return $result;
}
sub properties {
my ($self, $instance, $properties, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'object');
my $result = 1;
for my $prop (keys %$properties) {
next unless exists $instance->{$prop};
my $subschema = $properties->{$prop};
my $spath = json_pointer->append($schema_path, $prop);
my $ipath = json_pointer->append($instance_path, $prop);
my $r = $self->validator->_validate_schema($instance->{$prop}, $subschema, $ipath, $spath, $data);
$result = 0 unless $r;
}
return $result;
}
sub required {
my ($self, $instance, $required, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'object');
my $result = 1;
for my $idx (0 .. $#{$required}) {
my $prop = $required->[$idx];
next if exists $instance->{$prop};
push @{$data->{errors}}, error(
message => qq{instance does not have required property "$prop"},
instance_path => $instance_path,
schema_path => json_pointer->append($schema_path, $idx)
);
$result = 0;
}
return $result;
}
# doesn't work for string that looks like number with the same number in array
sub uniqueItems {
my ($self, $instance, $uniqueItems, $schema, $instance_path, $schema_path, $data) = @_;
return 1 unless $self->check_type($instance, 'array');
# uniqueItems is boolean
return 1 unless $uniqueItems;
my %hash = map {
my $type = detect_type($_, $self->strict);
my $value;
if ($type eq 'null') {
$value = ''
} elsif ($type eq 'object' || $type eq 'array') {
$value = serialize($_);
} elsif ($type eq 'boolean') {
$value = "$_";
} else {
# integer/number/string
$value = $_;
}
my $key = "${type}#${value}";
$key => 1;
} @$instance;
return 1 if scalar(keys %hash) == scalar @$instance;
push @{$data->{errors}}, error(
message => "instance has non-unique elements",
instance_path => $instance_path,
schema_path => $schema_path
);
return 0;
}
sub ref {
my ($self, $instance, $ref, $origin_schema, $instance_path, $schema_path, $data) = @_;
my $scope = $self->validator->scope;
$ref = URI->new($ref);
$ref = $ref->abs($scope) if $scope;
my ($current_scope, $schema) = $self->validator->resolver->resolve($ref);
croak "schema not resolved by ref $ref" unless $schema;
push @{$self->validator->scopes}, $current_scope;
my $result = eval {
$self->validator->_validate_schema($instance, $schema, $instance_path, $schema_path, $data, apply_scope => 0);
};
if ($@) {
$result = 0;
push @{$data->{errors}}, error(
message => "exception: $@",
instance_path => $instance_path,
schema_path => $schema_path
);
}
pop @{$self->validator->scopes};
return $result;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
JSONSchema::Validator::Constraints::Draft4 - JSON Schema Draft4 specification constraints
=head1 VERSION
version 0.011
=head1 AUTHORS
=over 4
=item *
Alexey Stavrov <logioniz@ya.ru>
=item *
Ivan Putintsev <uid@rydlab.ru>
=item *
Anton Fedotov <tosha.fedotov.2000@gmail.com>
=item *
Denis Ibaev <dionys@gmail.com>
=item *
Andrey Khozov <andrey@rydlab.ru>
=back
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2021 by Alexey Stavrov.
This is free software, licensed under:
The MIT (X11) License
=cut