blob: 5dedd7dfe95735ad11f407a5affb154992ee237c [file] [log] [blame]
// Copyright 2013-2018 Workiva Inc.
//
// Licensed under the Boost Software License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.boost.org/LICENSE_1_0.txt
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This software or document includes material copied from or derived
// from JSON-Schema-Test-Suite (https://github.com/json-schema-org/JSON-Schema-Test-Suite),
// Copyright (c) 2012 Julian Berman, which is licensed under the following terms:
//
// Copyright (c) 2012 Julian Berman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
part of json_schema.json_schema;
/// Initialized with schema, validates instances against it
class Validator {
Validator(this._rootSchema);
List<String> get errors => _errors;
// custom <class Validator>
/// Validate the [instance] against the this validator's schema
bool validate(dynamic instance, [bool reportMultipleErrors = false]) {
_logger.info("Validating ${instance.runtimeType}:$instance on ${_rootSchema}");
_reportMultipleErrors = reportMultipleErrors;
_errors = [];
if (!_reportMultipleErrors) {
try {
_validate(_rootSchema, instance);
return true;
} on FormatException {
return false;
} catch (e) {
_logger.shout("Unexpected Exception: $e");
return false;
}
}
_validate(_rootSchema, instance);
return _errors.length == 0;
}
static bool _typeMatch(SchemaType type, dynamic instance) {
switch (type) {
case SchemaType.OBJECT:
return instance is Map;
case SchemaType.STRING:
return instance is String;
case SchemaType.INTEGER:
return instance is int;
case SchemaType.NUMBER:
return instance is num;
case SchemaType.ARRAY:
return instance is List;
case SchemaType.BOOLEAN:
return instance is bool;
case SchemaType.NULL:
return instance == null;
}
return false;
}
void _numberValidation(Schema schema, num n) {
var maximum = schema._maximum;
var minimum = schema._minimum;
if (maximum != null) {
if (schema.exclusiveMaximum) {
if (n >= maximum) {
_err("${schema._path}: maximum exceeded ($n >= $maximum)");
}
} else {
if (n > maximum) {
_err("${schema._path}: maximum exceeded ($n > $maximum)");
}
}
} else if (minimum != null) {
if (schema.exclusiveMinimum) {
if (n <= minimum) {
_err("${schema._path}: minimum violated ($n <= $minimum)");
}
} else {
if (n < minimum) {
_err("${schema._path}: minimum violated ($n < $minimum)");
}
}
}
var multipleOf = schema._multipleOf;
if (multipleOf != null) {
if (multipleOf is int && n is int) {
if (0 != n % multipleOf) {
_err("${schema._path}: multipleOf violated ($n % $multipleOf)");
}
} else {
double result = n / multipleOf;
if (result.truncate() != result) {
_err("${schema._path}: multipleOf violated ($n % $multipleOf)");
}
}
}
}
void _typeValidation(Schema schema, dynamic instance) {
var typeList = schema._schemaTypeList;
if (typeList != null && typeList.length > 0) {
if (!typeList.any((type) => _typeMatch(type, instance))) {
_err("${schema._path}: type: wanted ${typeList} got $instance");
}
}
}
void _enumValidation(Schema schema, dynamic instance) {
var enumValues = schema._enumValues;
if (enumValues.length > 0) {
try {
enumValues.singleWhere((v) => _jsonEqual(instance, v));
} on StateError {
_err("${schema._path}: enum violated ${instance}");
}
}
}
void _stringValidation(Schema schema, String instance) {
int actual = instance.length;
var minLength = schema._minLength;
var maxLength = schema._maxLength;
if (maxLength is int && actual > maxLength) {
_err("${schema._path}: maxLength exceeded ($instance vs $maxLength)");
} else if (minLength is int && actual < minLength) {
_err("${schema._path}: minLength violated ($instance vs $minLength)");
}
var pattern = schema._pattern;
if (pattern != null && !pattern.hasMatch(instance)) {
_err("${schema._path}: pattern violated ($instance vs $pattern)");
}
}
void _itemsValidation(Schema schema, dynamic instance) {
int actual = instance.length;
var singleSchema = schema._items;
if (singleSchema != null) {
instance.forEach((item) => _validate(singleSchema, item));
} else {
var items = schema._itemsList;
var additionalItems = schema._additionalItems;
if (items != null) {
int expected = items.length;
int end = min(expected, actual);
for (int i = 0; i < end; i++) {
assert(items[i] != null);
_validate(items[i], instance[i]);
}
if (additionalItems is Schema) {
for (int i = end; i < actual; i++) {
_validate(additionalItems, instance[i]);
}
} else if (additionalItems is bool) {
if (!additionalItems && actual > end) {
_err("${schema._path}: additionalItems false");
}
}
}
}
var maxItems = schema._maxItems;
var minItems = schema._minItems;
if (maxItems is int && actual > maxItems) {
_err("${schema._path}: maxItems exceeded ($actual vs $maxItems)");
} else if (schema._minItems is int && actual < schema._minItems) {
_err("${schema._path}: minItems violated ($actual vs $minItems)");
}
if (schema._uniqueItems) {
int end = instance.length;
int penultimate = end - 1;
for (int i = 0; i < penultimate; i++) {
for (int j = i + 1; j < end; j++) {
if (_jsonEqual(instance[i], instance[j])) {
_err("${schema._path}: uniqueItems violated: $instance [$i]==[$j]");
}
}
}
}
}
void _validateAllOf(Schema schema, instance) {
List<Schema> schemas = schema._allOf;
int errorsSoFar = _errors.length;
int i = 0;
schemas.every((s) {
assert(s != null);
_validate(s, instance);
bool valid = _errors.length == errorsSoFar;
if (!valid) {
_err("${s._path}/$i: allOf violated ${instance}");
}
i++;
return valid;
});
}
void _validateAnyOf(Schema schema, instance) {
if (!schema._anyOf.any((s) => new Validator(s).validate(instance))) {
_err("${schema._path}/anyOf: anyOf violated ($instance, ${schema._anyOf})");
}
}
void _validateOneOf(Schema schema, instance) {
try {
schema._oneOf.singleWhere((s) => new Validator(s).validate(instance));
} on StateError catch (notOneOf) {
_err("${schema._path}/oneOf: violated ${notOneOf.message}");
}
}
void _validateNot(Schema schema, instance) {
if (new Validator(schema._notSchema).validate(instance)) {
_err("${schema._notSchema._path}: not violated");
}
}
void _validateFormat(Schema schema, instance) {
switch (schema._format) {
case 'date-time':
{
try {
DateTime.parse(instance);
} catch (e) {
_err("'date-time' format not accepted $instance");
}
}
break;
case 'uri':
{
var isValid = (_uriValidator != null) ? _uriValidator : _defaultUriValidator;
if (!isValid(instance)) {
_err("'uri' format not accepted $instance");
}
}
break;
case 'email':
{
var isValid = (_emailValidator != null) ? _emailValidator : _defaultEmailValidator;
if (!isValid(instance)) {
_err("'email' format not accepted $instance");
}
}
break;
case 'ipv4':
{
if (_ipv4Re.firstMatch(instance) == null) {
_err("'ipv4' format not accepted $instance");
}
}
break;
case 'ipv6':
{
if (_ipv6Re.firstMatch(instance) == null) {
_err("'ipv6' format not accepted $instance");
}
}
break;
case 'hostname':
{
if (_hostnameRe.firstMatch(instance) == null) {
_err("'hostname' format not accepted $instance");
}
}
break;
default:
{
_err("${schema._format} not supported as format");
}
}
}
void _objectPropertyValidation(Schema schema, Map instance) {
bool propMustValidate = schema._additionalProperties != null && !schema._additionalProperties;
instance.forEach((k, v) {
bool propCovered = false;
Schema propSchema = schema._properties[k];
if (propSchema != null) {
assert(propSchema != null);
_validate(propSchema, v);
propCovered = true;
}
schema._patternProperties.forEach((regex, patternSchema) {
if (regex.hasMatch(k)) {
assert(patternSchema != null);
_validate(patternSchema, v);
propCovered = true;
}
});
if (!propCovered) {
if (schema._additionalPropertiesSchema != null) {
_validate(schema._additionalPropertiesSchema, v);
} else if (propMustValidate) {
_err("${schema._path}: unallowed additional property $k");
}
}
});
}
void _propertyDependenciesValidation(Schema schema, Map instance) {
schema._propertyDependencies.forEach((k, dependencies) {
if (instance.containsKey(k)) {
if (!dependencies.every((prop) => instance.containsKey(prop))) {
_err("${schema._path}: prop $k => $dependencies required");
}
}
});
}
void _schemaDependenciesValidation(Schema schema, Map instance) {
schema._schemaDependencies.forEach((k, otherSchema) {
if (instance.containsKey(k)) {
if (!new Validator(otherSchema).validate(instance)) {
_err("${otherSchema._path}: prop $k violated schema dependency");
}
}
});
}
void _objectValidation(Schema schema, Map instance) {
int numProps = instance.length;
int minProps = schema._minProperties;
int maxProps = schema._maxProperties;
if (numProps < minProps) {
_err("${schema._path}: minProperties violated (${numProps} < ${minProps})");
} else if (maxProps != null && numProps > maxProps) {
_err("${schema._path}: maxProperties violated (${numProps} > ${maxProps})");
}
if (schema._requiredProperties != null) {
schema._requiredProperties.forEach((prop) {
if (!instance.containsKey(prop)) {
_err("${schema._path}: required prop missing: ${prop} from $instance");
}
});
}
_objectPropertyValidation(schema, instance);
if (schema._propertyDependencies != null) _propertyDependenciesValidation(schema, instance);
if (schema._schemaDependencies != null) _schemaDependenciesValidation(schema, instance);
}
void _validate(Schema schema, dynamic instance) {
_typeValidation(schema, instance);
_enumValidation(schema, instance);
if (instance is List) _itemsValidation(schema, instance);
if (instance is String) _stringValidation(schema, instance);
if (instance is num) _numberValidation(schema, instance);
if (schema._allOf.length > 0) _validateAllOf(schema, instance);
if (schema._anyOf.length > 0) _validateAnyOf(schema, instance);
if (schema._oneOf.length > 0) _validateOneOf(schema, instance);
if (schema._notSchema != null) _validateNot(schema, instance);
if (schema._format != null) _validateFormat(schema, instance);
if (instance is Map) _objectValidation(schema, instance);
}
void _err(String msg) {
_logger.warning(msg);
_errors.add(msg);
if (!_reportMultipleErrors) throw new FormatException(msg);
}
// end <class Validator>
Schema _rootSchema;
List<String> _errors = [];
bool _reportMultipleErrors;
}
// custom <part validator>
// end <part validator>
RegExp _emailRe = new RegExp(r'^[_A-Za-z0-9-\+]+(\.[_A-Za-z0-9-]+)*'
r'@'
r'[A-Za-z0-9-]+(\.[A-Za-z0-9]+)*(\.[A-Za-z]{2,})$');
var _defaultEmailValidator = (String email) => _emailRe.firstMatch(email) != null;
var _emailValidator = _defaultEmailValidator;
RegExp _ipv4Re = new RegExp(r'^(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.'
r'(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.'
r'(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.'
r'(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))$');
RegExp _ipv6Re = new RegExp(r'(^([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,6}$)|'
r'(^([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}$)|'
r'(^([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}$)|'
r'(^([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}$)|'
r'(^([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}$)|'
r'(^([0-9a-f]{1,4}:){1,6}(:[0-9a-f]{1,4}){1,1}$)|'
r'(^(([0-9a-f]{1,4}:){1,7}|:):$)|'
r'(^:(:[0-9a-f]{1,4}){1,7}$)|'
r'(^((([0-9a-f]{1,4}:){6})(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})$)|'
r'(^(([0-9a-f]{1,4}:){5}[0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})$)|'
r'(^([0-9a-f]{1,4}:){5}:[0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$)|'
r'(^([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$)|'
r'(^([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,3}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$)|'
r'(^([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,2}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$)|'
r'(^([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,1}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$)|'
r'(^(([0-9a-f]{1,4}:){1,5}|:):(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$)|'
r'(^:(:[0-9a-f]{1,4}){1,5}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$)');
var _defaultUriValidator = _defaultUriValidatorImpl;
bool _defaultUriValidatorImpl(String uri) {
try {
final result = Uri.parse(uri);
if (result.path.startsWith('//')) return false;
return true;
} catch (e) {
return false;
}
}
RegExp _hostnameRe = new RegExp(r'^(?=.{1,255}$)'
r'[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?'
r'(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$');
var _uriValidator = _defaultUriValidator;