ref.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. "use strict";
  2. module.exports = $Ref;
  3. const Pointer = require("./pointer");
  4. const { InvalidPointerError, isHandledError, normalizeError } = require("./util/errors");
  5. const { safePointerToPath, stripHash, getHash } = require("./util/url");
  6. /**
  7. * This class represents a single JSON reference and its resolved value.
  8. *
  9. * @class
  10. */
  11. function $Ref () {
  12. /**
  13. * The file path or URL of the referenced file.
  14. * This path is relative to the path of the main JSON schema file.
  15. *
  16. * This path does NOT contain document fragments (JSON pointers). It always references an ENTIRE file.
  17. * Use methods such as {@link $Ref#get}, {@link $Ref#resolve}, and {@link $Ref#exists} to get
  18. * specific JSON pointers within the file.
  19. *
  20. * @type {string}
  21. */
  22. this.path = undefined;
  23. /**
  24. * The resolved value of the JSON reference.
  25. * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
  26. *
  27. * @type {?*}
  28. */
  29. this.value = undefined;
  30. /**
  31. * The {@link $Refs} object that contains this {@link $Ref} object.
  32. *
  33. * @type {$Refs}
  34. */
  35. this.$refs = undefined;
  36. /**
  37. * Indicates the type of {@link $Ref#path} (e.g. "file", "http", etc.)
  38. *
  39. * @type {?string}
  40. */
  41. this.pathType = undefined;
  42. /**
  43. * List of all errors. Undefined if no errors.
  44. *
  45. * @type {Array<JSONParserError | ResolverError | ParserError | MissingPointerError>}
  46. */
  47. this.errors = undefined;
  48. }
  49. /**
  50. * Pushes an error to errors array.
  51. *
  52. * @param {Array<JSONParserError | JSONParserErrorGroup>} err - The error to be pushed
  53. * @returns {void}
  54. */
  55. $Ref.prototype.addError = function (err) {
  56. if (this.errors === undefined) {
  57. this.errors = [];
  58. }
  59. const existingErrors = this.errors.map(({ footprint }) => footprint);
  60. // the path has been almost certainly set at this point,
  61. // but just in case something went wrong, normalizeError injects path if necessary
  62. // moreover, certain errors might point at the same spot, so filter them out to reduce noise
  63. if (Array.isArray(err.errors)) {
  64. this.errors.push(...err.errors
  65. .map(normalizeError)
  66. .filter(({ footprint }) => !existingErrors.includes(footprint)),
  67. );
  68. }
  69. else if (!existingErrors.includes(err.footprint)) {
  70. this.errors.push(normalizeError(err));
  71. }
  72. };
  73. /**
  74. * Determines whether the given JSON reference exists within this {@link $Ref#value}.
  75. *
  76. * @param {string} path - The full path being resolved, optionally with a JSON pointer in the hash
  77. * @param {$RefParserOptions} options
  78. * @returns {boolean}
  79. */
  80. $Ref.prototype.exists = function (path, options) {
  81. try {
  82. this.resolve(path, options);
  83. return true;
  84. }
  85. catch (e) {
  86. return false;
  87. }
  88. };
  89. /**
  90. * Resolves the given JSON reference within this {@link $Ref#value} and returns the resolved value.
  91. *
  92. * @param {string} path - The full path being resolved, optionally with a JSON pointer in the hash
  93. * @param {$RefParserOptions} options
  94. * @returns {*} - Returns the resolved value
  95. */
  96. $Ref.prototype.get = function (path, options) {
  97. return this.resolve(path, options).value;
  98. };
  99. /**
  100. * Resolves the given JSON reference within this {@link $Ref#value}.
  101. *
  102. * @param {string} path - The full path being resolved, optionally with a JSON pointer in the hash
  103. * @param {$RefParserOptions} options
  104. * @param {string} friendlyPath - The original user-specified path (used for error messages)
  105. * @param {string} pathFromRoot - The path of `obj` from the schema root
  106. * @returns {Pointer | null}
  107. */
  108. $Ref.prototype.resolve = function (path, options, friendlyPath, pathFromRoot) {
  109. let pointer = new Pointer(this, path, friendlyPath);
  110. try {
  111. return pointer.resolve(this.value, options, pathFromRoot);
  112. }
  113. catch (err) {
  114. if (!options || !options.continueOnError || !isHandledError(err)) {
  115. throw err;
  116. }
  117. if (err.path === null) {
  118. err.path = safePointerToPath(getHash(pathFromRoot));
  119. }
  120. if (err instanceof InvalidPointerError) {
  121. // this is a special case - InvalidPointerError is thrown when dereferencing external file,
  122. // but the issue is caused by the source file that referenced the file that undergoes dereferencing
  123. err.source = decodeURI(stripHash(pathFromRoot));
  124. }
  125. this.addError(err);
  126. return null;
  127. }
  128. };
  129. /**
  130. * Sets the value of a nested property within this {@link $Ref#value}.
  131. * If the property, or any of its parents don't exist, they will be created.
  132. *
  133. * @param {string} path - The full path of the property to set, optionally with a JSON pointer in the hash
  134. * @param {*} value - The value to assign
  135. */
  136. $Ref.prototype.set = function (path, value) {
  137. let pointer = new Pointer(this, path);
  138. this.value = pointer.set(this.value, value);
  139. };
  140. /**
  141. * Determines whether the given value is a JSON reference.
  142. *
  143. * @param {*} value - The value to inspect
  144. * @returns {boolean}
  145. */
  146. $Ref.is$Ref = function (value) {
  147. return value && typeof value === "object" && typeof value.$ref === "string" && value.$ref.length > 0;
  148. };
  149. /**
  150. * Determines whether the given value is an external JSON reference.
  151. *
  152. * @param {*} value - The value to inspect
  153. * @returns {boolean}
  154. */
  155. $Ref.isExternal$Ref = function (value) {
  156. return $Ref.is$Ref(value) && value.$ref[0] !== "#";
  157. };
  158. /**
  159. * Determines whether the given value is a JSON reference, and whether it is allowed by the options.
  160. * For example, if it references an external file, then options.resolve.external must be true.
  161. *
  162. * @param {*} value - The value to inspect
  163. * @param {$RefParserOptions} options
  164. * @returns {boolean}
  165. */
  166. $Ref.isAllowed$Ref = function (value, options) {
  167. if ($Ref.is$Ref(value)) {
  168. if (value.$ref.substr(0, 2) === "#/" || value.$ref === "#") {
  169. // It's a JSON Pointer reference, which is always allowed
  170. return true;
  171. }
  172. else if (value.$ref[0] !== "#" && (!options || options.resolve.external)) {
  173. // It's an external reference, which is allowed by the options
  174. return true;
  175. }
  176. }
  177. };
  178. /**
  179. * Determines whether the given value is a JSON reference that "extends" its resolved value.
  180. * That is, it has extra properties (in addition to "$ref"), so rather than simply pointing to
  181. * an existing value, this $ref actually creates a NEW value that is a shallow copy of the resolved
  182. * value, plus the extra properties.
  183. *
  184. * @example:
  185. * {
  186. * person: {
  187. * properties: {
  188. * firstName: { type: string }
  189. * lastName: { type: string }
  190. * }
  191. * }
  192. * employee: {
  193. * properties: {
  194. * $ref: #/person/properties
  195. * salary: { type: number }
  196. * }
  197. * }
  198. * }
  199. *
  200. * In this example, "employee" is an extended $ref, since it extends "person" with an additional
  201. * property (salary). The result is a NEW value that looks like this:
  202. *
  203. * {
  204. * properties: {
  205. * firstName: { type: string }
  206. * lastName: { type: string }
  207. * salary: { type: number }
  208. * }
  209. * }
  210. *
  211. * @param {*} value - The value to inspect
  212. * @returns {boolean}
  213. */
  214. $Ref.isExtended$Ref = function (value) {
  215. return $Ref.is$Ref(value) && Object.keys(value).length > 1;
  216. };
  217. /**
  218. * Returns the resolved value of a JSON Reference.
  219. * If necessary, the resolved value is merged with the JSON Reference to create a new object
  220. *
  221. * @example:
  222. * {
  223. * person: {
  224. * properties: {
  225. * firstName: { type: string }
  226. * lastName: { type: string }
  227. * }
  228. * }
  229. * employee: {
  230. * properties: {
  231. * $ref: #/person/properties
  232. * salary: { type: number }
  233. * }
  234. * }
  235. * }
  236. *
  237. * When "person" and "employee" are merged, you end up with the following object:
  238. *
  239. * {
  240. * properties: {
  241. * firstName: { type: string }
  242. * lastName: { type: string }
  243. * salary: { type: number }
  244. * }
  245. * }
  246. *
  247. * @param {object} $ref - The JSON reference object (the one with the "$ref" property)
  248. * @param {*} resolvedValue - The resolved value, which can be any type
  249. * @returns {*} - Returns the dereferenced value
  250. */
  251. $Ref.dereference = function ($ref, resolvedValue) {
  252. if (resolvedValue && typeof resolvedValue === "object" && $Ref.isExtended$Ref($ref)) {
  253. let merged = {};
  254. for (let key of Object.keys($ref)) {
  255. if (key !== "$ref") {
  256. merged[key] = $ref[key];
  257. }
  258. }
  259. for (let key of Object.keys(resolvedValue)) {
  260. if (!(key in merged)) {
  261. merged[key] = resolvedValue[key];
  262. }
  263. }
  264. return merged;
  265. }
  266. else {
  267. // Completely replace the original reference with the resolved value
  268. return resolvedValue;
  269. }
  270. };