resolve.js 7.7 KB

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