'use strict'; var url = require('url') , equal = require('fast-deep-equal') , util = require('./util') , SchemaObject = require('./schema_obj') , traverse = require('json-schema-traverse'); module.exports = resolve; resolve.normalizeId = normalizeId; resolve.fullPath = getFullPath; resolve.url = resolveUrl; resolve.ids = resolveIds; resolve.inlineRef = inlineRef; resolve.schema = resolveSchema; /** * [resolve and compile the references ($ref)] * @this Ajv * @param {Function} compile reference to schema compilation funciton (localCompile) * @param {Object} root object with information about the root schema for the current schema * @param {String} ref reference to resolve * @return {Object|Function} schema object (if the schema can be inlined) or validation function */ function resolve(compile, root, ref) { /* jshint validthis: true */ var refVal = this._refs[ref]; if (typeof refVal == 'string') { if (this._refs[refVal]) refVal = this._refs[refVal]; else return resolve.call(this, compile, root, refVal); } refVal = refVal || this._schemas[ref]; if (refVal instanceof SchemaObject) { return inlineRef(refVal.schema, this._opts.inlineRefs) ? refVal.schema : refVal.validate || this._compile(refVal); } var res = resolveSchema.call(this, root, ref); var schema, v, baseId; if (res) { schema = res.schema; root = res.root; baseId = res.baseId; } if (schema instanceof SchemaObject) { v = schema.validate || compile.call(this, schema.schema, root, undefined, baseId); } else if (schema !== undefined) { v = inlineRef(schema, this._opts.inlineRefs) ? schema : compile.call(this, schema, root, undefined, baseId); } return v; } /** * Resolve schema, its root and baseId * @this Ajv * @param {Object} root root object with properties schema, refVal, refs * @param {String} ref reference to resolve * @return {Object} object with properties schema, root, baseId */ function resolveSchema(root, ref) { /* jshint validthis: true */ var p = url.parse(ref, false, true) , refPath = _getFullPath(p) , baseId = getFullPath(this._getId(root.schema)); if (refPath !== baseId) { var id = normalizeId(refPath); var refVal = this._refs[id]; if (typeof refVal == 'string') { return resolveRecursive.call(this, root, refVal, p); } else if (refVal instanceof SchemaObject) { if (!refVal.validate) this._compile(refVal); root = refVal; } else { refVal = this._schemas[id]; if (refVal instanceof SchemaObject) { if (!refVal.validate) this._compile(refVal); if (id == normalizeId(ref)) return { schema: refVal, root: root, baseId: baseId }; root = refVal; } else { return; } } if (!root.schema) return; baseId = getFullPath(this._getId(root.schema)); } return getJsonPointer.call(this, p, baseId, root.schema, root); } /* @this Ajv */ function resolveRecursive(root, ref, parsedRef) { /* jshint validthis: true */ var res = resolveSchema.call(this, root, ref); if (res) { var schema = res.schema; var baseId = res.baseId; root = res.root; var id = this._getId(schema); if (id) baseId = resolveUrl(baseId, id); return getJsonPointer.call(this, parsedRef, baseId, schema, root); } } var PREVENT_SCOPE_CHANGE = util.toHash(['properties', 'patternProperties', 'enum', 'dependencies', 'definitions']); /* @this Ajv */ function getJsonPointer(parsedRef, baseId, schema, root) { /* jshint validthis: true */ parsedRef.hash = parsedRef.hash || ''; if (parsedRef.hash.slice(0,2) != '#/') return; var parts = parsedRef.hash.split('/'); for (var i = 1; i < parts.length; i++) { var part = parts[i]; if (part) { part = util.unescapeFragment(part); schema = schema[part]; if (schema === undefined) break; var id; if (!PREVENT_SCOPE_CHANGE[part]) { id = this._getId(schema); if (id) baseId = resolveUrl(baseId, id); if (schema.$ref) { var $ref = resolveUrl(baseId, schema.$ref); var res = resolveSchema.call(this, root, $ref); if (res) { schema = res.schema; root = res.root; baseId = res.baseId; } } } } } if (schema !== undefined && schema !== root.schema) return { schema: schema, root: root, baseId: baseId }; } var SIMPLE_INLINED = util.toHash([ 'type', 'format', 'pattern', 'maxLength', 'minLength', 'maxProperties', 'minProperties', 'maxItems', 'minItems', 'maximum', 'minimum', 'uniqueItems', 'multipleOf', 'required', 'enum' ]); function inlineRef(schema, limit) { if (limit === false) return false; if (limit === undefined || limit === true) return checkNoRef(schema); else if (limit) return countKeys(schema) <= limit; } function checkNoRef(schema) { var item; if (Array.isArray(schema)) { for (var i=0; i<schema.length; i++) { item = schema[i]; if (typeof item == 'object' && !checkNoRef(item)) return false; } } else { for (var key in schema) { if (key == '$ref') return false; item = schema[key]; if (typeof item == 'object' && !checkNoRef(item)) return false; } } return true; } function countKeys(schema) { var count = 0, item; if (Array.isArray(schema)) { for (var i=0; i<schema.length; i++) { item = schema[i]; if (typeof item == 'object') count += countKeys(item); if (count == Infinity) return Infinity; } } else { for (var key in schema) { if (key == '$ref') return Infinity; if (SIMPLE_INLINED[key]) { count++; } else { item = schema[key]; if (typeof item == 'object') count += countKeys(item) + 1; if (count == Infinity) return Infinity; } } } return count; } function getFullPath(id, normalize) { if (normalize !== false) id = normalizeId(id); var p = url.parse(id, false, true); return _getFullPath(p); } function _getFullPath(p) { var protocolSeparator = p.protocol || p.href.slice(0,2) == '//' ? '//' : ''; return (p.protocol||'') + protocolSeparator + (p.host||'') + (p.path||'') + '#'; } var TRAILING_SLASH_HASH = /#\/?$/; function normalizeId(id) { return id ? id.replace(TRAILING_SLASH_HASH, '') : ''; } function resolveUrl(baseId, id) { id = normalizeId(id); return url.resolve(baseId, id); } /* @this Ajv */ function resolveIds(schema) { var schemaId = normalizeId(this._getId(schema)); var baseIds = {'': schemaId}; var fullPaths = {'': getFullPath(schemaId, false)}; var localRefs = {}; var self = this; traverse(schema, {allKeys: true}, function(sch, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) { if (jsonPtr === '') return; var id = self._getId(sch); var baseId = baseIds[parentJsonPtr]; var fullPath = fullPaths[parentJsonPtr] + '/' + parentKeyword; if (keyIndex !== undefined) fullPath += '/' + (typeof keyIndex == 'number' ? keyIndex : util.escapeFragment(keyIndex)); if (typeof id == 'string') { id = baseId = normalizeId(baseId ? url.resolve(baseId, id) : id); var refVal = self._refs[id]; if (typeof refVal == 'string') refVal = self._refs[refVal]; if (refVal && refVal.schema) { if (!equal(sch, refVal.schema)) throw new Error('id "' + id + '" resolves to more than one schema'); } else if (id != normalizeId(fullPath)) { if (id[0] == '#') { if (localRefs[id] && !equal(sch, localRefs[id])) throw new Error('id "' + id + '" resolves to more than one schema'); localRefs[id] = sch; } else { self._refs[id] = fullPath; } } } baseIds[jsonPtr] = baseId; fullPaths[jsonPtr] = fullPath; }); return localRefs; }