| // 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; |