blob: 52f80b1df8cfeec598a5a004dfb3d9726be76645 [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;
/// Constructed with a json schema, either as string or Map. Validation of
/// the schema itself is done on construction. Any errors in the schema
/// result in a FormatException being thrown.
class Schema {
Schema._fromMap(this._root, this._schemaMap, this._path) {
// custom <Schema._fromMap>
_initialize();
_addSchema(path, this);
// end <Schema._fromMap>
}
Schema._fromRootMap(this._schemaMap) {
// custom <Schema._fromRootMap>
_initialize();
// end <Schema._fromRootMap>
}
Schema get root => _root;
Map get schemaMap => _schemaMap;
String get path => _path;
num get multipleOf => _multipleOf;
num get maximum => _maximum;
num get minimum => _minimum;
int get maxLength => _maxLength;
int get minLength => _minLength;
RegExp get pattern => _pattern;
List get enumValues => _enumValues;
List<Schema> get allOf => _allOf;
List<Schema> get anyOf => _anyOf;
List<Schema> get oneOf => _oneOf;
Schema get notSchema => _notSchema;
Map<String, Schema> get definitions => _definitions;
Uri get id => _id;
String get ref => _ref;
String get description => _description;
String get title => _title;
List<SchemaType> get schemaTypeList => _schemaTypeList;
/// To match all items to a schema
Schema get items => _items;
/// To match each item in array to a schema
List<Schema> get itemsList => _itemsList;
dynamic get additionalItems => _additionalItems;
int get maxItems => _maxItems;
int get minItems => _minItems;
bool get uniqueItems => _uniqueItems;
List<String> get requiredProperties => _requiredProperties;
int get maxProperties => _maxProperties;
int get minProperties => _minProperties;
Map<String, Schema> get properties => _properties;
bool get additionalProperties => _additionalProperties;
Schema get additionalPropertiesSchema => _additionalPropertiesSchema;
Map<RegExp, Schema> get patternProperties => _patternProperties;
Map<String, Schema> get schemaDependencies => _schemaDependencies;
Map<String, List<String>> get propertyDependencies => _propertyDependencies;
dynamic get defaultValue => _defaultValue;
/// Map of path to schema object
Map<String, Schema> get refMap => _refMap;
// custom <class Schema>
static Future<Schema> createSchemaFromUrl(String schemaUrl) {
Uri uri = Uri.parse(schemaUrl);
if (uri.scheme == 'http') {
_logger.info('Getting url $uri');
return new HttpClient().getUrl(uri).then((HttpClientRequest request) {
request.followRedirects = true;
return request.close();
}).then((HttpClientResponse response) {
return response.transform(new convert.Utf8Decoder()).join().then((schemaText) {
Map map = convert.JSON.decode(schemaText);
return createSchema(map);
});
});
} else if (uri.scheme == 'file' || uri.scheme == '') {
return new File(uri.scheme == 'file' ? uri.toFilePath() : schemaUrl)
.readAsString()
.then((text) => createSchema(convert.JSON.decode(text)));
} else {
throw new FormatException("Url schemd must be http, file, or empty: $schemaUrl");
}
}
/// Create a schema from a [data]
/// Typically [data] is result of JSON.decode(jsonSchemaString)
static Future<Schema> createSchema(Map data) => new Schema._fromRootMap(data)._thisCompleter.future;
/// Validate [instance] against this schema
bool validate(dynamic instance) => new Validator(this).validate(instance);
bool get exclusiveMaximum => _exclusiveMaximum == null || _exclusiveMaximum;
bool get exclusiveMinimum => _exclusiveMinimum == null || _exclusiveMinimum;
bool propertyRequired(String property) => _requiredProperties != null && _requiredProperties.contains(property);
/// Given path, follow all references to an end path pointing to schema
String endPath(String path) {
_pathsEncountered.clear();
return _endPath(path);
}
/// Returns paths of all paths
Set get paths => new Set.from(_schemaRefs.keys)..addAll(_refMap.keys);
/// Method to find schema from path
Schema resolvePath(String path) {
while (_schemaRefs.containsKey(path)) {
path = _schemaRefs[path];
}
return _refMap[path];
}
FormatException _boolError(String key, dynamic instance) => _error("$key must be boolean: $instance");
FormatException _numError(String key, dynamic instance) => _error("$key must be num: $instance");
FormatException _intError(String key, dynamic instance) => _error("$key must be int: $instance");
FormatException _stringError(String key, dynamic instance) => _error("$key must be string: $instance");
FormatException _objectError(String key, dynamic instance) => _error("$key must be object: $instance");
FormatException _arrayError(String key, dynamic instance) => _error("$key must be array: $instance");
FormatException _schemaError(String key, dynamic instance) => _error("$key must be valid schema object: $instance");
FormatException _error(String msg) {
msg = "$_path: $msg";
if (logFormatExceptions) _logger.warning(msg);
return new FormatException(msg);
}
String _requireString(String key, dynamic value) {
if (value is String) return value;
throw _stringError(key, value);
}
dynamic _requireNonNegative(String key, dynamic value) {
if (value < 0) throw _error("$key must be non-negative: $value");
return value;
}
int _requireNonNegativeInt(String key, dynamic value) {
if (value is int) return _requireNonNegative(key, value);
throw _intError(key, value);
}
_addSchema(String path, Schema schema) => _refMap[path] = schema;
_getMultipleOf(dynamic value) {
if (value is! num) throw _numError("multiple", value);
if (value <= 0) throw _error("multipleOf must be > 0: $value");
_multipleOf = value;
}
_getMaximum(dynamic value) {
if (value is! num) throw _numError("maximum", value);
_maximum = value;
}
_getExclusiveMaximum(dynamic value) {
if (value is! bool) throw _boolError("exclusiveMaximum", value);
_exclusiveMaximum = value;
}
_getMinimum(dynamic value) {
if (value is! num) throw _numError("minimum", value);
_minimum = value;
}
_getExclusiveMinimum(dynamic value) {
if (value is! bool) throw _boolError("exclusiveMinimum", value);
_exclusiveMinimum = value;
}
_getMaxLength(dynamic value) => _maxLength = _requireNonNegativeInt('maxLength', value);
_getMinLength(dynamic value) => _minLength = _requireNonNegativeInt('minLength', value);
_getPattern(dynamic value) {
if (value is! String) throw _stringError("pattern", value);
_pattern = new RegExp(value);
}
_makeSchema(String path, dynamic schema, assigner(Schema rhs)) {
if (schema is! Map) throw _schemaError(path, schema);
if (_registerSchemaRef(path, schema)) {
_schemaAssignments.add(() => assigner(_resolvePath(path)));
} else {
assigner(_createSubSchema(schema, path));
}
}
_getProperties(dynamic value) {
if (value is Map) {
value.forEach((property, subSchema) =>
_makeSchema("$_path/properties/$property", subSchema, (rhs) => _properties[property] = rhs));
} else {
throw _objectError("properties", value);
}
}
_getItems(dynamic value) {
if (value is Map) {
_makeSchema("$_path/items", value, (rhs) => _items = rhs);
} else if (value is List) {
int index = 0;
_itemsList = new List(value.length);
for (int i = 0; i < value.length; i++) {
_makeSchema("$_path/items/${index++}", value[i], (rhs) => _itemsList[i] = rhs);
}
} else {
throw _error("items must be object or array: $value");
}
}
_getAdditionalItems(dynamic value) {
if (value is bool) {
_additionalItems = value;
} else if (value is Map) {
_makeSchema("$_path/additionalItems", value, (rhs) => _additionalItems = rhs);
} else {
throw _error("additionalItems must be boolean or object: $value");
}
}
_getMaxItems(dynamic value) => _maxItems = _requireNonNegativeInt('maxItems', value);
_getMinItems(dynamic value) => _minItems = _requireNonNegativeInt('minItems', value);
_getUniqueItems(dynamic value) {
if (value is! bool) throw _boolError("uniqueItems", value);
_uniqueItems = value;
}
_getRequired(dynamic value) {
if (value is! List) throw _arrayError("required", value);
if (value.length == 0) throw _error("required must be a non-empty array: $value");
_requiredProperties = new List.from(value);
}
_getMaxProperties(dynamic value) => _maxProperties = _requireNonNegativeInt('maxProperties', value);
_getMinProperties(dynamic value) => _minProperties = _requireNonNegativeInt('minProperties', value);
_getAdditionalProperties(dynamic value) {
if (value is bool) {
_additionalProperties = value;
} else if (value is Map) {
_makeSchema("$_path/additionalProperties", value, (rhs) => _additionalPropertiesSchema = rhs);
} else {
throw _error("additionalProperties must be a bool or valid schema object: $value");
}
}
_getPatternProperties(dynamic value) {
if (value is! Map) throw _objectError("patternProperties", value);
value.forEach(
(k, v) => _makeSchema("$_path/patternProperties/$k", v, (rhs) => _patternProperties[new RegExp(k)] = rhs));
}
_getDependencies(dynamic value) {
if (value is Map) {
value.forEach((k, v) {
if (v is Map) {
_makeSchema("$_path/dependencies/$k", v, (rhs) => _schemaDependencies[k] = rhs);
} else if (v is List) {
if (v.length == 0) throw _error("property dependencies must be non-empty array");
Set uniqueDeps = new Set();
v.forEach((propDep) {
if (propDep is! String) throw _stringError("propertyDependency", v);
if (uniqueDeps.contains(propDep)) throw _error("property dependencies must be unique: $v");
_propertyDependencies.putIfAbsent(k, () => []).add(propDep);
uniqueDeps.add(propDep);
});
} else {
throw _error("dependency values must be object or array: $v");
}
});
} else {
throw _objectError("dependencies", value);
}
}
_getEnum(dynamic value) {
if (value is List) {
if (value.length == 0) throw _error("enum must be a non-empty array: $value");
int i = 0;
value.forEach((v) {
for (int j = i + 1; j < value.length; j++) {
if (_jsonEqual(value[i], value[j])) throw _error("enum values must be unique: $value [$i]==[$j]");
}
i++;
_enumValues.add(v);
});
} else {
throw _arrayError("enum", value);
}
}
_getType(dynamic value) {
if (value is String) {
_schemaTypeList = [SchemaType.fromString(value)];
} else if (value is List) {
_schemaTypeList = value.map((v) => SchemaType.fromString(v)).toList();
} else {
throw _error("type must be string or array: ${value.runtimeType}");
}
if (_schemaTypeList.contains(null)) throw _error("type(s) invalid $value");
}
_requireListOfSchema(String key, dynamic value, schemaAdder(Schema schema)) {
if (value is List) {
if (value.length == 0) throw _error("$key array must not be empty");
for (int i = 0; i < value.length; i++) {
_makeSchema("$_path/$key/$i", value[i], (rhs) => schemaAdder(rhs));
}
} else {
throw _arrayError(key, value);
}
}
_getAllOf(dynamic value) => _requireListOfSchema("allOf", value, (schema) => _allOf.add(schema));
_getAnyOf(dynamic value) => _requireListOfSchema("anyOf", value, (schema) => _anyOf.add(schema));
_getOneOf(dynamic value) => _requireListOfSchema("oneOf", value, (schema) => _oneOf.add(schema));
_getNot(dynamic value) {
if (value is Map) {
_makeSchema("$_path/not", value, (rhs) => _notSchema = rhs);
} else {
throw _objectError("not", value);
}
}
_getDefinitions(dynamic value) {
if (value is Map) {
value.forEach((k, v) => _makeSchema("$_path/definitions/$k", v, (rhs) => _definitions[k] = rhs));
} else {
throw _objectError("definition", value);
}
}
_getRef(dynamic value) {
if (value is String) {
_ref = value;
if (_ref.length == 0) throw _error("\$ref must be non-empty string");
if (_ref[0] != '#') {
var refSchemaFuture = createSchemaFromUrl(_ref).then((schema) => _addSchema(_ref, schema));
_retrievalRequests.add(refSchemaFuture);
}
} else {
throw _stringError(r"$ref", value);
}
}
_getSchema(dynamic value) {
if (value is String) {
if (value != "http://json-schema.org/draft-04/schema#") {
throw _error("Only draft 4 schema supported");
}
} else {
throw _stringError(r"$ref", value);
}
}
_getId(dynamic value) {
if (value is String) {
String id = _requireString("id", value);
try {
_id = Uri.parse(id);
} catch (e) {
throw _error("id must be a valid URI: $value ($e)");
}
} else {
throw _stringError("id", value);
}
}
_getTitle(dynamic value) => _title = _requireString("title", value);
_getDescription(dynamic value) => _description = _requireString("description", value);
_getDefault(dynamic value) => _defaultValue = value;
_getFormat(dynamic value) {
_format = _requireString("format", value);
}
static Map _accessMap = {
"multipleOf": (s, v) => s._getMultipleOf(v),
"maximum": (s, v) => s._getMaximum(v),
"exclusiveMaximum": (s, v) => s._getExclusiveMaximum(v),
"minimum": (s, v) => s._getMinimum(v),
"exclusiveMinimum": (s, v) => s._getExclusiveMinimum(v),
"maxLength": (s, v) => s._getMaxLength(v),
"minLength": (s, v) => s._getMinLength(v),
"pattern": (s, v) => s._getPattern(v),
"properties": (s, v) => s._getProperties(v),
"maxProperties": (s, v) => s._getMaxProperties(v),
"minProperties": (s, v) => s._getMinProperties(v),
"additionalProperties": (s, v) => s._getAdditionalProperties(v),
"dependencies": (s, v) => s._getDependencies(v),
"patternProperties": (s, v) => s._getPatternProperties(v),
"items": (s, v) => s._getItems(v),
"additionalItems": (s, v) => s._getAdditionalItems(v),
"maxItems": (s, v) => s._getMaxItems(v),
"minItems": (s, v) => s._getMinItems(v),
"uniqueItems": (s, v) => s._getUniqueItems(v),
"required": (s, v) => s._getRequired(v),
"default": (s, v) => s._getDefault(v),
"enum": (s, v) => s._getEnum(v),
"type": (s, v) => s._getType(v),
"allOf": (s, v) => s._getAllOf(v),
"anyOf": (s, v) => s._getAnyOf(v),
"oneOf": (s, v) => s._getOneOf(v),
"not": (s, v) => s._getNot(v),
"definitions": (s, v) => s._getDefinitions(v),
"id": (s, v) => s._getId(v),
"\$ref": (s, v) => s._getRef(v),
"\$schema": (s, v) => s._getSchema(v),
"title": (s, v) => s._getTitle(v),
"description": (s, v) => s._getDescription(v),
"format": (s, v) => s._getFormat(v),
};
void _validateSchema() {
_logger.info("Validating schema $_path");
if (_registerSchemaRef(_path, _schemaMap)) {
_logger.info("Top level schema is ref: $_schemaRefs");
}
_schemaMap.forEach((k, v) {
var accessor = _accessMap[k];
if (accessor != null) {
accessor(this, v);
} else {
_freeFormMap[PATH.join(_path, _normalizePath(k))] = v;
}
});
if (_exclusiveMinimum != null && _minimum == null) throw _error("exclusiveMinimum requires minimum");
if (_exclusiveMaximum != null && _maximum == null) throw _error("exclusiveMaximum requires maximum");
if (_root == this) {
_schemaAssignments.forEach((assignment) => assignment());
if (_retrievalRequests.isNotEmpty) {
Future.wait(_retrievalRequests).then((_) => _thisCompleter.complete(_resolvePath('#')));
} else {
_thisCompleter.complete(_resolvePath('#'));
}
_logger.info("Marked $_path complete");
}
_logger.info("Completed Validating schema $_path");
}
void _initialize() {
if (_root == null) {
_root = this;
_path = '#';
_addSchema('#', this);
_thisCompleter = new Completer();
} else {
_schemaRefs = _root._schemaRefs;
_refMap = _root._refMap;
_freeFormMap = _root._freeFormMap;
_thisCompleter = _root._thisCompleter;
_schemaAssignments = _root._schemaAssignments;
}
_validateSchema();
}
String _endPath(String path) {
if (_pathsEncountered.contains(path)) throw _error("Encountered path cycle ${_pathsEncountered}, adding $path");
var referredTo = _schemaRefs[path];
if (referredTo == null) {
return path;
} else {
_pathsEncountered.add(path);
return _endPath(referredTo);
}
}
Schema _resolvePath(String original) {
String path = endPath(original);
Schema result = _refMap[path];
if (result == null) {
var schema = _freeFormMap[path];
if (schema is! Map) throw _schemaError("free-form property $original at $path", schema);
return new Schema._fromMap(_root, schema, path);
}
return result;
}
bool _registerSchemaRef(String path, dynamic schemaDefinition) {
if (schemaDefinition is Map) {
dynamic ref = schemaDefinition[r"$ref"];
if (ref != null) {
if (ref is String) {
_logger.info("Linking $path to $ref");
_schemaRefs[path] = ref;
return true;
} else {
throw _stringError("\$ref", ref);
}
}
}
return false;
}
static String _normalizePath(String path) => path.replaceAll('~', '~0').replaceAll('/', '~1').replaceAll('%', '%25');
Schema _createSubSchema(dynamic schemaDefinition, String path) {
assert(!_schemaRefs.containsKey(path));
assert(!_refMap.containsKey(path));
return new Schema._fromMap(_root, schemaDefinition, path);
}
String toString() => "${_schemaMap}";
// end <class Schema>
Schema _root;
Map _schemaMap = {};
String _path;
num _multipleOf;
num _maximum;
bool _exclusiveMaximum;
num _minimum;
bool _exclusiveMinimum;
int _maxLength;
int _minLength;
RegExp _pattern;
List _enumValues = [];
List<Schema> _allOf = [];
List<Schema> _anyOf = [];
List<Schema> _oneOf = [];
Schema _notSchema;
Map<String, Schema> _definitions = {};
Uri _id;
String _ref;
String _description;
String _title;
List<SchemaType> _schemaTypeList;
Schema _items;
List<Schema> _itemsList;
dynamic _additionalItems;
int _maxItems;
int _minItems;
bool _uniqueItems = false;
List<String> _requiredProperties;
int _maxProperties;
int _minProperties = 0;
Map<String, Schema> _properties = {};
bool _additionalProperties;
Schema _additionalPropertiesSchema;
Map<RegExp, Schema> _patternProperties = {};
Map<String, Schema> _schemaDependencies = {};
Map<String, List<String>> _propertyDependencies = {};
dynamic _defaultValue;
Map<String, Schema> _refMap = {};
/// For schemas with $ref maps path of schema to $ref path
Map<String, String> _schemaRefs = {};
/// Assignments to call for resolution upon end of parse
List _schemaAssignments = [];
/// Maps any non-key top level property to its original value
Map<String, dynamic> _freeFormMap = {};
Completer<Schema> _thisCompleter = new Completer<Schema>();
List<Future<Schema>> _retrievalRequests = [];
/// Set of strings to gaurd against path cycles
Set<String> _pathsEncountered = new Set();
/// Support for optional formats (date-time, uri, email, ipv6, hostname)
String _format;
}
// custom <part schema>
// end <part schema>