resolve.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. 'use strict';
  2. var url = require('url')
  3. , equal = require('fast-deep-equal')
  4. , util = require('./util')
  5. , SchemaObject = require('./schema_obj')
  6. , traverse = require('json-schema-traverse');
  7. module.exports = resolve;
  8. resolve.normalizeId = normalizeId;
  9. resolve.fullPath = getFullPath;
  10. resolve.url = resolveUrl;
  11. resolve.ids = resolveIds;
  12. resolve.inlineRef = inlineRef;
  13. resolve.schema = resolveSchema;
  14. /**
  15. * [resolve and compile the references ($ref)]
  16. * @this Ajv
  17. * @param {Function} compile reference to schema compilation funciton (localCompile)
  18. * @param {Object} root object with information about the root schema for the current schema
  19. * @param {String} ref reference to resolve
  20. * @return {Object|Function} schema object (if the schema can be inlined) or validation function
  21. */
  22. function resolve(compile, root, ref) {
  23. /* jshint validthis: true */
  24. var refVal = this._refs[ref];
  25. if (typeof refVal == 'string') {
  26. if (this._refs[refVal]) refVal = this._refs[refVal];
  27. else return resolve.call(this, compile, root, refVal);
  28. }
  29. refVal = refVal || this._schemas[ref];
  30. if (refVal instanceof SchemaObject) {
  31. return inlineRef(refVal.schema, this._opts.inlineRefs)
  32. ? refVal.schema
  33. : refVal.validate || this._compile(refVal);
  34. }
  35. var res = resolveSchema.call(this, root, ref);
  36. var schema, v, baseId;
  37. if (res) {
  38. schema = res.schema;
  39. root = res.root;
  40. baseId = res.baseId;
  41. }
  42. if (schema instanceof SchemaObject) {
  43. v = schema.validate || compile.call(this, schema.schema, root, undefined, baseId);
  44. } else if (schema !== undefined) {
  45. v = inlineRef(schema, this._opts.inlineRefs)
  46. ? schema
  47. : compile.call(this, schema, root, undefined, baseId);
  48. }
  49. return v;
  50. }
  51. /**
  52. * Resolve schema, its root and baseId
  53. * @this Ajv
  54. * @param {Object} root root object with properties schema, refVal, refs
  55. * @param {String} ref reference to resolve
  56. * @return {Object} object with properties schema, root, baseId
  57. */
  58. function resolveSchema(root, ref) {
  59. /* jshint validthis: true */
  60. var p = url.parse(ref, false, true)
  61. , refPath = _getFullPath(p)
  62. , baseId = getFullPath(this._getId(root.schema));
  63. if (refPath !== baseId) {
  64. var id = normalizeId(refPath);
  65. var refVal = this._refs[id];
  66. if (typeof refVal == 'string') {
  67. return resolveRecursive.call(this, root, refVal, p);
  68. } else if (refVal instanceof SchemaObject) {
  69. if (!refVal.validate) this._compile(refVal);
  70. root = refVal;
  71. } else {
  72. refVal = this._schemas[id];
  73. if (refVal instanceof SchemaObject) {
  74. if (!refVal.validate) this._compile(refVal);
  75. if (id == normalizeId(ref))
  76. return { schema: refVal, root: root, baseId: baseId };
  77. root = refVal;
  78. } else {
  79. return;
  80. }
  81. }
  82. if (!root.schema) return;
  83. baseId = getFullPath(this._getId(root.schema));
  84. }
  85. return getJsonPointer.call(this, p, baseId, root.schema, root);
  86. }
  87. /* @this Ajv */
  88. function resolveRecursive(root, ref, parsedRef) {
  89. /* jshint validthis: true */
  90. var res = resolveSchema.call(this, root, ref);
  91. if (res) {
  92. var schema = res.schema;
  93. var baseId = res.baseId;
  94. root = res.root;
  95. var id = this._getId(schema);
  96. if (id) baseId = resolveUrl(baseId, id);
  97. return getJsonPointer.call(this, parsedRef, baseId, schema, root);
  98. }
  99. }
  100. var PREVENT_SCOPE_CHANGE = util.toHash(['properties', 'patternProperties', 'enum', 'dependencies', 'definitions']);
  101. /* @this Ajv */
  102. function getJsonPointer(parsedRef, baseId, schema, root) {
  103. /* jshint validthis: true */
  104. parsedRef.hash = parsedRef.hash || '';
  105. if (parsedRef.hash.slice(0,2) != '#/') return;
  106. var parts = parsedRef.hash.split('/');
  107. for (var i = 1; i < parts.length; i++) {
  108. var part = parts[i];
  109. if (part) {
  110. part = util.unescapeFragment(part);
  111. schema = schema[part];
  112. if (schema === undefined) break;
  113. var id;
  114. if (!PREVENT_SCOPE_CHANGE[part]) {
  115. id = this._getId(schema);
  116. if (id) baseId = resolveUrl(baseId, id);
  117. if (schema.$ref) {
  118. var $ref = resolveUrl(baseId, schema.$ref);
  119. var res = resolveSchema.call(this, root, $ref);
  120. if (res) {
  121. schema = res.schema;
  122. root = res.root;
  123. baseId = res.baseId;
  124. }
  125. }
  126. }
  127. }
  128. }
  129. if (schema !== undefined && schema !== root.schema)
  130. return { schema: schema, root: root, baseId: baseId };
  131. }
  132. var SIMPLE_INLINED = util.toHash([
  133. 'type', 'format', 'pattern',
  134. 'maxLength', 'minLength',
  135. 'maxProperties', 'minProperties',
  136. 'maxItems', 'minItems',
  137. 'maximum', 'minimum',
  138. 'uniqueItems', 'multipleOf',
  139. 'required', 'enum'
  140. ]);
  141. function inlineRef(schema, limit) {
  142. if (limit === false) return false;
  143. if (limit === undefined || limit === true) return checkNoRef(schema);
  144. else if (limit) return countKeys(schema) <= limit;
  145. }
  146. function checkNoRef(schema) {
  147. var item;
  148. if (Array.isArray(schema)) {
  149. for (var i=0; i<schema.length; i++) {
  150. item = schema[i];
  151. if (typeof item == 'object' && !checkNoRef(item)) return false;
  152. }
  153. } else {
  154. for (var key in schema) {
  155. if (key == '$ref') return false;
  156. item = schema[key];
  157. if (typeof item == 'object' && !checkNoRef(item)) return false;
  158. }
  159. }
  160. return true;
  161. }
  162. function countKeys(schema) {
  163. var count = 0, item;
  164. if (Array.isArray(schema)) {
  165. for (var i=0; i<schema.length; i++) {
  166. item = schema[i];
  167. if (typeof item == 'object') count += countKeys(item);
  168. if (count == Infinity) return Infinity;
  169. }
  170. } else {
  171. for (var key in schema) {
  172. if (key == '$ref') return Infinity;
  173. if (SIMPLE_INLINED[key]) {
  174. count++;
  175. } else {
  176. item = schema[key];
  177. if (typeof item == 'object') count += countKeys(item) + 1;
  178. if (count == Infinity) return Infinity;
  179. }
  180. }
  181. }
  182. return count;
  183. }
  184. function getFullPath(id, normalize) {
  185. if (normalize !== false) id = normalizeId(id);
  186. var p = url.parse(id, false, true);
  187. return _getFullPath(p);
  188. }
  189. function _getFullPath(p) {
  190. var protocolSeparator = p.protocol || p.href.slice(0,2) == '//' ? '//' : '';
  191. return (p.protocol||'') + protocolSeparator + (p.host||'') + (p.path||'') + '#';
  192. }
  193. var TRAILING_SLASH_HASH = /#\/?$/;
  194. function normalizeId(id) {
  195. return id ? id.replace(TRAILING_SLASH_HASH, '') : '';
  196. }
  197. function resolveUrl(baseId, id) {
  198. id = normalizeId(id);
  199. return url.resolve(baseId, id);
  200. }
  201. /* @this Ajv */
  202. function resolveIds(schema) {
  203. var schemaId = normalizeId(this._getId(schema));
  204. var baseIds = {'': schemaId};
  205. var fullPaths = {'': getFullPath(schemaId, false)};
  206. var localRefs = {};
  207. var self = this;
  208. traverse(schema, {allKeys: true}, function(sch, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) {
  209. if (jsonPtr === '') return;
  210. var id = self._getId(sch);
  211. var baseId = baseIds[parentJsonPtr];
  212. var fullPath = fullPaths[parentJsonPtr] + '/' + parentKeyword;
  213. if (keyIndex !== undefined)
  214. fullPath += '/' + (typeof keyIndex == 'number' ? keyIndex : util.escapeFragment(keyIndex));
  215. if (typeof id == 'string') {
  216. id = baseId = normalizeId(baseId ? url.resolve(baseId, id) : id);
  217. var refVal = self._refs[id];
  218. if (typeof refVal == 'string') refVal = self._refs[refVal];
  219. if (refVal && refVal.schema) {
  220. if (!equal(sch, refVal.schema))
  221. throw new Error('id "' + id + '" resolves to more than one schema');
  222. } else if (id != normalizeId(fullPath)) {
  223. if (id[0] == '#') {
  224. if (localRefs[id] && !equal(sch, localRefs[id]))
  225. throw new Error('id "' + id + '" resolves to more than one schema');
  226. localRefs[id] = sch;
  227. } else {
  228. self._refs[id] = fullPath;
  229. }
  230. }
  231. }
  232. baseIds[jsonPtr] = baseId;
  233. fullPaths[jsonPtr] = fullPath;
  234. });
  235. return localRefs;
  236. }