resolve.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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) {
  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(root.schema.id);
  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(root.schema.id);
  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. if (schema.id) baseId = resolveUrl(baseId, schema.id);
  95. return getJsonPointer.call(this, parsedRef, baseId, schema, root);
  96. }
  97. }
  98. var PREVENT_SCOPE_CHANGE = util.toHash(['properties', 'patternProperties', 'enum', 'dependencies', 'definitions']);
  99. /* @this Ajv */
  100. function getJsonPointer(parsedRef, baseId, schema, root) {
  101. /* jshint validthis: true */
  102. parsedRef.hash = parsedRef.hash || '';
  103. if (parsedRef.hash.slice(0,2) != '#/') return;
  104. var parts = parsedRef.hash.split('/');
  105. for (var i = 1; i < parts.length; i++) {
  106. var part = parts[i];
  107. if (part) {
  108. part = util.unescapeFragment(part);
  109. schema = schema[part];
  110. if (!schema) break;
  111. if (schema.id && !PREVENT_SCOPE_CHANGE[part]) baseId = resolveUrl(baseId, schema.id);
  112. if (schema.$ref) {
  113. var $ref = resolveUrl(baseId, schema.$ref);
  114. var res = resolveSchema.call(this, root, $ref);
  115. if (res) {
  116. schema = res.schema;
  117. root = res.root;
  118. baseId = res.baseId;
  119. }
  120. }
  121. }
  122. }
  123. if (schema && schema != root.schema)
  124. return { schema: schema, root: root, baseId: baseId };
  125. }
  126. var SIMPLE_INLINED = util.toHash([
  127. 'type', 'format', 'pattern',
  128. 'maxLength', 'minLength',
  129. 'maxProperties', 'minProperties',
  130. 'maxItems', 'minItems',
  131. 'maximum', 'minimum',
  132. 'uniqueItems', 'multipleOf',
  133. 'required', 'enum'
  134. ]);
  135. function inlineRef(schema, limit) {
  136. if (limit === false) return false;
  137. if (limit === undefined || limit === true) return checkNoRef(schema);
  138. else if (limit) return countKeys(schema) <= limit;
  139. }
  140. function checkNoRef(schema) {
  141. var item;
  142. if (Array.isArray(schema)) {
  143. for (var i=0; i<schema.length; i++) {
  144. item = schema[i];
  145. if (typeof item == 'object' && !checkNoRef(item)) return false;
  146. }
  147. } else {
  148. for (var key in schema) {
  149. if (key == '$ref') return false;
  150. item = schema[key];
  151. if (typeof item == 'object' && !checkNoRef(item)) return false;
  152. }
  153. }
  154. return true;
  155. }
  156. function countKeys(schema) {
  157. var count = 0, item;
  158. if (Array.isArray(schema)) {
  159. for (var i=0; i<schema.length; i++) {
  160. item = schema[i];
  161. if (typeof item == 'object') count += countKeys(item);
  162. if (count == Infinity) return Infinity;
  163. }
  164. } else {
  165. for (var key in schema) {
  166. if (key == '$ref') return Infinity;
  167. if (SIMPLE_INLINED[key]) {
  168. count++;
  169. } else {
  170. item = schema[key];
  171. if (typeof item == 'object') count += countKeys(item) + 1;
  172. if (count == Infinity) return Infinity;
  173. }
  174. }
  175. }
  176. return count;
  177. }
  178. function getFullPath(id, normalize) {
  179. if (normalize !== false) id = normalizeId(id);
  180. var p = url.parse(id, false, true);
  181. return _getFullPath(p);
  182. }
  183. function _getFullPath(p) {
  184. var protocolSeparator = p.protocol || p.href.slice(0,2) == '//' ? '//' : '';
  185. return (p.protocol||'') + protocolSeparator + (p.host||'') + (p.path||'') + '#';
  186. }
  187. var TRAILING_SLASH_HASH = /#\/?$/;
  188. function normalizeId(id) {
  189. return id ? id.replace(TRAILING_SLASH_HASH, '') : '';
  190. }
  191. function resolveUrl(baseId, id) {
  192. id = normalizeId(id);
  193. return url.resolve(baseId, id);
  194. }
  195. /* @this Ajv */
  196. function resolveIds(schema) {
  197. /* eslint no-shadow: 0 */
  198. /* jshint validthis: true */
  199. var id = normalizeId(schema.id);
  200. var localRefs = {};
  201. _resolveIds.call(this, schema, getFullPath(id, false), id);
  202. return localRefs;
  203. /* @this Ajv */
  204. function _resolveIds(schema, fullPath, baseId) {
  205. /* jshint validthis: true */
  206. if (Array.isArray(schema)) {
  207. for (var i=0; i<schema.length; i++)
  208. _resolveIds.call(this, schema[i], fullPath+'/'+i, baseId);
  209. } else if (schema && typeof schema == 'object') {
  210. if (typeof schema.id == 'string') {
  211. var id = baseId = baseId
  212. ? url.resolve(baseId, schema.id)
  213. : schema.id;
  214. id = normalizeId(id);
  215. var refVal = this._refs[id];
  216. if (typeof refVal == 'string') refVal = this._refs[refVal];
  217. if (refVal && refVal.schema) {
  218. if (!equal(schema, refVal.schema))
  219. throw new Error('id "' + id + '" resolves to more than one schema');
  220. } else if (id != normalizeId(fullPath)) {
  221. if (id[0] == '#') {
  222. if (localRefs[id] && !equal(schema, localRefs[id]))
  223. throw new Error('id "' + id + '" resolves to more than one schema');
  224. localRefs[id] = schema;
  225. } else {
  226. this._refs[id] = fullPath;
  227. }
  228. }
  229. }
  230. for (var key in schema)
  231. _resolveIds.call(this, schema[key], fullPath+'/'+util.escapeFragment(key), baseId);
  232. }
  233. }
  234. }