From 948175e41e1dbd1ae9c20b1030ddc580b25e18c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Sat, 22 Feb 2020 00:46:12 +0800 Subject: [PATCH 01/12] Foundation for error handling --- docs/options.md | 1 + lib/index.js | 36 +++++++++++++++++-- lib/options.js | 10 ++++-- lib/parse.js | 1 + lib/ref.js | 27 ++++++++++++++ lib/resolve-external.js | 24 ++++++++++--- lib/util/errors.js | 78 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 lib/util/errors.js diff --git a/docs/options.md b/docs/options.md index ee4290a0..d1960925 100644 --- a/docs/options.md +++ b/docs/options.md @@ -27,6 +27,7 @@ $RefParser.dereference("my-schema.yaml", { withCredentials: true, // Include auth credentials when resolving HTTP references } }, + failFast: true, // Abort upon first exception dereference: { circular: false // Don't allow circular $refs } diff --git a/lib/index.js b/lib/index.js index 762b9678..7a74338a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,11 +8,16 @@ const resolveExternal = require("./resolve-external"); const bundle = require("./bundle"); const dereference = require("./dereference"); const url = require("./util/url"); +const { GenericError, MissingPointerError, ResolverError, ParserError, isHandledError } = require("./util/errors"); const maybe = require("call-me-maybe"); const { ono } = require("ono"); module.exports = $RefParser; module.exports.YAML = require("./util/yaml"); +module.exports.GenericError = GenericError; +module.exports.MissingPointerError = MissingPointerError; +module.exports.ResolverError = ResolverError; +module.exports.ParserError = ParserError; /** * This class parses a JSON schema, builds a map of its JSON references and their resolved values, @@ -38,6 +43,25 @@ function $RefParser () { this.$refs = new $Refs(); } +/** + * List of all errors + * @type {Array} + */ +Object.defineProperty($RefParser.prototype, "errors", { + get () { + const errors = []; + + for (const $ref of Object.values(this.$refs._$refs)) { + if ($ref.errors) { + errors.push(...$ref.errors); + } + } + + return errors; + }, + enumerable: true, +}); + /** * Parses the given JSON schema. * This method does not resolve any JSON references. @@ -119,8 +143,16 @@ $RefParser.prototype.parse = async function (path, schema, options, callback) { return maybe(args.callback, Promise.resolve(me.schema)); } } - catch (e) { - return maybe(args.callback, Promise.reject(e)); + catch (err) { + if (args.options.failFast || !isHandledError(err)) { + return maybe(args.callback, Promise.reject(err)); + } + + if (this.$refs._$refs[url.stripHash(args.path)]) { + this.$refs._$refs[url.stripHash(args.path)].addError(err); + } + + return maybe(args.callback, Promise.resolve(null)); } }; diff --git a/lib/options.js b/lib/options.js index bfe3ad9f..001c622d 100644 --- a/lib/options.js +++ b/lib/options.js @@ -26,7 +26,7 @@ $RefParserOptions.defaults = { * Determines how different types of files will be parsed. * * You can add additional parsers of your own, replace an existing one with - * your own implemenation, or disable any parser by setting it to false. + * your own implementation, or disable any parser by setting it to false. */ parse: { json: jsonParser, @@ -39,7 +39,7 @@ $RefParserOptions.defaults = { * Determines how JSON References will be resolved. * * You can add additional resolvers of your own, replace an existing one with - * your own implemenation, or disable any resolver by setting it to false. + * your own implementation, or disable any resolver by setting it to false. */ resolve: { file: fileResolver, @@ -55,6 +55,12 @@ $RefParserOptions.defaults = { external: true, }, + /** + * Determines how lenient the processing should be. + * If this option is enable, the processing will be performed in a bail mode - will abort upon the first exception. + */ + failFast: true, + /** * Determines the types of JSON references that are allowed. */ diff --git a/lib/parse.js b/lib/parse.js index d505d3d1..addc6a2f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -3,6 +3,7 @@ const { ono } = require("ono"); const url = require("./util/url"); const plugins = require("./util/plugins"); +const { StoplightParserError, ResolverError, ParserError } = require("./util/errors"); module.exports = parse; diff --git a/lib/ref.js b/lib/ref.js index 30c69f45..84a8ab1d 100644 --- a/lib/ref.js +++ b/lib/ref.js @@ -3,6 +3,7 @@ module.exports = $Ref; const Pointer = require("./pointer"); +const { GenericError, GenericErrorGroup, ParserError, MissingPointerError, ResolverError } = require("./util/errors"); /** * This class represents a single JSON reference and its resolved value. @@ -40,8 +41,34 @@ function $Ref () { * @type {?string} */ this.pathType = undefined; + + /** + * List of all errors. Undefined if no errors. + * @type {Array} + */ + this.errors = undefined; } +/** + * Pushes an error to errors array. + * + * @param {Array} error - The error to be pushed + * @returns {void} + */ +$Ref.prototype.addError = function (err) { + if (this.errors === undefined) { + this.errors = []; + } + + if (Array.isArray(err.errors)) { + this.errors.push(...err.errors); + } + else { + this.errors.push(err); + } +}; + + /** * Determines whether the given JSON reference exists within this {@link $Ref#value}. * diff --git a/lib/resolve-external.js b/lib/resolve-external.js index fb2e0ae7..1155cfae 100644 --- a/lib/resolve-external.js +++ b/lib/resolve-external.js @@ -4,6 +4,7 @@ const $Ref = require("./ref"); const Pointer = require("./pointer"); const parse = require("./parse"); const url = require("./util/url"); +const { isHandledError } = require("./util/errors"); module.exports = resolveExternal; @@ -101,11 +102,24 @@ async function resolve$Ref ($ref, path, $refs, options) { } // Parse the $referenced file/url - const result = await parse(resolvedPath, $refs, options); + try { + const result = await parse(resolvedPath, $refs, options); + + // Crawl the parsed value + // console.log('Resolving $ref pointers in %s', withoutHash); + let promises = crawl(result, withoutHash + "#", $refs, options); + + return Promise.all(promises); + } + catch (err) { + if (options.failFast || !isHandledError(err)) { + throw err; + } - // Crawl the parsed value - // console.log('Resolving $ref pointers in %s', withoutHash); - let promises = crawl(result, withoutHash + "#", $refs, options); + if ($refs._$refs[withoutHash]) { + err.source = url.stripHash(path); + } - return Promise.all(promises); + return []; + } } diff --git a/lib/util/errors.js b/lib/util/errors.js new file mode 100644 index 00000000..0d9bf0c4 --- /dev/null +++ b/lib/util/errors.js @@ -0,0 +1,78 @@ +"use strict"; + +const { stripHash } = require("./url"); + +const GenericError = exports.GenericError = class GenericError extends Error { + constructor (message, source) { + super(); + + this.message = message; + this.source = source; + this.path = []; + } +}; + +setErrorName(GenericError); + +const GenericErrorGroup = exports.GenericErrorGroup = class GenericErrorGroup extends Error { + constructor (errors, source) { + super(); + + this.source = source; + this.errors = errors; + } +}; + +exports.StoplightParserError = class StoplightParserError extends GenericErrorGroup { + constructor (errors, source) { + super(errors.filter(error => error.severity === 0).map(error => { + const parsingError = new ParserError(error.message, source); + parsingError.message = error.message; + if (error.path) { + parsingError.path = error.path; + } + + return parsingError; + })); + + this.message = `Error parsing ${source}`; + } +}; + +const ParserError = exports.ParserError = class ParserError extends GenericError { + constructor (message, source) { + super(`Error parsing ${source}: ${message}`, source); + } +}; + +setErrorName(ParserError); + +const ResolverError = exports.ResolverError = class ResolverError extends GenericError { + constructor (ex, source) { + super(ex.message || `Error reading file ${source}`, source); + if ("code" in ex) { + this.code = String(ex.code); + } + } +}; + +setErrorName(ResolverError); + +const MissingPointerError = exports.MissingPointerError = class MissingPointerError extends GenericError { + constructor (token, path) { + super(`Token "${token}" does not exist.`, stripHash(path)); + } +}; + +setErrorName(MissingPointerError); + +function setErrorName (err) { + Object.defineProperty(err.prototype, "name", { + value: err.name, + enumerable: true, + }); +} + +exports.isHandledError = function (err) { + return err instanceof GenericError || err instanceof GenericErrorGroup; +}; From c0cc8bf915572fc67e3587146d8c83742e93db02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Wed, 5 Feb 2020 19:54:26 +0700 Subject: [PATCH 02/12] Allow parsing errors --- lib/index.js | 12 ++- lib/parse.js | 47 ++++++----- lib/parsers/binary.js | 2 +- lib/parsers/json.js | 44 +++++++---- lib/parsers/text.js | 6 +- lib/parsers/yaml.js | 30 +++---- lib/util/errors.js | 49 +++++++++--- lib/util/plugins.js | 7 +- lib/util/yaml.js | 35 ++------- package-lock.json | 56 ++++++++++++- package.json | 3 + test/specs/callbacks.spec.js | 5 +- test/specs/invalid/invalid.spec.js | 121 +++++++++++++++++++++++++++-- test/specs/parsers/parsers.spec.js | 25 +++++- test/specs/yaml.spec.js | 8 +- 15 files changed, 337 insertions(+), 113 deletions(-) diff --git a/lib/index.js b/lib/index.js index 7a74338a..a58f8925 100644 --- a/lib/index.js +++ b/lib/index.js @@ -135,13 +135,17 @@ $RefParser.prototype.parse = async function (path, schema, options, callback) { try { let result = await promise; - if (!result || typeof result !== "object" || Buffer.isBuffer(result)) { - throw ono.syntax(`"${me.$refs._root$Ref.path || result}" is not a valid JSON Schema`); - } - else { + if (result !== null && typeof result === "object" && !Buffer.isBuffer(result)) { me.schema = result; return maybe(args.callback, Promise.resolve(me.schema)); } + else if (!args.options.failFast) { + me.schema = null; // it's already set to null at line 79, but let's set it again for the sake of readability + return maybe(args.callback, Promise.resolve(me.schema)); + } + else { + throw ono.syntax(`"${me.$refs._root$Ref.path || result}" is not a valid JSON Schema`); + } } catch (err) { if (args.options.failFast || !isHandledError(err)) { diff --git a/lib/parse.js b/lib/parse.js index addc6a2f..e4f80a59 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -18,21 +18,21 @@ module.exports = parse; * The promise resolves with the parsed file contents, NOT the raw (Buffer) contents. */ async function parse (path, $refs, options) { - try { - // Remove the URL fragment, if any - path = url.stripHash(path); + // Remove the URL fragment, if any + path = url.stripHash(path); - // Add a new $Ref for this file, even though we don't have the value yet. - // This ensures that we don't simultaneously read & parse the same file multiple times - let $ref = $refs._add(path); + // Add a new $Ref for this file, even though we don't have the value yet. + // This ensures that we don't simultaneously read & parse the same file multiple times + let $ref = $refs._add(path); - // This "file object" will be passed to all resolvers and parsers. - let file = { - url: path, - extension: url.getExtension(path), - }; + // This "file object" will be passed to all resolvers and parsers. + let file = { + url: path, + extension: url.getExtension(path), + }; - // Read the file and then parse the data + // Read the file and then parse the data + try { const resolver = await readFile(file, options, $refs); $ref.pathType = resolver.plugin.name; file.data = resolver.result; @@ -42,8 +42,14 @@ async function parse (path, $refs, options) { return parser.result; } - catch (e) { - return Promise.reject(e); + catch (ex) { + if (!("error" in ex)) { + throw ex; + } + else { + $ref.value = ex.error; + throw ex.error; + } } } @@ -113,7 +119,7 @@ function parseFile (file, options, $refs) { .then(onParsed, onError); function onParsed (parser) { - if (!parser.plugin.allowEmpty && isEmpty(parser.result)) { + if ((!options.failFast || !parser.plugin.allowEmpty) && isEmpty(parser.result)) { reject(ono.syntax(`Error parsing "${file.url}" as ${parser.plugin.name}. \nParsed value is empty`)); } else { @@ -122,12 +128,15 @@ function parseFile (file, options, $refs) { } function onError (err) { - if (err) { - err = err instanceof Error ? err : new Error(err); - reject(ono.syntax(err, `Error parsing ${file.url}`)); + if (!err || !("error" in err)) { + reject(ono.syntax(`Unable to parse ${file.url}`)); + } + else if (err.error instanceof ParserError || err.error instanceof StoplightParserError) { + reject(err); } else { - reject(ono.syntax(`Unable to parse ${file.url}`)); + err.error = new ParserError(err.error.message, file.url); + reject(err); } } })); diff --git a/lib/parsers/binary.js b/lib/parsers/binary.js index 0a3b280b..be15073c 100644 --- a/lib/parsers/binary.js +++ b/lib/parsers/binary.js @@ -41,7 +41,7 @@ module.exports = { * @param {string} file.url - The full URL of the referenced file * @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.) * @param {*} file.data - The file contents. This will be whatever data type was returned by the resolver - * @returns {Promise} + * @returns {Buffer} */ parse (file) { if (Buffer.isBuffer(file.data)) { diff --git a/lib/parsers/json.js b/lib/parsers/json.js index 45ac68dc..dac373d6 100644 --- a/lib/parsers/json.js +++ b/lib/parsers/json.js @@ -1,5 +1,9 @@ "use strict"; +const { parseWithPointers } = require("@stoplight/json"); +const { StoplightParserError } = require("../util/errors"); + + module.exports = { /** * The order that this parser will run, in relation to other parsers. @@ -21,7 +25,7 @@ module.exports = { * Parsers that don't match will be skipped, UNLESS none of the parsers match, in which case * every parser will be tried. * - * @type {RegExp|string[]|function} + * @type {RegExp|string|string[]|function} */ canParse: ".json", @@ -34,25 +38,31 @@ module.exports = { * @param {*} file.data - The file contents. This will be whatever data type was returned by the resolver * @returns {Promise} */ - parse (file) { - return new Promise(((resolve, reject) => { - let data = file.data; - if (Buffer.isBuffer(data)) { - data = data.toString(); - } + async parse (file) { + let data = file.data; + if (Buffer.isBuffer(data)) { + data = data.toString(); + } - if (typeof data === "string") { - if (data.trim().length === 0) { - resolve(undefined); // This mirrors the YAML behavior - } - else { - resolve(JSON.parse(data)); - } + if (typeof data === "string") { + if (data.trim().length === 0) { + return; } else { - // data is already a JavaScript value (object, array, number, null, NaN, etc.) - resolve(data); + let result = parseWithPointers(data, { + ignoreDuplicateKeys: false, + }); + + if (StoplightParserError.hasErrors(result.diagnostics)) { + throw new StoplightParserError(result.diagnostics, file.url); + } + + return result.data; } - })); + } + else { + // data is already a JavaScript value (object, array, number, null, NaN, etc.) + return data; + } } }; diff --git a/lib/parsers/text.js b/lib/parsers/text.js index f2452e15..03bcd96b 100644 --- a/lib/parsers/text.js +++ b/lib/parsers/text.js @@ -1,5 +1,7 @@ "use strict"; +const { ParserError } = require("../util/errors"); + let TEXT_REGEXP = /\.(txt|htm|html|md|xml|js|min|map|css|scss|less|svg)$/i; module.exports = { @@ -48,7 +50,7 @@ module.exports = { * @param {string} file.url - The full URL of the referenced file * @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.) * @param {*} file.data - The file contents. This will be whatever data type was returned by the resolver - * @returns {Promise} + * @returns {string} */ parse (file) { if (typeof file.data === "string") { @@ -58,7 +60,7 @@ module.exports = { return file.data.toString(this.encoding); } else { - throw new Error("data is not text"); + throw new ParserError("data is not text", file.url); } } }; diff --git a/lib/parsers/yaml.js b/lib/parsers/yaml.js index 13cd98c5..5b537495 100644 --- a/lib/parsers/yaml.js +++ b/lib/parsers/yaml.js @@ -1,6 +1,7 @@ "use strict"; const YAML = require("../util/yaml"); +const { StoplightParserError } = require("../util/errors"); module.exports = { /** @@ -36,20 +37,23 @@ module.exports = { * @param {*} file.data - The file contents. This will be whatever data type was returned by the resolver * @returns {Promise} */ - parse (file) { - return new Promise(((resolve, reject) => { - let data = file.data; - if (Buffer.isBuffer(data)) { - data = data.toString(); - } + async parse (file) { + let data = file.data; + if (Buffer.isBuffer(data)) { + data = data.toString(); + } - if (typeof data === "string") { - resolve(YAML.parse(data)); - } - else { - // data is already a JavaScript value (object, array, number, null, NaN, etc.) - resolve(data); + if (typeof data === "string") { + let result = YAML.parse(data); + if (StoplightParserError.hasErrors(result.diagnostics)) { + throw new StoplightParserError(result.diagnostics, file.url); } - })); + + return result.data; + } + else { + // data is already a JavaScript value (object, array, number, null, NaN, etc.) + return data; + } } }; diff --git a/lib/util/errors.js b/lib/util/errors.js index 0d9bf0c4..4be23ac7 100644 --- a/lib/util/errors.js +++ b/lib/util/errors.js @@ -18,25 +18,54 @@ const GenericErrorGroup = exports.GenericErrorGroup = class GenericErrorGroup ex constructor (errors, source) { super(); - this.source = source; + this._path = undefined; + this._source = source; this.errors = errors; } + + get source () { + return this._source; + } + + set source (source) { + this._source = source; + + for (let error of this.errors) { + error.source = source; + } + } + + get path () { + return this.path; + } + + set path (path) { + this._path = path; + + for (let error of this.errors) { + error.path = path; + } + } }; exports.StoplightParserError = class StoplightParserError extends GenericErrorGroup { - constructor (errors, source) { - super(errors.filter(error => error.severity === 0).map(error => { - const parsingError = new ParserError(error.message, source); - parsingError.message = error.message; - if (error.path) { - parsingError.path = error.path; - } - - return parsingError; + constructor (diagnostics, source) { + super(diagnostics.filter(StoplightParserError.pickError).map(error => { + let parserError = new ParserError(error.message, source); + parserError.message = error.message; + return parserError; })); this.message = `Error parsing ${source}`; } + + static pickError (diagnostic) { + return diagnostic.severity === 0; + } + + static hasErrors (diagnostics) { + return diagnostics.some(StoplightParserError.pickError); + } }; const ParserError = exports.ParserError = class ParserError extends GenericError { diff --git a/lib/util/plugins.js b/lib/util/plugins.js index 932281cf..3d327367 100644 --- a/lib/util/plugins.js +++ b/lib/util/plugins.js @@ -108,9 +108,12 @@ exports.run = function (plugins, method, file, $refs) { }); } - function onError (err) { + function onError (error) { // console.log(' %s', err.message || err); - lastError = err; + lastError = { + plugin, + error, + }; runNextPlugin(); } })); diff --git a/lib/util/yaml.js b/lib/util/yaml.js index db2e250a..ab802064 100644 --- a/lib/util/yaml.js +++ b/lib/util/yaml.js @@ -1,8 +1,7 @@ /* eslint lines-around-comment: [2, {beforeBlockComment: false}] */ "use strict"; -const yaml = require("js-yaml"); -const { ono } = require("ono"); +const { parseWithPointers, safeStringify } = require("@stoplight/yaml"); /** * Simple YAML parsing functions, similar to {@link JSON.parse} and {@link JSON.stringify} @@ -16,18 +15,11 @@ module.exports = { * @returns {*} */ parse (text, reviver) { - try { - return yaml.safeLoad(text); - } - catch (e) { - if (e instanceof Error) { - throw e; - } - else { - // https://github.com/nodeca/js-yaml/issues/153 - throw ono(e, e.message); - } - } + return parseWithPointers(text, { + json: true, + mergeKeys: true, + ignoreDuplicateKeys: false, + }); }, /** @@ -39,18 +31,7 @@ module.exports = { * @returns {string} */ stringify (value, replacer, space) { - try { - let indent = (typeof space === "string" ? space.length : space) || 2; - return yaml.safeDump(value, { indent }); - } - catch (e) { - if (e instanceof Error) { - throw e; - } - else { - // https://github.com/nodeca/js-yaml/issues/153 - throw ono(e, e.message); - } - } + let indent = (typeof space === "string" ? space.length : space) || 2; + return safeStringify(value, { indent }); } }; diff --git a/package-lock.json b/package-lock.json index d74dd0a5..4138d5b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -932,6 +932,40 @@ "fastq": "^1.6.0" } }, + "@stoplight/json": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.5.1.tgz", + "integrity": "sha512-O5WUW2yfAvtrqeq60YrbxpTvk87Ti2IeJ5oVa2XNJ2s+IIxx0CM+j316QoOjSGs+twrRpwb3jT9CFPrq7Ghkzg==", + "requires": { + "@stoplight/types": "^11.4.0", + "jsonc-parser": "~2.2.0", + "lodash": "^4.17.15", + "safe-stable-stringify": "^1.1" + } + }, + "@stoplight/types": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-11.4.0.tgz", + "integrity": "sha512-kMh1Sv7bA8BdbUaRXsRyi2K8Y5PzPOUWNSjB4qKOi0P6+dLczjlKggEIw9Xzuu1tCgBFdEvNwjnYDey0iqgeZQ==", + "requires": { + "@types/json-schema": "^7.0.3" + } + }, + "@stoplight/yaml": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-3.5.1.tgz", + "integrity": "sha512-pMhLVgHy/jYpKg6NKHEfWHB/k2sbmXpcRCtekvae8L98KcG8C3hEr2O+Hlft/5FE/PqIpb92lYh4/ypk0gN6Gg==", + "requires": { + "@stoplight/types": "^11.1.1", + "@stoplight/yaml-ast-parser": "0.0.44", + "lodash": "^4.17.15" + } + }, + "@stoplight/yaml-ast-parser": { + "version": "0.0.44", + "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.44.tgz", + "integrity": "sha512-PdY8p2Ufgtorf4d2DbKMfknILMa8KwuyyMMR/2lgK1mLaU8F5PKWYc+h9hIzC+ar0bh7m9h2rINo32m7ADfVyA==" + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -958,8 +992,7 @@ "@types/json-schema": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", - "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", - "dev": true + "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==" }, "@types/minimatch": { "version": "3.0.3", @@ -2294,6 +2327,12 @@ "type-detect": "^4.0.5" } }, + "chai-subset": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz", + "integrity": "sha1-pdDKFOMpp5WW7XAFi2ZGvWmIz+k=", + "dev": true + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5661,6 +5700,11 @@ "minimist": "^1.2.0" } }, + "jsonc-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", + "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -6015,8 +6059,7 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.camelcase": { "version": "4.3.0", @@ -8500,6 +8543,11 @@ "ret": "~0.1.10" } }, + "safe-stable-stringify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.0.tgz", + "integrity": "sha512-8h+96qSufNQrydRPzbHms38VftQQSRGbqUkaIMWUBWN4/N8sLNALIALa8KmFcQ8P/a9uzMkA+KY04Rj5WQiXPA==" + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", diff --git a/package.json b/package.json index d69b92ba..6806f636 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@types/json-schema": "^7.0.4", "@types/node": "^13.1.2", "chai": "^4.2.0", + "chai-subset": "^1.6.0", "coveralls": "^3.0.9", "eslint": "^6.8.0", "eslint-config-modular": "^7.0.1", @@ -68,6 +69,8 @@ "version-bump-prompt": "^5.0.7" }, "dependencies": { + "@stoplight/json": "^3.5.1", + "@stoplight/yaml": "^3.5.1", "call-me-maybe": "^1.0.1", "js-yaml": "^3.13.1", "ono": "^6.0.0" diff --git a/test/specs/callbacks.spec.js b/test/specs/callbacks.spec.js index 46f1f292..59920efa 100644 --- a/test/specs/callbacks.spec.js +++ b/test/specs/callbacks.spec.js @@ -4,6 +4,7 @@ const { expect } = require("chai"); const $RefParser = require("../../lib"); const helper = require("../utils/helper"); const path = require("../utils/path"); +const { StoplightParserError } = require("../../lib/util/errors"); describe("Callback & Promise syntax", () => { for (let method of ["parse", "resolve", "dereference", "bundle"]) { @@ -42,7 +43,7 @@ describe("Callback & Promise syntax", () => { return function (done) { $RefParser[method](path.rel("specs/invalid/invalid.yaml"), (err, result) => { try { - expect(err).to.be.an.instanceOf(SyntaxError); + expect(err).to.be.an.instanceOf(StoplightParserError); expect(result).to.be.undefined; done(); } @@ -75,7 +76,7 @@ describe("Callback & Promise syntax", () => { return $RefParser[method](path.rel("specs/invalid/invalid.yaml")) .then(helper.shouldNotGetCalled) .catch((err) => { - expect(err).to.be.an.instanceOf(SyntaxError); + expect(err).to.be.an.instanceOf(StoplightParserError); }); }; } diff --git a/test/specs/invalid/invalid.spec.js b/test/specs/invalid/invalid.spec.js index 53f71735..f44b1f5d 100644 --- a/test/specs/invalid/invalid.spec.js +++ b/test/specs/invalid/invalid.spec.js @@ -1,10 +1,14 @@ "use strict"; const { host } = require("host-environment"); -const { expect } = require("chai"); +const chai = require("chai"); +const chaiSubset = require("chai-subset"); +chai.use(chaiSubset); +const { expect } = chai; const $RefParser = require("../../../lib"); const helper = require("../../utils/helper"); const path = require("../../utils/path"); +const { StoplightParserError, ParserError } = require("../../../lib/util/errors"); describe("Invalid syntax", () => { describe("in main file", () => { @@ -28,7 +32,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(SyntaxError); + expect(err).to.be.an.instanceOf(StoplightParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.yaml"); } @@ -40,7 +44,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(SyntaxError); + expect(err).to.be.an.instanceOf(StoplightParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.json"); } @@ -52,7 +56,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(SyntaxError); + expect(err).to.be.an.instanceOf(StoplightParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.json"); } @@ -68,6 +72,60 @@ describe("Invalid syntax", () => { expect(err.message).to.contain('invalid/invalid.yaml" is not a valid JSON Schema'); } }); + + describe("when failFast is false", () => { + it("should not throw an error for an invalid YAML file", async () => { + const parser = new $RefParser(); + const result = await parser.dereference(path.rel("specs/invalid/invalid.yaml"), { failFast: false }); + expect(result).to.be.null; + expect(parser.errors.length).to.equal(1); + expect(parser.errors).to.containSubset([ + { + name: ParserError.name, + message: "incomplete explicit mapping pair; a key node is missed", + path: [], + source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.yaml"), + }, + ]); + }); + + it("should not throw an error for an invalid JSON file", async () => { + const parser = new $RefParser(); + const result = await parser.dereference(path.rel("specs/invalid/invalid.json"), { failFast: false }); + expect(result).to.be.null; + expect(parser.errors.length).to.equal(1); + expect(parser.errors).to.containSubset([ + { + name: ParserError.name, + message: "unexpected end of the stream within a flow collection", + path: [], + source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.json"), + } + ]); + }); + + it("should not throw an error for an invalid JSON file with YAML disabled", async () => { + const parser = new $RefParser(); + const result = await parser.dereference(path.rel("specs/invalid/invalid.json"), { failFast: false, parse: { yaml: false }}); + expect(result).to.be.null; + expect(parser.errors.length).to.equal(1); + expect(parser.errors).to.containSubset([ + { + name: ParserError.name, + message: "CloseBraceExpected", + path: [], + source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.json"), + } + ]); + }); + + it("should not throw an error for an invalid YAML file with JSON and YAML disabled", async () => { + const parser = new $RefParser(); + const result = await parser.dereference(path.rel("specs/invalid/invalid.yaml"), { failFast: false, parse: { yaml: false, json: false }}); + expect(result).to.be.null; + expect(parser.errors).to.deep.equal([]); + }); + }); }); describe("in referenced files", () => { @@ -77,7 +135,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(SyntaxError); + expect(err).to.be.an.instanceOf(StoplightParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.yaml"); } @@ -89,7 +147,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(SyntaxError); + expect(err).to.be.an.instanceOf(StoplightParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.json"); } @@ -103,7 +161,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(SyntaxError); + expect(err).to.be.an.instanceOf(StoplightParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.json"); } @@ -120,5 +178,54 @@ describe("Invalid syntax", () => { foo: ":\n" }); }); + + describe("when failFast is false", () => { + it("should not throw an error for an invalid YAML file", async () => { + const parser = new $RefParser(); + const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { failFast: false }); + expect(parser.errors.length).to.equal(1); + expect(parser.errors).to.containSubset([ + { + name: ParserError.name, + message: "incomplete explicit mapping pair; a key node is missed", + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/"), + }, + ]); + }); + + it("should not throw an error for an invalid JSON file", async () => { + const parser = new $RefParser(); + const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false }); + expect(parser.errors).to.containSubset([ + { + name: ParserError.name, + message: "unexpected end of the stream within a flow collection", + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/"), + } + ]); + }); + + it("should not throw an error for an invalid JSON file with YAML disabled", async () => { + const parser = new $RefParser(); + const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false, parse: { yaml: false }}); + expect(parser.errors).to.containSubset([ + { + name: ParserError.name, + message: "CloseBraceExpected", + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/"), + } + ]); + }); + + it("should not throw an error for an invalid YAML file with JSON and YAML disabled", async () => { + const parser = new $RefParser(); + const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { failFast: false, parse: { yaml: false, json: false }}); + expect(result).to.deep.equal({ foo: ":\n" }); + expect(parser.errors).to.deep.equal([]); + }); + }); }); }); diff --git a/test/specs/parsers/parsers.spec.js b/test/specs/parsers/parsers.spec.js index 3773a01f..e0613287 100644 --- a/test/specs/parsers/parsers.spec.js +++ b/test/specs/parsers/parsers.spec.js @@ -6,6 +6,7 @@ const helper = require("../../utils/helper"); const path = require("../../utils/path"); const parsedSchema = require("./parsed"); const dereferencedSchema = require("./dereferenced"); +const { StoplightParserError, ParserError } = require("../../../lib/util/errors"); describe("References to non-JSON files", () => { it("should parse successfully", async () => { @@ -70,7 +71,7 @@ describe("References to non-JSON files", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(SyntaxError); + expect(err).to.be.an.instanceOf(StoplightParserError); expect(err.message).to.contain("Error parsing "); } }); @@ -161,4 +162,26 @@ describe("References to non-JSON files", () => { expect(schema).to.deep.equal(dereferencedSchema.defaultParsers); }); + it("should normalize errors thrown by parsers", async () => { + try { + await $RefParser.dereference(path.rel("specs/parsers/parsers.yaml"), { + parse: { + // A custom parser that always fails, + // so the built-in parsers will be used as a fallback + yaml: { + order: 1, + parse () { + throw new Error("Woops"); + } + } + } + }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(ParserError); + expect(err.message).to.contain("Error parsing"); + expect(err.message).to.contain("arsers/parsers.yaml: Woops"); + } + }); }); diff --git a/test/specs/yaml.spec.js b/test/specs/yaml.spec.js index 28902dce..f9b47efe 100644 --- a/test/specs/yaml.spec.js +++ b/test/specs/yaml.spec.js @@ -18,7 +18,7 @@ describe("YAML object", () => { " type: number" ); - expect(obj).to.deep.equal({ + expect(obj).to.have.property("data").that.deep.equal({ title: "person", required: ["name", "age"], properties: { @@ -34,12 +34,12 @@ describe("YAML object", () => { it("should parse a string", async () => { let str = $RefParser.YAML.parse("hello, world"); - expect(str).to.equal("hello, world"); + expect(str).to.have.property("data", "hello, world"); }); it("should parse a number", async () => { let str = $RefParser.YAML.parse("42"); - expect(str).to.be.a("number").equal(42); + expect(str).to.have.property("data").that.is.a("number").and.equal(42); }); }); @@ -127,7 +127,7 @@ describe("YAML object", () => { it("should stringify a string", async () => { let yaml = $RefParser.YAML.stringify("hello, world"); - expect(yaml).to.equal("'hello, world'\n"); + expect(yaml).to.equal("hello, world"); }); it("should stringify a number", async () => { From e3b3562c212a18521afd1a9e5569ad284e6f9c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Wed, 19 Feb 2020 00:40:19 +0800 Subject: [PATCH 03/12] Allow resolver errors --- lib/parse.js | 8 ++- lib/resolvers/file.js | 7 +-- lib/resolvers/http.js | 7 +-- test/specs/invalid/invalid.spec.js | 68 +++++++------------------- test/specs/resolvers/resolvers.spec.js | 22 +++++++++ 5 files changed, 53 insertions(+), 59 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index e4f80a59..991466ea 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -80,11 +80,15 @@ function readFile (file, options, $refs) { function onError (err) { // Throw the original error, if it's one of our own (user-friendly) errors. // Otherwise, throw a generic, friendly error. - if (err && !(err instanceof SyntaxError)) { + if (!err || !(err instanceof SyntaxError)) { + reject(ono.syntax(`Unable to resolve $ref pointer "${file.url}"`)); + } + else if (err.error instanceof ResolverError) { reject(err); } else { - reject(ono.syntax(`Unable to resolve $ref pointer "${file.url}"`)); + err.error = new ResolverError(err, file.url); + reject(err); } } })); diff --git a/lib/resolvers/file.js b/lib/resolvers/file.js index c15fcb51..1c0ca11a 100644 --- a/lib/resolvers/file.js +++ b/lib/resolvers/file.js @@ -2,6 +2,7 @@ const fs = require("fs"); const { ono } = require("ono"); const url = require("../util/url"); +const { ResolverError } = require("../util/errors"); module.exports = { /** @@ -40,7 +41,7 @@ module.exports = { path = url.toFileSystemPath(file.url); } catch (err) { - reject(ono.uri(err, `Malformed URI: ${file.url}`)); + reject(new ResolverError(ono.uri(err, `Malformed URI: ${file.url}`), file.url)); } // console.log('Opening file: %s', path); @@ -48,7 +49,7 @@ module.exports = { try { fs.readFile(path, (err, data) => { if (err) { - reject(ono(err, `Error opening file "${path}"`)); + reject(new ResolverError(ono(err, `Error opening file "${path}"`), path)); } else { resolve(data); @@ -56,7 +57,7 @@ module.exports = { }); } catch (err) { - reject(ono(err, `Error opening file "${path}"`)); + reject(new ResolverError(ono(err, `Error opening file "${path}"`), path)); } })); } diff --git a/lib/resolvers/http.js b/lib/resolvers/http.js index 5dc88c5c..e0f86e6b 100644 --- a/lib/resolvers/http.js +++ b/lib/resolvers/http.js @@ -4,6 +4,7 @@ const http = require("http"); const https = require("https"); const { ono } = require("ono"); const url = require("../util/url"); +const { ResolverError } = require("../util/errors"); module.exports = { /** @@ -106,8 +107,8 @@ function download (u, httpOptions, redirects) { } else if (res.statusCode >= 300) { if (redirects.length > httpOptions.redirects) { - reject(ono({ status: res.statusCode }, - `Error downloading ${redirects[0]}. \nToo many redirects: \n ${redirects.join(" \n ")}`)); + reject(new ResolverError(ono({ status: res.statusCode }, + `Error downloading ${redirects[0]}. \nToo many redirects: \n ${redirects.join(" \n ")}`))); } else if (!res.headers.location) { throw ono({ status: res.statusCode }, `HTTP ${res.statusCode} redirect with no location header`); @@ -123,7 +124,7 @@ function download (u, httpOptions, redirects) { } }) .catch((err) => { - reject(ono(err, `Error downloading ${u.href}`)); + reject(new ResolverError(ono(err, `Error downloading ${u.href}`), u.href)); }); })); } diff --git a/test/specs/invalid/invalid.spec.js b/test/specs/invalid/invalid.spec.js index f44b1f5d..7a679a50 100644 --- a/test/specs/invalid/invalid.spec.js +++ b/test/specs/invalid/invalid.spec.js @@ -8,7 +8,7 @@ const { expect } = chai; const $RefParser = require("../../../lib"); const helper = require("../../utils/helper"); const path = require("../../utils/path"); -const { StoplightParserError, ParserError } = require("../../../lib/util/errors"); +const { StoplightParserError, ParserError, ResolverError } = require("../../../lib/util/errors"); describe("Invalid syntax", () => { describe("in main file", () => { @@ -18,7 +18,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(Error); + expect(err).to.be.an.instanceOf(ResolverError); if (host.node) { expect(err.code).to.equal("ENOENT"); expect(err.message).to.contain("Error opening file "); @@ -74,6 +74,21 @@ describe("Invalid syntax", () => { }); describe("when failFast is false", () => { + it("should not throw an error for an invalid file path", async () => { + const parser = new $RefParser(); + const result = await parser.dereference("this file does not exist", { failFast: false }); + expect(result).to.be.null; + expect(parser.errors.length).to.equal(1); + expect(parser.errors).to.containSubset([ + { + name: ResolverError.name, + message: expectedValue => expectedValue.startsWith("Error opening file"), + path: [], + source: expectedValue => expectedValue.endsWith("/test/this file does not exist"), + } + ]); + }); + it("should not throw an error for an invalid YAML file", async () => { const parser = new $RefParser(); const result = await parser.dereference(path.rel("specs/invalid/invalid.yaml"), { failFast: false }); @@ -178,54 +193,5 @@ describe("Invalid syntax", () => { foo: ":\n" }); }); - - describe("when failFast is false", () => { - it("should not throw an error for an invalid YAML file", async () => { - const parser = new $RefParser(); - const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { failFast: false }); - expect(parser.errors.length).to.equal(1); - expect(parser.errors).to.containSubset([ - { - name: ParserError.name, - message: "incomplete explicit mapping pair; a key node is missed", - path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/"), - }, - ]); - }); - - it("should not throw an error for an invalid JSON file", async () => { - const parser = new $RefParser(); - const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false }); - expect(parser.errors).to.containSubset([ - { - name: ParserError.name, - message: "unexpected end of the stream within a flow collection", - path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/"), - } - ]); - }); - - it("should not throw an error for an invalid JSON file with YAML disabled", async () => { - const parser = new $RefParser(); - const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false, parse: { yaml: false }}); - expect(parser.errors).to.containSubset([ - { - name: ParserError.name, - message: "CloseBraceExpected", - path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/"), - } - ]); - }); - - it("should not throw an error for an invalid YAML file with JSON and YAML disabled", async () => { - const parser = new $RefParser(); - const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { failFast: false, parse: { yaml: false, json: false }}); - expect(result).to.deep.equal({ foo: ":\n" }); - expect(parser.errors).to.deep.equal([]); - }); - }); }); }); diff --git a/test/specs/resolvers/resolvers.spec.js b/test/specs/resolvers/resolvers.spec.js index 736086b1..a9997710 100644 --- a/test/specs/resolvers/resolvers.spec.js +++ b/test/specs/resolvers/resolvers.spec.js @@ -6,6 +6,7 @@ const helper = require("../../utils/helper"); const path = require("../../utils/path"); const parsedSchema = require("./parsed"); const dereferencedSchema = require("./dereferenced"); +const { ResolverError } = require("../../../lib/util/errors"); describe("options.resolve", () => { it('should not resolve external links if "resolve.external" is disabled', async () => { @@ -111,4 +112,25 @@ describe("options.resolve", () => { expect(schema).to.deep.equal(dereferencedSchema); }); + it("should normalize errors thrown by resolvers", async () => { + try { + await $RefParser.dereference({ $ref: path.abs("specs/resolvers/resolvers.yaml") }, { + resolve: { + // A custom resolver that always fails + file: { + order: 1, + canRead: true, + parse () { + throw new Error("Woops"); + } + } + } + }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(ResolverError); + expect(err.message).to.contain("Error opening file"); + } + }); }); From d3ac54f9e885e30d07dec53cf90e766a2a4f3e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Sat, 22 Feb 2020 00:00:33 +0800 Subject: [PATCH 04/12] Allow pointer errors --- lib/bundle.js | 6 +- lib/dereference.js | 9 ++- lib/pointer.js | 28 +++++--- lib/ref.js | 21 ++++-- lib/refs.js | 9 +-- lib/resolve-external.js | 1 + lib/util/url.js | 18 +++++ test/specs/invalid/invalid.spec.js | 68 +++++++++++++++++++ .../missing-pointers/missing-pointers.spec.js | 36 ++++++++++ test/specs/refs.spec.js | 10 +-- 10 files changed, 180 insertions(+), 26 deletions(-) create mode 100644 test/specs/missing-pointers/missing-pointers.spec.js diff --git a/lib/bundle.js b/lib/bundle.js index 1fa423e2..f41563b2 100644 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -96,7 +96,11 @@ function crawl (parent, key, path, pathFromRoot, indirections, inventory, $refs, function inventory$Ref ($refParent, $refKey, path, pathFromRoot, indirections, inventory, $refs, options) { let $ref = $refKey === null ? $refParent : $refParent[$refKey]; let $refPath = url.resolve(path, $ref.$ref); - let pointer = $refs._resolve($refPath, options); + let pointer = $refs._resolve($refPath, pathFromRoot, options); + if (pointer === null) { + return; + } + let depth = Pointer.parse(pathFromRoot).length; let file = url.stripHash(pointer.path); let hash = url.getHash(pointer.path); diff --git a/lib/dereference.js b/lib/dereference.js index e8f541c5..8113d1cc 100644 --- a/lib/dereference.js +++ b/lib/dereference.js @@ -96,7 +96,14 @@ function dereference$Ref ($ref, path, pathFromRoot, parents, $refs, options) { // console.log('Dereferencing $ref pointer "%s" at %s', $ref.$ref, path); let $refPath = url.resolve(path, $ref.$ref); - let pointer = $refs._resolve($refPath, options); + let pointer = $refs._resolve($refPath, pathFromRoot, options); + + if (pointer === null) { + return { + circular: false, + value: null, + }; + } // Check for circular references let directCircular = pointer.circular; diff --git a/lib/pointer.js b/lib/pointer.js index 9f5baf17..9d5bcfac 100644 --- a/lib/pointer.js +++ b/lib/pointer.js @@ -4,7 +4,7 @@ module.exports = Pointer; const $Ref = require("./ref"); const url = require("./util/url"); -const { ono } = require("ono"); +const { GenericError, MissingPointerError, isHandledError } = require("./util/errors"); const slashes = /\//g; const tildes = /~/g; const escapedSlash = /~1/g; @@ -75,7 +75,8 @@ Pointer.prototype.resolve = function (obj, options) { let tokens = Pointer.parse(this.path); // Crawl the object, one token at a time - this.value = obj; + this.value = unwrapOrThrow(obj); + for (let i = 0; i < tokens.length; i++) { if (resolveIf$Ref(this, options)) { // The $ref path has changed, so append the remaining tokens to the path @@ -83,8 +84,9 @@ Pointer.prototype.resolve = function (obj, options) { } let token = tokens[i]; - if (this.value[token] === undefined) { - throw ono.syntax(`Error resolving $ref pointer "${this.originalPath}". \nToken "${token}" does not exist.`); + if (this.value[token] === undefined || this.value[token] === null) { + this.value = null; + throw new MissingPointerError(token, this.originalPath); } else { this.value = this.value[token]; @@ -117,7 +119,8 @@ Pointer.prototype.set = function (obj, value, options) { } // Crawl the object, one token at a time - this.value = obj; + this.value = unwrapOrThrow(obj); + for (let i = 0; i < tokens.length - 1; i++) { resolveIf$Ref(this, options); @@ -171,7 +174,7 @@ Pointer.parse = function (path) { } if (pointer[0] !== "") { - throw ono.syntax(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`); + throw new GenericError(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`); } return pointer.slice(1); @@ -222,7 +225,7 @@ function resolveIf$Ref (pointer, options) { pointer.circular = true; } else { - let resolved = pointer.$ref.$refs._resolve($refPath, options); + let resolved = pointer.$ref.$refs._resolve($refPath, url.getHash(pointer.path), options); pointer.indirections += resolved.indirections + 1; if ($Ref.isExtended$Ref(pointer.value)) { @@ -264,7 +267,16 @@ function setValue (pointer, token, value) { } } else { - throw ono.syntax(`Error assigning $ref pointer "${pointer.path}". \nCannot set "${token}" of a non-object.`); + throw new GenericError(`Error assigning $ref pointer "${pointer.path}". \nCannot set "${token}" of a non-object.`); + } + return value; +} + + +function unwrapOrThrow (value) { + if (isHandledError(value)) { + throw value; } + return value; } diff --git a/lib/ref.js b/lib/ref.js index 84a8ab1d..330dc0fb 100644 --- a/lib/ref.js +++ b/lib/ref.js @@ -3,7 +3,8 @@ module.exports = $Ref; const Pointer = require("./pointer"); -const { GenericError, GenericErrorGroup, ParserError, MissingPointerError, ResolverError } = require("./util/errors"); +const { GenericError, GenericErrorGroup, ParserError, MissingPointerError, ResolverError, isHandledError } = require("./util/errors"); +const { safePointerToPath } = require("./util/url"); /** * This class represents a single JSON reference and its resolved value. @@ -102,12 +103,24 @@ $Ref.prototype.get = function (path, options) { * * @param {string} path - The full path being resolved, optionally with a JSON pointer in the hash * @param {$RefParserOptions} options - * @param {string} [friendlyPath] - The original user-specified path (used for error messages) + * @param {string} friendlyPath - The original user-specified path (used for error messages) +* @param {string} pathFromRoot - The path of `obj` from the schema root * @returns {Pointer} */ -$Ref.prototype.resolve = function (path, options, friendlyPath) { +$Ref.prototype.resolve = function (path, options, friendlyPath, pathFromRoot) { let pointer = new Pointer(this, path, friendlyPath); - return pointer.resolve(this.value, options); + try { + return pointer.resolve(this.value, options); + } + catch (err) { + if (!options || options.failFast || !isHandledError(err)) { + throw err; + } + + err.path = safePointerToPath(pathFromRoot); + this.addError(err); + return null; + } }; /** diff --git a/lib/refs.js b/lib/refs.js index 81c2eeb6..996fd213 100644 --- a/lib/refs.js +++ b/lib/refs.js @@ -79,7 +79,7 @@ $Refs.prototype.toJSON = $Refs.prototype.values; */ $Refs.prototype.exists = function (path, options) { try { - this._resolve(path, options); + this._resolve(path, "", options); return true; } catch (e) { @@ -95,7 +95,7 @@ $Refs.prototype.exists = function (path, options) { * @returns {*} - Returns the resolved value */ $Refs.prototype.get = function (path, options) { - return this._resolve(path, options).value; + return this._resolve(path, "", options).value; }; /** @@ -139,11 +139,12 @@ $Refs.prototype._add = function (path) { * Resolves the given JSON reference. * * @param {string} path - The path being resolved, optionally with a JSON pointer in the hash + * @param {string} pathFromRoot - The path of `obj` from the schema root * @param {$RefParserOptions} [options] * @returns {Pointer} * @protected */ -$Refs.prototype._resolve = function (path, options) { +$Refs.prototype._resolve = function (path, pathFromRoot, options) { let absPath = url.resolve(this._root$Ref.path, path); let withoutHash = url.stripHash(absPath); let $ref = this._$refs[withoutHash]; @@ -152,7 +153,7 @@ $Refs.prototype._resolve = function (path, options) { throw ono(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`); } - return $ref.resolve(absPath, options, path); + return $ref.resolve(absPath, options, path, pathFromRoot); }; /** diff --git a/lib/resolve-external.js b/lib/resolve-external.js index 1155cfae..1d3b5cda 100644 --- a/lib/resolve-external.js +++ b/lib/resolve-external.js @@ -118,6 +118,7 @@ async function resolve$Ref ($ref, path, $refs, options) { if ($refs._$refs[withoutHash]) { err.source = url.stripHash(path); + err.path = url.safePointerToPath(url.getHash(path)); } return []; diff --git a/lib/util/url.js b/lib/util/url.js index b7157055..bc132342 100644 --- a/lib/util/url.js +++ b/lib/util/url.js @@ -1,5 +1,7 @@ "use strict"; +const { pointerToPath } = require("@stoplight/json"); + let isWindows = /^win/.test(process.platform), forwardSlashPattern = /\//g, protocolPattern = /^(\w{2,}):\/\//i, @@ -220,3 +222,19 @@ exports.toFileSystemPath = function toFileSystemPath (path, keepFileProtocol) { return path; }; + +/** + * Converts a $ref pointer to a valid JSON Path. + * It _does not_ throw. + * + * @param {string} pointer + * @returns {Array} + */ +exports.safePointerToPath = function safePointerToPath (pointer) { + try { + return pointerToPath(pointer); + } + catch (ex) { + return []; + } +}; diff --git a/test/specs/invalid/invalid.spec.js b/test/specs/invalid/invalid.spec.js index 7a679a50..4294f0dd 100644 --- a/test/specs/invalid/invalid.spec.js +++ b/test/specs/invalid/invalid.spec.js @@ -193,5 +193,73 @@ describe("Invalid syntax", () => { foo: ":\n" }); }); + + describe("when failFast is false", () => { + it("should not throw an error for an invalid file path", async () => { + const parser = new $RefParser(); + const result = await parser.dereference({ foo: { $ref: "this file does not exist" }}, { failFast: false }); + expect(parser.errors.length).to.equal(1); + expect(parser.errors).to.containSubset([ + { + name: ResolverError.name, + message: expectedValue => expectedValue.startsWith("Error opening file"), + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/"), + } + ]); + }); + + it("should not throw an error for an invalid YAML file", async () => { + const parser = new $RefParser(); + const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { failFast: false }); + expect(result).to.deep.equal({ foo: null }); + expect(parser.errors.length).to.equal(1); + expect(parser.errors).to.containSubset([ + { + name: ParserError.name, + message: "incomplete explicit mapping pair; a key node is missed", + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/"), + }, + ]); + }); + + it("should not throw an error for an invalid JSON file", async () => { + const parser = new $RefParser(); + const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false }); + expect(result).to.deep.equal({ foo: null }); + expect(parser.errors.length).to.equal(1); + expect(parser.errors).to.containSubset([ + { + name: ParserError.name, + message: "unexpected end of the stream within a flow collection", + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/"), + } + ]); + }); + + it("should not throw an error for an invalid JSON file with YAML disabled", async () => { + const parser = new $RefParser(); + const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false, parse: { yaml: false }}); + expect(result).to.deep.equal({ foo: null }); + expect(parser.errors.length).to.equal(1); + expect(parser.errors).to.containSubset([ + { + name: ParserError.name, + message: "CloseBraceExpected", + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/"), + } + ]); + }); + + it("should not throw an error for an invalid YAML file with JSON and YAML disabled", async () => { + const parser = new $RefParser(); + const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { failFast: false, parse: { yaml: false, json: false }}); + expect(result).to.deep.equal({ foo: ":\n" }); + expect(parser.errors).to.deep.equal([]); + }); + }); }); }); diff --git a/test/specs/missing-pointers/missing-pointers.spec.js b/test/specs/missing-pointers/missing-pointers.spec.js new file mode 100644 index 00000000..5848cd68 --- /dev/null +++ b/test/specs/missing-pointers/missing-pointers.spec.js @@ -0,0 +1,36 @@ +"use strict"; + +const chai = require("chai"); +const chaiSubset = require("chai-subset"); +chai.use(chaiSubset); +const { expect } = chai; +const $RefParser = require("../../../lib"); +const helper = require("../../utils/helper"); +const { MissingPointerError } = require("../../../lib/util/errors"); + +describe("Schema with missing pointers", () => { + it("should throw an error for missing pointer", async () => { + try { + await $RefParser.dereference({ foo: { $ref: "#/baz" }}); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.an.instanceOf(MissingPointerError); + expect(err.message).to.contain("Token \"baz\" does not exist."); + } + }); + + it("should not throw an error for missing pointer if failFast is false", async () => { + const parser = new $RefParser(); + const result = await parser.dereference({ foo: { $ref: "#/baz" }}, { failFast: false }); + expect(result).to.deep.equal({ foo: null }); + expect(parser.errors).to.containSubset([ + { + name: MissingPointerError.name, + message: "Token \"baz\" does not exist.", + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/"), + } + ]); + }); +}); diff --git a/test/specs/refs.spec.js b/test/specs/refs.spec.js index 5fe8ea90..f4fa12a3 100644 --- a/test/specs/refs.spec.js +++ b/test/specs/refs.spec.js @@ -216,10 +216,7 @@ describe("$Refs object", () => { } catch (err) { expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.equal( - 'Error resolving $ref pointer "definitions/name.yaml#/". ' + - '\nToken "" does not exist.' - ); + expect(err.message).to.equal('Token "" does not exist.'); } }); @@ -257,10 +254,7 @@ describe("$Refs object", () => { } catch (err) { expect(err).to.be.an.instanceOf(Error); - expect(err.message).to.equal( - 'Error resolving $ref pointer "external.yaml#/foo/bar". ' + - '\nToken "foo" does not exist.' - ); + expect(err.message).to.equal('Token "foo" does not exist.'); } }); }); From 1c0f45aa25991489bc7f1f441fd3aff2084953a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Sun, 23 Feb 2020 21:26:24 +0800 Subject: [PATCH 05/12] Update docs and typings --- docs/ref-parser.md | 8 ++++++++ lib/index.d.ts | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/docs/ref-parser.md b/docs/ref-parser.md index 0e0d3f1e..d69d20d1 100644 --- a/docs/ref-parser.md +++ b/docs/ref-parser.md @@ -6,6 +6,7 @@ This is the default export of JSON Schema $Ref Parser. You can creates instance ##### Properties - [`schema`](#schema) - [`$refs`](#refs) +- [`errors`](#errors) ##### Methods - [`dereference()`](#dereferenceschema-options-callback) @@ -42,6 +43,13 @@ await parser.dereference("my-schema.json"); parser.$refs.paths(); // => ["my-schema.json"] ``` +### `errors` +The `errors` property contains all list of errors that occurred during the bundling/resolving/dereferencing process. +All errors share error properties: +- path - json path to the document property +- message +- source - the uri of document where the faulty document was referenced + ### `dereference(schema, [options], [callback])` diff --git a/lib/index.d.ts b/lib/index.d.ts index 0c43d604..d5370417 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -25,6 +25,13 @@ declare class $RefParser { */ $refs: $RefParser.$Refs + /** + * List of all errors + * + * See https://github.com/APIDevTools/json-schema-ref-parser/blob/master/docs/ref-parser.md#errors + */ + errors: Array<$RefParser.GenericError | $RefParser.ResolverError | $RefParser.ParserError | $RefParser.MissingPointerError>; + /** * Dereferences all `$ref` pointers in the JSON Schema, replacing each reference with its resolved value. This results in a schema object that does not contain any `$ref` pointers. Instead, it's a normal JavaScript object tree that can easily be crawled and used just like any other JavaScript object. This is great for programmatic usage, especially when using tools that don't understand JSON references. * @@ -210,6 +217,12 @@ declare namespace $RefParser { [key: string]: Partial } + /** + * Determines how lenient the processing should be. + * If this option is enable, the processing will be performed in a bail mode - will abort upon the first exception. + */ + failFast?: boolean; + /** * The `dereference` options control how JSON Schema `$Ref` Parser will dereference `$ref` pointers within the JSON schema. */ @@ -398,4 +411,15 @@ declare namespace $RefParser { set($ref: string, value: JSONSchema4Type | JSONSchema6Type): void } + export class GenericError extends Error { + readonly message: string; + readonly path: Array; + readonly source: string; + } + + export class ParserError extends GenericError {} + export class ResolverError extends GenericError { + readonly code?: string; + } + export class MissingPointerError extends GenericError {} } From c482edd307e56ac6342fdaa2dda22dec7d3e160c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Mon, 24 Feb 2020 01:34:39 +0800 Subject: [PATCH 06/12] Allow no parser or resolver to be matched --- lib/index.d.ts | 4 ++- lib/index.js | 6 ++-- lib/parse.js | 40 +++++++++++++----------- lib/util/errors.js | 18 ++++++++++- test/specs/parsers/parsers.spec.js | 43 +++++++++++++++++++++++++- test/specs/resolvers/resolvers.spec.js | 28 +++++++++++++++-- 6 files changed, 114 insertions(+), 25 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index d5370417..d7028e8e 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -30,7 +30,7 @@ declare class $RefParser { * * See https://github.com/APIDevTools/json-schema-ref-parser/blob/master/docs/ref-parser.md#errors */ - errors: Array<$RefParser.GenericError | $RefParser.ResolverError | $RefParser.ParserError | $RefParser.MissingPointerError>; + errors: Array<$RefParser.GenericError | $RefParser.ResolverError | $RefParser.ParserError | $RefParser.MissingPointerError | $RefParser.UnmatchedParserError | $RefParser.UnmatchedResolverError>; /** * Dereferences all `$ref` pointers in the JSON Schema, replacing each reference with its resolved value. This results in a schema object that does not contain any `$ref` pointers. Instead, it's a normal JavaScript object tree that can easily be crawled and used just like any other JavaScript object. This is great for programmatic usage, especially when using tools that don't understand JSON references. @@ -418,8 +418,10 @@ declare namespace $RefParser { } export class ParserError extends GenericError {} + export class UnmatchedParserError extends GenericError {} export class ResolverError extends GenericError { readonly code?: string; } + export class UnmatchedResolverError extends GenericError {} export class MissingPointerError extends GenericError {} } diff --git a/lib/index.js b/lib/index.js index a58f8925..2c0273a2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,7 +8,7 @@ const resolveExternal = require("./resolve-external"); const bundle = require("./bundle"); const dereference = require("./dereference"); const url = require("./util/url"); -const { GenericError, MissingPointerError, ResolverError, ParserError, isHandledError } = require("./util/errors"); +const { GenericError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError } = require("./util/errors"); const maybe = require("call-me-maybe"); const { ono } = require("ono"); @@ -18,6 +18,8 @@ module.exports.GenericError = GenericError; module.exports.MissingPointerError = MissingPointerError; module.exports.ResolverError = ResolverError; module.exports.ParserError = ParserError; +module.exports.UnmatchedParserError = UnmatchedParserError; +module.exports.UnmatchedResolverError = UnmatchedResolverError; /** * This class parses a JSON schema, builds a map of its JSON references and their resolved values, @@ -45,7 +47,7 @@ function $RefParser () { /** * List of all errors - * @type {Array} + * @type {Array} */ Object.defineProperty($RefParser.prototype, "errors", { get () { diff --git a/lib/parse.js b/lib/parse.js index 991466ea..36229c0d 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -3,7 +3,7 @@ const { ono } = require("ono"); const url = require("./util/url"); const plugins = require("./util/plugins"); -const { StoplightParserError, ResolverError, ParserError } = require("./util/errors"); +const { StoplightParserError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError } = require("./util/errors"); module.exports = parse; @@ -42,14 +42,12 @@ async function parse (path, $refs, options) { return parser.result; } - catch (ex) { - if (!("error" in ex)) { - throw ex; - } - else { - $ref.value = ex.error; - throw ex.error; + catch (err) { + if (isHandledError(err)) { + $ref.value = err; } + + throw err; } } @@ -78,17 +76,20 @@ function readFile (file, options, $refs) { .then(resolve, onError); function onError (err) { - // Throw the original error, if it's one of our own (user-friendly) errors. - // Otherwise, throw a generic, friendly error. - if (!err || !(err instanceof SyntaxError)) { + if (!err && !options.failFast) { + // No resolver could be matched + reject(new UnmatchedResolverError(file.url)); + } + else if (!err || !("error" in err)) { + // Throw a generic, friendly error. reject(ono.syntax(`Unable to resolve $ref pointer "${file.url}"`)); } + // Throw the original error, if it's one of our own (user-friendly) errors. else if (err.error instanceof ResolverError) { - reject(err); + reject(err.error); } else { - err.error = new ResolverError(err, file.url); - reject(err); + reject(new ResolverError(err, file.url)); } } })); @@ -132,15 +133,18 @@ function parseFile (file, options, $refs) { } function onError (err) { - if (!err || !("error" in err)) { + if (!err && !options.failFast) { + // No resolver could be matched + reject(new UnmatchedParserError(file.url)); + } + else if (!err || !("error" in err)) { reject(ono.syntax(`Unable to parse ${file.url}`)); } else if (err.error instanceof ParserError || err.error instanceof StoplightParserError) { - reject(err); + reject(err.error); } else { - err.error = new ParserError(err.error.message, file.url); - reject(err); + reject(new ParserError(err.error.message, file.url)); } } })); diff --git a/lib/util/errors.js b/lib/util/errors.js index 4be23ac7..00244099 100644 --- a/lib/util/errors.js +++ b/lib/util/errors.js @@ -76,9 +76,17 @@ const ParserError = exports.ParserError = class ParserError extends GenericError setErrorName(ParserError); +const UnmatchedParserError = exports.UnmatchedParserError = class UnmatchedParserError extends GenericError { + constructor (source) { + super(`Could not find parser for "${source}"`, source); + } +}; + +setErrorName(UnmatchedParserError); + const ResolverError = exports.ResolverError = class ResolverError extends GenericError { constructor (ex, source) { - super(ex.message || `Error reading file ${source}`, source); + super(ex.message || `Error reading file "${source}"`, source); if ("code" in ex) { this.code = String(ex.code); } @@ -87,6 +95,14 @@ const ResolverError = exports.ResolverError = class ResolverError extends Generi setErrorName(ResolverError); +const UnmatchedResolverError = exports.UnmatchedResolverError = class UnmatchedResolverError extends GenericError { + constructor (source) { + super(`Could not find resolver for "${source}"`, source); + } +}; + +setErrorName(UnmatchedResolverError); + const MissingPointerError = exports.MissingPointerError = class MissingPointerError extends GenericError { constructor (token, path) { super(`Token "${token}" does not exist.`, stripHash(path)); diff --git a/test/specs/parsers/parsers.spec.js b/test/specs/parsers/parsers.spec.js index e0613287..8bdd6bfd 100644 --- a/test/specs/parsers/parsers.spec.js +++ b/test/specs/parsers/parsers.spec.js @@ -6,7 +6,7 @@ const helper = require("../../utils/helper"); const path = require("../../utils/path"); const parsedSchema = require("./parsed"); const dereferencedSchema = require("./dereferenced"); -const { StoplightParserError, ParserError } = require("../../../lib/util/errors"); +const { StoplightParserError, ParserError, UnmatchedParserError } = require("../../../lib/util/errors"); describe("References to non-JSON files", () => { it("should parse successfully", async () => { @@ -65,6 +65,24 @@ describe("References to non-JSON files", () => { expect(schema).to.deep.equal(dereferencedSchema.binaryParser); }); + it("should throw an error if no no parser can be matched", async () => { + try { + await $RefParser.dereference(path.rel("specs/parsers/parsers.yaml"), { + parse: { + yaml: false, + json: false, + text: false, + binary: false, + }, + }); + } + catch (err) { + expect(err).to.be.an.instanceOf(SyntaxError); + expect(err.message).to.contain("Unable to parse "); + expect(err.message).to.contain("parsers/parsers.yaml"); + } + }); + it('should throw an error if "parse.text" and "parse.binary" are disabled', async () => { try { await $RefParser.dereference(path.rel("specs/parsers/parsers.yaml"), { parse: { text: false, binary: false }}); @@ -184,4 +202,27 @@ describe("References to non-JSON files", () => { expect(err.message).to.contain("arsers/parsers.yaml: Woops"); } }); + + it("should let no parser to be matched if fastFail is false", async () => { + const parser = new $RefParser(); + await parser.dereference(path.rel("specs/parsers/parsers.yaml"), { + parse: { + yaml: false, + json: false, + text: false, + binary: false, + }, + failFast: false, + }); + + expect(parser.errors.length).to.equal(1); + expect(parser.errors).to.containSubset([ + { + name: UnmatchedParserError.name, + message: expectedValue => expectedValue.startsWith("Could not find parser for"), + path: [], + source: expectedValue => expectedValue.endsWith("specs/parsers/parsers.yaml"), + }, + ]); + }); }); diff --git a/test/specs/resolvers/resolvers.spec.js b/test/specs/resolvers/resolvers.spec.js index a9997710..7f0db463 100644 --- a/test/specs/resolvers/resolvers.spec.js +++ b/test/specs/resolvers/resolvers.spec.js @@ -1,12 +1,15 @@ "use strict"; -const { expect } = require("chai"); +const chai = require("chai"); +const chaiSubset = require("chai-subset"); +chai.use(chaiSubset); +const { expect } = chai; const $RefParser = require("../../.."); const helper = require("../../utils/helper"); const path = require("../../utils/path"); const parsedSchema = require("./parsed"); const dereferencedSchema = require("./dereferenced"); -const { ResolverError } = require("../../../lib/util/errors"); +const { ResolverError, UnmatchedResolverError } = require("../../../lib/util/errors"); describe("options.resolve", () => { it('should not resolve external links if "resolve.external" is disabled', async () => { @@ -133,4 +136,25 @@ describe("options.resolve", () => { expect(err.message).to.contain("Error opening file"); } }); + + it("should let no resolver to be matched if fastFail is false", async () => { + const parser = new $RefParser(); + await parser.dereference(path.abs("specs/resolvers/resolvers.yaml"), { + resolve: { + file: false, + http: false, + }, + failFast: false, + }); + + expect(parser.errors.length).to.equal(1); + expect(parser.errors).to.containSubset([ + { + name: UnmatchedResolverError.name, + message: expectedValue => expectedValue.startsWith("Could not find resolver for"), + path: [], + source: expectedValue => expectedValue.endsWith("specs/resolvers/resolvers.yaml"), + }, + ]); + }); }); From c9c11b313690bd1743a258c5b7062f8fd45c0acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Mon, 24 Feb 2020 02:09:52 +0800 Subject: [PATCH 07/12] Update tests for browser --- test/specs/invalid/invalid.spec.js | 15 ++++++++------- .../missing-pointers/missing-pointers.spec.js | 2 +- test/specs/parsers/parsers.spec.js | 7 +++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/test/specs/invalid/invalid.spec.js b/test/specs/invalid/invalid.spec.js index 4294f0dd..4353d9c6 100644 --- a/test/specs/invalid/invalid.spec.js +++ b/test/specs/invalid/invalid.spec.js @@ -82,9 +82,9 @@ describe("Invalid syntax", () => { expect(parser.errors).to.containSubset([ { name: ResolverError.name, - message: expectedValue => expectedValue.startsWith("Error opening file"), + message: expectedValue => expectedValue.startsWith("Error opening file") || expectedValue.endsWith("HTTP ERROR 404"), path: [], - source: expectedValue => expectedValue.endsWith("/test/this file does not exist"), + source: expectedValue => expectedValue.endsWith("this file does not exist") || expectedValue.startsWith("http://localhost"), } ]); }); @@ -198,13 +198,14 @@ describe("Invalid syntax", () => { it("should not throw an error for an invalid file path", async () => { const parser = new $RefParser(); const result = await parser.dereference({ foo: { $ref: "this file does not exist" }}, { failFast: false }); + expect(result).to.deep.equal({ foo: null }); expect(parser.errors.length).to.equal(1); expect(parser.errors).to.containSubset([ { name: ResolverError.name, - message: expectedValue => expectedValue.startsWith("Error opening file"), + message: expectedValue => expectedValue.startsWith("Error opening file") || expectedValue.endsWith("HTTP ERROR 404"), path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/"), + source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), } ]); }); @@ -219,7 +220,7 @@ describe("Invalid syntax", () => { name: ParserError.name, message: "incomplete explicit mapping pair; a key node is missed", path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/"), + source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), }, ]); }); @@ -234,7 +235,7 @@ describe("Invalid syntax", () => { name: ParserError.name, message: "unexpected end of the stream within a flow collection", path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/"), + source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), } ]); }); @@ -249,7 +250,7 @@ describe("Invalid syntax", () => { name: ParserError.name, message: "CloseBraceExpected", path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/"), + source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), } ]); }); diff --git a/test/specs/missing-pointers/missing-pointers.spec.js b/test/specs/missing-pointers/missing-pointers.spec.js index 5848cd68..ce705445 100644 --- a/test/specs/missing-pointers/missing-pointers.spec.js +++ b/test/specs/missing-pointers/missing-pointers.spec.js @@ -29,7 +29,7 @@ describe("Schema with missing pointers", () => { name: MissingPointerError.name, message: "Token \"baz\" does not exist.", path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/"), + source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), } ]); }); diff --git a/test/specs/parsers/parsers.spec.js b/test/specs/parsers/parsers.spec.js index 8bdd6bfd..2dcaf5a0 100644 --- a/test/specs/parsers/parsers.spec.js +++ b/test/specs/parsers/parsers.spec.js @@ -1,6 +1,9 @@ "use strict"; -const { expect } = require("chai"); +const chai = require("chai"); +const chaiSubset = require("chai-subset"); +chai.use(chaiSubset); +const { expect } = chai; const $RefParser = require("../../.."); const helper = require("../../utils/helper"); const path = require("../../utils/path"); @@ -221,7 +224,7 @@ describe("References to non-JSON files", () => { name: UnmatchedParserError.name, message: expectedValue => expectedValue.startsWith("Could not find parser for"), path: [], - source: expectedValue => expectedValue.endsWith("specs/parsers/parsers.yaml"), + source: expectedValue => expectedValue.endsWith("specs/parsers/parsers.yaml") || expectedValue.startsWith("http://localhost"), }, ]); }); From 0b2a9d204ef13f50f0e1e4e232dbc7c184c5b3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Mon, 16 Mar 2020 14:21:37 +0800 Subject: [PATCH 08/12] Dedicated error for invalid pointers --- lib/index.d.ts | 3 +- lib/index.js | 3 +- lib/pointer.js | 9 +++-- lib/util/errors.js | 8 ++++ .../invalid-pointers/invalid-pointers.js | 37 +++++++++++++++++++ test/specs/invalid-pointers/invalid.json | 5 +++ 6 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 test/specs/invalid-pointers/invalid-pointers.js create mode 100644 test/specs/invalid-pointers/invalid.json diff --git a/lib/index.d.ts b/lib/index.d.ts index d7028e8e..9acaaa9e 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -30,7 +30,7 @@ declare class $RefParser { * * See https://github.com/APIDevTools/json-schema-ref-parser/blob/master/docs/ref-parser.md#errors */ - errors: Array<$RefParser.GenericError | $RefParser.ResolverError | $RefParser.ParserError | $RefParser.MissingPointerError | $RefParser.UnmatchedParserError | $RefParser.UnmatchedResolverError>; + errors: Array<$RefParser.GenericError | $RefParser.InvalidPointerError | $RefParser.ResolverError | $RefParser.ParserError | $RefParser.MissingPointerError | $RefParser.UnmatchedParserError | $RefParser.UnmatchedResolverError>; /** * Dereferences all `$ref` pointers in the JSON Schema, replacing each reference with its resolved value. This results in a schema object that does not contain any `$ref` pointers. Instead, it's a normal JavaScript object tree that can easily be crawled and used just like any other JavaScript object. This is great for programmatic usage, especially when using tools that don't understand JSON references. @@ -424,4 +424,5 @@ declare namespace $RefParser { } export class UnmatchedResolverError extends GenericError {} export class MissingPointerError extends GenericError {} + export class InvalidPointerError extends GenericError {} } diff --git a/lib/index.js b/lib/index.js index 2c0273a2..9b96da59 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,13 +8,14 @@ const resolveExternal = require("./resolve-external"); const bundle = require("./bundle"); const dereference = require("./dereference"); const url = require("./util/url"); -const { GenericError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError } = require("./util/errors"); +const { GenericError, InvalidPointerError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError } = require("./util/errors"); const maybe = require("call-me-maybe"); const { ono } = require("ono"); module.exports = $RefParser; module.exports.YAML = require("./util/yaml"); module.exports.GenericError = GenericError; +module.exports.InvalidPointerError = InvalidPointerError; module.exports.MissingPointerError = MissingPointerError; module.exports.ResolverError = ResolverError; module.exports.ParserError = ParserError; diff --git a/lib/pointer.js b/lib/pointer.js index 9d5bcfac..cb053f7e 100644 --- a/lib/pointer.js +++ b/lib/pointer.js @@ -4,7 +4,7 @@ module.exports = Pointer; const $Ref = require("./ref"); const url = require("./util/url"); -const { GenericError, MissingPointerError, isHandledError } = require("./util/errors"); +const { GenericError, InvalidPointerError, MissingPointerError, isHandledError } = require("./util/errors"); const slashes = /\//g; const tildes = /~/g; const escapedSlash = /~1/g; @@ -72,7 +72,7 @@ function Pointer ($ref, path, friendlyPath) { * of the resolved value. */ Pointer.prototype.resolve = function (obj, options) { - let tokens = Pointer.parse(this.path); + let tokens = Pointer.parse(this.path, this.originalPath); // Crawl the object, one token at a time this.value = unwrapOrThrow(obj); @@ -153,9 +153,10 @@ Pointer.prototype.set = function (obj, value, options) { * {@link https://tools.ietf.org/html/rfc6901#section-3} * * @param {string} path + * @param {string} [originalPath] * @returns {string[]} */ -Pointer.parse = function (path) { +Pointer.parse = function (path, originalPath) { // Get the JSON pointer from the path's hash let pointer = url.getHash(path).substr(1); @@ -174,7 +175,7 @@ Pointer.parse = function (path) { } if (pointer[0] !== "") { - throw new GenericError(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`); + throw new InvalidPointerError(pointer, originalPath === undefined ? path : originalPath); } return pointer.slice(1); diff --git a/lib/util/errors.js b/lib/util/errors.js index 00244099..5ab4f2ea 100644 --- a/lib/util/errors.js +++ b/lib/util/errors.js @@ -111,6 +111,14 @@ const MissingPointerError = exports.MissingPointerError = class MissingPointerEr setErrorName(MissingPointerError); +const InvalidPointerError = exports.InvalidPointerError = class InvalidPointerError extends GenericError { + constructor (pointer, path) { + super(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`, stripHash(path)); + } +}; + +setErrorName(InvalidPointerError); + function setErrorName (err) { Object.defineProperty(err.prototype, "name", { value: err.name, diff --git a/test/specs/invalid-pointers/invalid-pointers.js b/test/specs/invalid-pointers/invalid-pointers.js new file mode 100644 index 00000000..16e87446 --- /dev/null +++ b/test/specs/invalid-pointers/invalid-pointers.js @@ -0,0 +1,37 @@ +"use strict"; + +const chai = require("chai"); +const chaiSubset = require("chai-subset"); +chai.use(chaiSubset); +const { expect } = chai; +const $RefParser = require("../../../lib"); +const helper = require("../../utils/helper"); +const path = require("../../utils/path"); +const { InvalidPointerError } = require("../../../lib/util/errors"); + +describe("Schema with invalid pointers", () => { + it("should throw an error for invalid pointer", async () => { + try { + await $RefParser.dereference(path.rel("specs/invalid-pointers/invalid.json")); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.an.instanceOf(InvalidPointerError); + expect(err.message).to.contain("Invalid $ref pointer \"f\". Pointers must begin with \"#/\""); + } + }); + + it("should not throw an error for invalid pointer if failFast is false", async () => { + const parser = new $RefParser(); + const result = await parser.dereference(path.rel("specs/invalid-pointers/invalid.json"), { failFast: false }); + expect(result).to.deep.equal({ foo: null }); + expect(parser.errors).to.containSubset([ + { + name: InvalidPointerError.name, + message: "Invalid $ref pointer \"f\". Pointers must begin with \"#/\"", + path: ["foo"], + source: path.abs("specs/invalid-pointers/invalid.json"), + } + ]); + }); +}); diff --git a/test/specs/invalid-pointers/invalid.json b/test/specs/invalid-pointers/invalid.json new file mode 100644 index 00000000..fd0d8ea3 --- /dev/null +++ b/test/specs/invalid-pointers/invalid.json @@ -0,0 +1,5 @@ +{ + "foo": { + "$ref": "./invalid.json#f" + } +} From 5d1a1a11f5d02b4ca05e86b6994971072b8a1fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Mon, 16 Mar 2020 14:35:46 +0800 Subject: [PATCH 09/12] Levearge Ono to make exceptions serialiazable --- lib/util/errors.js | 6 ++++++ package-lock.json | 28 ++++++++++++++++++++-------- package.json | 5 ++--- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/util/errors.js b/lib/util/errors.js index 5ab4f2ea..6b82bffe 100644 --- a/lib/util/errors.js +++ b/lib/util/errors.js @@ -1,5 +1,7 @@ "use strict"; +const { Ono } = require("ono"); + const { stripHash } = require("./url"); const GenericError = exports.GenericError = class GenericError extends Error { @@ -9,6 +11,8 @@ const GenericError = exports.GenericError = class GenericError extends Error { this.message = message; this.source = source; this.path = []; + + Ono.extend(this); } }; @@ -21,6 +25,8 @@ const GenericErrorGroup = exports.GenericErrorGroup = class GenericErrorGroup ex this._path = undefined; this._source = source; this.errors = errors; + + Ono.extend(this); } get source () { diff --git a/package-lock.json b/package-lock.json index 4138d5b8..7eacc752 100644 --- a/package-lock.json +++ b/package-lock.json @@ -906,6 +906,11 @@ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "dev": true }, + "@jsdevtools/ono": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.1.tgz", + "integrity": "sha512-pu5fxkbLQWzRbBgfFbZfHXz0KlYojOfVdUhcNfy9lef8ZhBt0pckGr8g7zv4vPX4Out5vBNvqd/az4UaVWzZ9A==" + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -952,9 +957,9 @@ } }, "@stoplight/yaml": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-3.5.1.tgz", - "integrity": "sha512-pMhLVgHy/jYpKg6NKHEfWHB/k2sbmXpcRCtekvae8L98KcG8C3hEr2O+Hlft/5FE/PqIpb92lYh4/ypk0gN6Gg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-3.6.0.tgz", + "integrity": "sha512-38EhZjNsVwuwrxPawr5R3Q1QhO3l/MTLD5F/d0v7vIrvWgde6GJzdh6rmWbrKJttEqqwuqX8mpLjWihWB7Qm1A==", "requires": { "@stoplight/types": "^11.1.1", "@stoplight/yaml-ast-parser": "0.0.44", @@ -1353,6 +1358,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -3792,7 +3798,8 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true }, "esquery": { "version": "1.0.1", @@ -5644,6 +5651,7 @@ "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -7473,9 +7481,12 @@ } }, "ono": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ono/-/ono-6.0.0.tgz", - "integrity": "sha512-vhx50giT0dDBLYYXwKU/tuNsT6CwPzGZmd6yypPsXrkq+ujT0lX0q4tvMQ/5jxM6HKntk7p3N51Ts0fD8qL5dA==" + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ono/-/ono-7.1.1.tgz", + "integrity": "sha512-k7lbQdKDAjJ+022kB2I3ONAWvnjI/KnfdvKzCatERyUcCwedrTECcUBa8WMEbXrFc7tAhfurMXJE1oZkK+2XLg==", + "requires": { + "@jsdevtools/ono": "7.1.1" + } }, "optimist": { "version": "0.6.1", @@ -9127,7 +9138,8 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true }, "sshpk": { "version": "1.16.1", diff --git a/package.json b/package.json index 6806f636..7f4f17d7 100644 --- a/package.json +++ b/package.json @@ -70,9 +70,8 @@ }, "dependencies": { "@stoplight/json": "^3.5.1", - "@stoplight/yaml": "^3.5.1", + "@stoplight/yaml": "^3.6.0", "call-me-maybe": "^1.0.1", - "js-yaml": "^3.13.1", - "ono": "^6.0.0" + "ono": "^7.1.1" } } From fdb277417e4f35c03c555a017184cb135ffcd7a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Mon, 16 Mar 2020 14:56:42 +0800 Subject: [PATCH 10/12] Rename GenericError to JSONParserError --- lib/index.d.ts | 16 ++++++++-------- lib/index.js | 6 +++--- lib/pointer.js | 4 ++-- lib/ref.js | 6 +++--- lib/util/errors.js | 22 +++++++++++----------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index e450f116..775e504a 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -30,7 +30,7 @@ declare class $RefParser { * * See https://github.com/APIDevTools/json-schema-ref-parser/blob/master/docs/ref-parser.md#errors */ - errors: Array<$RefParser.GenericError | $RefParser.InvalidPointerError | $RefParser.ResolverError | $RefParser.ParserError | $RefParser.MissingPointerError | $RefParser.UnmatchedParserError | $RefParser.UnmatchedResolverError>; + errors: Array<$RefParser.JSONParserError | $RefParser.InvalidPointerError | $RefParser.ResolverError | $RefParser.ParserError | $RefParser.MissingPointerError | $RefParser.UnmatchedParserError | $RefParser.UnmatchedResolverError>; /** * Dereferences all `$ref` pointers in the JSON Schema, replacing each reference with its resolved value. This results in a schema object that does not contain any `$ref` pointers. Instead, it's a normal JavaScript object tree that can easily be crawled and used just like any other JavaScript object. This is great for programmatic usage, especially when using tools that don't understand JSON references. @@ -411,18 +411,18 @@ declare namespace $RefParser { set($ref: string, value: JSONSchema4Type | JSONSchema6Type): void } - export class GenericError extends Error { + export class JSONParserError extends Error { readonly message: string; readonly path: Array; readonly source: string; } - export class ParserError extends GenericError {} - export class UnmatchedParserError extends GenericError {} - export class ResolverError extends GenericError { + export class ParserError extends JSONParserError {} + export class UnmatchedParserError extends JSONParserError {} + export class ResolverError extends JSONParserError { readonly code?: string; } - export class UnmatchedResolverError extends GenericError {} - export class MissingPointerError extends GenericError {} - export class InvalidPointerError extends GenericError {} + export class UnmatchedResolverError extends JSONParserError {} + export class MissingPointerError extends JSONParserError {} + export class InvalidPointerError extends JSONParserError {} } diff --git a/lib/index.js b/lib/index.js index de446688..5d49bf90 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,13 +8,13 @@ const resolveExternal = require("./resolve-external"); const bundle = require("./bundle"); const dereference = require("./dereference"); const url = require("./util/url"); -const { GenericError, InvalidPointerError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError } = require("./util/errors"); +const { JSONParserError, InvalidPointerError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError } = require("./util/errors"); const maybe = require("call-me-maybe"); const { ono } = require("@jsdevtools/ono"); module.exports = $RefParser; module.exports.YAML = require("./util/yaml"); -module.exports.GenericError = GenericError; +module.exports.JSONParserError = JSONParserError; module.exports.InvalidPointerError = InvalidPointerError; module.exports.MissingPointerError = MissingPointerError; module.exports.ResolverError = ResolverError; @@ -48,7 +48,7 @@ function $RefParser () { /** * List of all errors - * @type {Array} + * @type {Array} */ Object.defineProperty($RefParser.prototype, "errors", { get () { diff --git a/lib/pointer.js b/lib/pointer.js index cb053f7e..b312e95d 100644 --- a/lib/pointer.js +++ b/lib/pointer.js @@ -4,7 +4,7 @@ module.exports = Pointer; const $Ref = require("./ref"); const url = require("./util/url"); -const { GenericError, InvalidPointerError, MissingPointerError, isHandledError } = require("./util/errors"); +const { JSONParserError, InvalidPointerError, MissingPointerError, isHandledError } = require("./util/errors"); const slashes = /\//g; const tildes = /~/g; const escapedSlash = /~1/g; @@ -268,7 +268,7 @@ function setValue (pointer, token, value) { } } else { - throw new GenericError(`Error assigning $ref pointer "${pointer.path}". \nCannot set "${token}" of a non-object.`); + throw new JSONParserError(`Error assigning $ref pointer "${pointer.path}". \nCannot set "${token}" of a non-object.`); } return value; } diff --git a/lib/ref.js b/lib/ref.js index 330dc0fb..ee66be80 100644 --- a/lib/ref.js +++ b/lib/ref.js @@ -3,7 +3,7 @@ module.exports = $Ref; const Pointer = require("./pointer"); -const { GenericError, GenericErrorGroup, ParserError, MissingPointerError, ResolverError, isHandledError } = require("./util/errors"); +const { JSONParserError, JSONParserErrorGroup, ParserError, MissingPointerError, ResolverError, isHandledError } = require("./util/errors"); const { safePointerToPath } = require("./util/url"); /** @@ -45,7 +45,7 @@ function $Ref () { /** * List of all errors. Undefined if no errors. - * @type {Array} + * @type {Array} */ this.errors = undefined; } @@ -53,7 +53,7 @@ function $Ref () { /** * Pushes an error to errors array. * - * @param {Array} error - The error to be pushed + * @param {Array} error - The error to be pushed * @returns {void} */ $Ref.prototype.addError = function (err) { diff --git a/lib/util/errors.js b/lib/util/errors.js index 721cf81e..27c4aeaf 100644 --- a/lib/util/errors.js +++ b/lib/util/errors.js @@ -4,7 +4,7 @@ const { Ono } = require("@jsdevtools/ono"); const { stripHash } = require("./url"); -const GenericError = exports.GenericError = class GenericError extends Error { +const JSONParserError = exports.JSONParserError = class JSONParserError extends Error { constructor (message, source) { super(); @@ -16,9 +16,9 @@ const GenericError = exports.GenericError = class GenericError extends Error { } }; -setErrorName(GenericError); +setErrorName(JSONParserError); -const GenericErrorGroup = exports.GenericErrorGroup = class GenericErrorGroup extends Error { +const JSONParserErrorGroup = exports.JSONParserErrorGroup = class JSONParserErrorGroup extends Error { constructor (errors, source) { super(); @@ -54,7 +54,7 @@ const GenericErrorGroup = exports.GenericErrorGroup = class GenericErrorGroup ex } }; -exports.StoplightParserError = class StoplightParserError extends GenericErrorGroup { +exports.StoplightParserError = class StoplightParserError extends JSONParserErrorGroup { constructor (diagnostics, source) { super(diagnostics.filter(StoplightParserError.pickError).map(error => { let parserError = new ParserError(error.message, source); @@ -74,7 +74,7 @@ exports.StoplightParserError = class StoplightParserError extends GenericErrorGr } }; -const ParserError = exports.ParserError = class ParserError extends GenericError { +const ParserError = exports.ParserError = class ParserError extends JSONParserError { constructor (message, source) { super(`Error parsing ${source}: ${message}`, source); } @@ -82,7 +82,7 @@ const ParserError = exports.ParserError = class ParserError extends GenericError setErrorName(ParserError); -const UnmatchedParserError = exports.UnmatchedParserError = class UnmatchedParserError extends GenericError { +const UnmatchedParserError = exports.UnmatchedParserError = class UnmatchedParserError extends JSONParserError { constructor (source) { super(`Could not find parser for "${source}"`, source); } @@ -90,7 +90,7 @@ const UnmatchedParserError = exports.UnmatchedParserError = class UnmatchedParse setErrorName(UnmatchedParserError); -const ResolverError = exports.ResolverError = class ResolverError extends GenericError { +const ResolverError = exports.ResolverError = class ResolverError extends JSONParserError { constructor (ex, source) { super(ex.message || `Error reading file "${source}"`, source); if ("code" in ex) { @@ -101,7 +101,7 @@ const ResolverError = exports.ResolverError = class ResolverError extends Generi setErrorName(ResolverError); -const UnmatchedResolverError = exports.UnmatchedResolverError = class UnmatchedResolverError extends GenericError { +const UnmatchedResolverError = exports.UnmatchedResolverError = class UnmatchedResolverError extends JSONParserError { constructor (source) { super(`Could not find resolver for "${source}"`, source); } @@ -109,7 +109,7 @@ const UnmatchedResolverError = exports.UnmatchedResolverError = class UnmatchedR setErrorName(UnmatchedResolverError); -const MissingPointerError = exports.MissingPointerError = class MissingPointerError extends GenericError { +const MissingPointerError = exports.MissingPointerError = class MissingPointerError extends JSONParserError { constructor (token, path) { super(`Token "${token}" does not exist.`, stripHash(path)); } @@ -117,7 +117,7 @@ const MissingPointerError = exports.MissingPointerError = class MissingPointerEr setErrorName(MissingPointerError); -const InvalidPointerError = exports.InvalidPointerError = class InvalidPointerError extends GenericError { +const InvalidPointerError = exports.InvalidPointerError = class InvalidPointerError extends JSONParserError { constructor (pointer, path) { super(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`, stripHash(path)); } @@ -133,5 +133,5 @@ function setErrorName (err) { } exports.isHandledError = function (err) { - return err instanceof GenericError || err instanceof GenericErrorGroup; + return err instanceof JSONParserError || err instanceof JSONParserErrorGroup; }; From 2cd48e0beb4da2313abefc417833a6e8780fb92b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Mon, 16 Mar 2020 19:07:13 +0800 Subject: [PATCH 11/12] Include error codes --- lib/index.d.ts | 33 ++++++++++++++++++++++++------ lib/util/errors.js | 18 +++++++++++++++- test/specs/invalid/invalid.spec.js | 2 +- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index 775e504a..14c2f1a5 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -411,18 +411,39 @@ declare namespace $RefParser { set($ref: string, value: JSONSchema4Type | JSONSchema6Type): void } + export type JSONParserErrorType = "EUNKNOWN" | "EPARSER" | "EUNMATCHEDPARSER" | "ERESOLVER" | "EUNMATCHEDRESOLVER" | "EMISSINGPOINTER" | "EINVALIDPOINTER"; + export class JSONParserError extends Error { + readonly name: string; readonly message: string; readonly path: Array; readonly source: string; + readonly code: JSONParserErrorType; } - export class ParserError extends JSONParserError {} - export class UnmatchedParserError extends JSONParserError {} + export class ParserError extends JSONParserError { + readonly name = "ParserError"; + readonly code = "EPARSER"; + } + export class UnmatchedParserError extends JSONParserError { + readonly name = "UnmatchedParserError"; + readonly code ="EUNMATCHEDPARSER"; + } export class ResolverError extends JSONParserError { - readonly code?: string; + readonly name = "ResolverError"; + readonly code ="ERESOLVER"; + readonly ioErrorCode?: string; + } + export class UnmatchedResolverError extends JSONParserError { + readonly name = "UnmatchedResolverError"; + readonly code ="EUNMATCHEDRESOLVER"; + } + export class MissingPointerError extends JSONParserError { + readonly name = "MissingPointerError"; + readonly code ="EMISSINGPOINTER"; + } + export class InvalidPointerError extends JSONParserError { + readonly name = "InvalidPointerError"; + readonly code ="EINVALIDPOINTER"; } - export class UnmatchedResolverError extends JSONParserError {} - export class MissingPointerError extends JSONParserError {} - export class InvalidPointerError extends JSONParserError {} } diff --git a/lib/util/errors.js b/lib/util/errors.js index 27c4aeaf..28b2750a 100644 --- a/lib/util/errors.js +++ b/lib/util/errors.js @@ -8,6 +8,7 @@ const JSONParserError = exports.JSONParserError = class JSONParserError extends constructor (message, source) { super(); + this.code = "EUNKNOWN"; this.message = message; this.source = source; this.path = []; @@ -22,6 +23,7 @@ const JSONParserErrorGroup = exports.JSONParserErrorGroup = class JSONParserErro constructor (errors, source) { super(); + this.code = "EUNKNOWN"; this._path = undefined; this._source = source; this.errors = errors; @@ -62,6 +64,7 @@ exports.StoplightParserError = class StoplightParserError extends JSONParserErro return parserError; })); + this.code = "ESTOPLIGHTPARSER"; this.message = `Error parsing ${source}`; } @@ -77,6 +80,8 @@ exports.StoplightParserError = class StoplightParserError extends JSONParserErro const ParserError = exports.ParserError = class ParserError extends JSONParserError { constructor (message, source) { super(`Error parsing ${source}: ${message}`, source); + + this.code = "EPARSER"; } }; @@ -85,6 +90,8 @@ setErrorName(ParserError); const UnmatchedParserError = exports.UnmatchedParserError = class UnmatchedParserError extends JSONParserError { constructor (source) { super(`Could not find parser for "${source}"`, source); + + this.code = "EUNMATCHEDPARSER"; } }; @@ -93,8 +100,11 @@ setErrorName(UnmatchedParserError); const ResolverError = exports.ResolverError = class ResolverError extends JSONParserError { constructor (ex, source) { super(ex.message || `Error reading file "${source}"`, source); + + this.code = "ERESOLVER"; + if ("code" in ex) { - this.code = String(ex.code); + this.ioErrorCode = String(ex.code); } } }; @@ -104,6 +114,8 @@ setErrorName(ResolverError); const UnmatchedResolverError = exports.UnmatchedResolverError = class UnmatchedResolverError extends JSONParserError { constructor (source) { super(`Could not find resolver for "${source}"`, source); + + this.code = "EUNMATCHEDRESOLVER"; } }; @@ -112,6 +124,8 @@ setErrorName(UnmatchedResolverError); const MissingPointerError = exports.MissingPointerError = class MissingPointerError extends JSONParserError { constructor (token, path) { super(`Token "${token}" does not exist.`, stripHash(path)); + + this.code = "EMISSINGPOINTER"; } }; @@ -120,6 +134,8 @@ setErrorName(MissingPointerError); const InvalidPointerError = exports.InvalidPointerError = class InvalidPointerError extends JSONParserError { constructor (pointer, path) { super(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`, stripHash(path)); + + this.code = "EINVALIDPOINTER"; } }; diff --git a/test/specs/invalid/invalid.spec.js b/test/specs/invalid/invalid.spec.js index 22d6e981..2841ab4e 100644 --- a/test/specs/invalid/invalid.spec.js +++ b/test/specs/invalid/invalid.spec.js @@ -20,7 +20,7 @@ describe("Invalid syntax", () => { catch (err) { expect(err).to.be.an.instanceOf(ResolverError); if (host.node) { - expect(err.code).to.equal("ENOENT"); + expect(err.ioErrorCode).to.equal("ENOENT"); expect(err.message).to.contain("Error opening file "); } } From de4c5880a731119bc0f227523924885d99227883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Wed, 25 Mar 2020 21:39:08 +0100 Subject: [PATCH 12/12] Always fail --- docs/ref-parser.md | 8 - lib/index.d.ts | 30 +- lib/index.js | 31 +-- lib/util/errors.js | 81 ++++-- .../invalid-pointers/invalid-pointers.js | 33 ++- test/specs/invalid/invalid.spec.js | 259 +++++++++++------- .../missing-pointers/missing-pointers.spec.js | 33 ++- test/specs/parsers/parsers.spec.js | 49 ++-- test/specs/resolvers/resolvers.spec.js | 43 +-- 9 files changed, 332 insertions(+), 235 deletions(-) diff --git a/docs/ref-parser.md b/docs/ref-parser.md index d69d20d1..0e0d3f1e 100644 --- a/docs/ref-parser.md +++ b/docs/ref-parser.md @@ -6,7 +6,6 @@ This is the default export of JSON Schema $Ref Parser. You can creates instance ##### Properties - [`schema`](#schema) - [`$refs`](#refs) -- [`errors`](#errors) ##### Methods - [`dereference()`](#dereferenceschema-options-callback) @@ -43,13 +42,6 @@ await parser.dereference("my-schema.json"); parser.$refs.paths(); // => ["my-schema.json"] ``` -### `errors` -The `errors` property contains all list of errors that occurred during the bundling/resolving/dereferencing process. -All errors share error properties: -- path - json path to the document property -- message -- source - the uri of document where the faulty document was referenced - ### `dereference(schema, [options], [callback])` diff --git a/lib/index.d.ts b/lib/index.d.ts index 14c2f1a5..e3d5187f 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -25,13 +25,6 @@ declare class $RefParser { */ $refs: $RefParser.$Refs - /** - * List of all errors - * - * See https://github.com/APIDevTools/json-schema-ref-parser/blob/master/docs/ref-parser.md#errors - */ - errors: Array<$RefParser.JSONParserError | $RefParser.InvalidPointerError | $RefParser.ResolverError | $RefParser.ParserError | $RefParser.MissingPointerError | $RefParser.UnmatchedParserError | $RefParser.UnmatchedResolverError>; - /** * Dereferences all `$ref` pointers in the JSON Schema, replacing each reference with its resolved value. This results in a schema object that does not contain any `$ref` pointers. Instead, it's a normal JavaScript object tree that can easily be crawled and used just like any other JavaScript object. This is great for programmatic usage, especially when using tools that don't understand JSON references. * @@ -417,10 +410,31 @@ declare namespace $RefParser { readonly name: string; readonly message: string; readonly path: Array; - readonly source: string; + readonly errors: string; readonly code: JSONParserErrorType; } + export class JSONParserErrorGroup extends Error { + /** + * List of all errors + * + * See https://github.com/APIDevTools/json-schema-ref-parser/blob/master/docs/ref-parser.md#errors + */ + readonly errors: Array<$RefParser.JSONParserError | $RefParser.InvalidPointerError | $RefParser.ResolverError | $RefParser.ParserError | $RefParser.MissingPointerError | $RefParser.UnmatchedParserError | $RefParser.UnmatchedResolverError>; + + /** + * The fields property is a `$RefParser` instance + * + * See https://apitools.dev/json-schema-ref-parser/docs/ref-parser.html + */ + readonly files: $RefParser; + + /** + * User friendly message containing the total amount of errors, as well as the absolute path to the source document + */ + readonly message: string; + } + export class ParserError extends JSONParserError { readonly name = "ParserError"; readonly code = "EPARSER"; diff --git a/lib/index.js b/lib/index.js index 5d49bf90..af2a81ac 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,7 +8,7 @@ const resolveExternal = require("./resolve-external"); const bundle = require("./bundle"); const dereference = require("./dereference"); const url = require("./util/url"); -const { JSONParserError, InvalidPointerError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError } = require("./util/errors"); +const { JSONParserError, InvalidPointerError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError, JSONParserErrorGroup } = require("./util/errors"); const maybe = require("call-me-maybe"); const { ono } = require("@jsdevtools/ono"); @@ -46,25 +46,6 @@ function $RefParser () { this.$refs = new $Refs(); } -/** - * List of all errors - * @type {Array} - */ -Object.defineProperty($RefParser.prototype, "errors", { - get () { - const errors = []; - - for (const $ref of Object.values(this.$refs._$refs)) { - if ($ref.errors) { - errors.push(...$ref.errors); - } - } - - return errors; - }, - enumerable: true, -}); - /** * Parses the given JSON schema. * This method does not resolve any JSON references. @@ -202,6 +183,7 @@ $RefParser.prototype.resolve = async function (path, schema, options, callback) try { await this.parse(args.path, args.schema, args.options); await resolveExternal(me, args.options); + finalize(me); return maybe(args.callback, Promise.resolve(me.$refs)); } catch (err) { @@ -244,6 +226,7 @@ $RefParser.prototype.bundle = async function (path, schema, options, callback) { try { await this.resolve(args.path, args.schema, args.options); bundle(me, args.options); + finalize(me); return maybe(args.callback, Promise.resolve(me.schema)); } catch (err) { @@ -284,9 +267,17 @@ $RefParser.prototype.dereference = async function (path, schema, options, callba try { await this.resolve(args.path, args.schema, args.options); dereference(me, args.options); + finalize(me); return maybe(args.callback, Promise.resolve(me.schema)); } catch (err) { return maybe(args.callback, Promise.reject(err)); } }; + +function finalize (parser) { + const errors = JSONParserErrorGroup.getParserErrors(parser); + if (errors.length > 0) { + throw new JSONParserErrorGroup(parser); + } +} diff --git a/lib/util/errors.js b/lib/util/errors.js index 28b2750a..2354522b 100644 --- a/lib/util/errors.js +++ b/lib/util/errors.js @@ -2,7 +2,7 @@ const { Ono } = require("@jsdevtools/ono"); -const { stripHash } = require("./url"); +const { stripHash, toFileSystemPath } = require("./url"); const JSONParserError = exports.JSONParserError = class JSONParserError extends Error { constructor (message, source) { @@ -20,52 +20,47 @@ const JSONParserError = exports.JSONParserError = class JSONParserError extends setErrorName(JSONParserError); const JSONParserErrorGroup = exports.JSONParserErrorGroup = class JSONParserErrorGroup extends Error { - constructor (errors, source) { + constructor (parser) { super(); - this.code = "EUNKNOWN"; - this._path = undefined; - this._source = source; - this.errors = errors; + this.files = parser; + this.message = `${this.errors.length} error${this.errors.length > 1 ? "s" : ""} occurred while reading '${toFileSystemPath(parser.$refs._root$Ref.path)}'`; Ono.extend(this); } - get source () { - return this._source; - } - - set source (source) { - this._source = source; + static getParserErrors (parser) { + const errors = []; - for (let error of this.errors) { - error.source = source; + for (const $ref of Object.values(parser.$refs._$refs)) { + if ($ref.errors) { + errors.push(...$ref.errors); + } } - } - get path () { - return this.path; + return errors; } - set path (path) { - this._path = path; - - for (let error of this.errors) { - error.path = path; - } + get errors () { + return JSONParserErrorGroup.getParserErrors(this.files); } }; -exports.StoplightParserError = class StoplightParserError extends JSONParserErrorGroup { +setErrorName(JSONParserErrorGroup); + +exports.StoplightParserError = class StoplightParserError extends JSONParserError { constructor (diagnostics, source) { - super(diagnostics.filter(StoplightParserError.pickError).map(error => { + super(`Error parsing ${source}`, source); + + this.code = "ESTOPLIGHTPARSER"; + + this._source = source; + this._path = []; + this.errors = diagnostics.filter(StoplightParserError.pickError).map(error => { let parserError = new ParserError(error.message, source); parserError.message = error.message; return parserError; - })); - - this.code = "ESTOPLIGHTPARSER"; - this.message = `Error parsing ${source}`; + }); } static pickError (diagnostic) { @@ -75,6 +70,34 @@ exports.StoplightParserError = class StoplightParserError extends JSONParserErro static hasErrors (diagnostics) { return diagnostics.some(StoplightParserError.pickError); } + + get source () { + return this._source; + } + + set source (source) { + this._source = source; + + if (this.errors) { + for (let error of this.errors) { + error.source = source; + } + } + } + + get path () { + return this._path; + } + + set path (path) { + this._path = path; + + if (this.errors) { + for (let error of this.errors) { + error.path = path; + } + } + } }; const ParserError = exports.ParserError = class ParserError extends JSONParserError { diff --git a/test/specs/invalid-pointers/invalid-pointers.js b/test/specs/invalid-pointers/invalid-pointers.js index 16e87446..033b681b 100644 --- a/test/specs/invalid-pointers/invalid-pointers.js +++ b/test/specs/invalid-pointers/invalid-pointers.js @@ -7,10 +7,10 @@ const { expect } = chai; const $RefParser = require("../../../lib"); const helper = require("../../utils/helper"); const path = require("../../utils/path"); -const { InvalidPointerError } = require("../../../lib/util/errors"); +const { JSONParserErrorGroup, InvalidPointerError } = require("../../../lib/util/errors"); describe("Schema with invalid pointers", () => { - it("should throw an error for invalid pointer", async () => { + it("should throw an error for an invalid pointer", async () => { try { await $RefParser.dereference(path.rel("specs/invalid-pointers/invalid.json")); helper.shouldNotGetCalled(); @@ -21,17 +21,24 @@ describe("Schema with invalid pointers", () => { } }); - it("should not throw an error for invalid pointer if failFast is false", async () => { + it("should throw a grouped error for an invalid pointer if failFast is false", async () => { const parser = new $RefParser(); - const result = await parser.dereference(path.rel("specs/invalid-pointers/invalid.json"), { failFast: false }); - expect(result).to.deep.equal({ foo: null }); - expect(parser.errors).to.containSubset([ - { - name: InvalidPointerError.name, - message: "Invalid $ref pointer \"f\". Pointers must begin with \"#/\"", - path: ["foo"], - source: path.abs("specs/invalid-pointers/invalid.json"), - } - ]); + try { + await parser.dereference(path.rel("specs/invalid-pointers/invalid.json"), { failFast: false }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.files).to.equal(parser); + expect(err.message).to.equal(`1 error occurred while reading '${path.abs("specs/invalid-pointers/invalid.json")}'`); + expect(err.errors).to.containSubset([ + { + name: InvalidPointerError.name, + message: "Invalid $ref pointer \"f\". Pointers must begin with \"#/\"", + path: ["foo"], + source: path.abs("specs/invalid-pointers/invalid.json"), + } + ]); + } }); }); diff --git a/test/specs/invalid/invalid.spec.js b/test/specs/invalid/invalid.spec.js index 2841ab4e..3bb89a7e 100644 --- a/test/specs/invalid/invalid.spec.js +++ b/test/specs/invalid/invalid.spec.js @@ -8,7 +8,7 @@ const { expect } = chai; const $RefParser = require("../../../lib"); const helper = require("../../utils/helper"); const path = require("../../utils/path"); -const { StoplightParserError, ParserError, ResolverError } = require("../../../lib/util/errors"); +const { JSONParserErrorGroup, StoplightParserError, ParserError, ResolverError } = require("../../../lib/util/errors"); describe("Invalid syntax", () => { describe("in main file", () => { @@ -74,71 +74,99 @@ describe("Invalid syntax", () => { }); describe("when failFast is false", () => { - it("should not throw an error for an invalid file path", async () => { + it("should throw a grouped error for an invalid file path", async () => { const parser = new $RefParser(); - const result = await parser.dereference("this file does not exist", { failFast: false }); - expect(result).to.be.null; - expect(parser.errors.length).to.equal(1); - expect(parser.errors).to.containSubset([ - { - name: ResolverError.name, - message: expectedValue => expectedValue.startsWith("Error opening file") || expectedValue.endsWith("HTTP ERROR 404"), - path: [], - source: expectedValue => expectedValue.endsWith("this file does not exist") || expectedValue.startsWith("http://localhost"), - } - ]); + try { + await parser.dereference("this file does not exist", { failFast: false }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.files).to.equal(parser); + expect(err.message).to.have.string("1 error occurred while reading '"); + expect(err.message).to.have.string("this file does not exist'"); + expect(err.errors.length).to.equal(1); + expect(err.errors).to.containSubset([ + { + name: ResolverError.name, + message: expectedValue => expectedValue.startsWith("Error opening file") || expectedValue.endsWith("HTTP ERROR 404"), + path: [], + source: expectedValue => expectedValue.endsWith("this file does not exist") || expectedValue.startsWith("http://localhost"), + } + ]); + } }); - it("should not throw an error for an invalid YAML file", async () => { + it("should throw a grouped error for an invalid YAML file", async () => { const parser = new $RefParser(); - const result = await parser.dereference(path.rel("specs/invalid/invalid.yaml"), { failFast: false }); - expect(result).to.be.null; - expect(parser.errors.length).to.equal(1); - expect(parser.errors).to.containSubset([ - { - name: ParserError.name, - message: "incomplete explicit mapping pair; a key node is missed", - path: [], - source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.yaml"), - }, - ]); + try { + await parser.dereference(path.rel("specs/invalid/invalid.yaml"), { failFast: false }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.files).to.equal(parser); + expect(err.message).to.equal(`1 error occurred while reading '${path.abs("specs/invalid/invalid.yaml")}'`); + expect(err.errors.length).to.equal(1); + expect(err.errors).to.containSubset([ + { + name: ParserError.name, + message: "incomplete explicit mapping pair; a key node is missed", + path: [], + source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.yaml"), + }, + ]); + } }); - it("should not throw an error for an invalid JSON file", async () => { + it("should throw a grouped error for an invalid JSON file", async () => { const parser = new $RefParser(); - const result = await parser.dereference(path.rel("specs/invalid/invalid.json"), { failFast: false }); - expect(result).to.be.null; - expect(parser.errors.length).to.equal(1); - expect(parser.errors).to.containSubset([ - { - name: ParserError.name, - message: "unexpected end of the stream within a flow collection", - path: [], - source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.json"), - } - ]); + try { + await parser.dereference(path.rel("specs/invalid/invalid.json"), { failFast: false }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.files).to.equal(parser); + expect(err.message).to.equal(`1 error occurred while reading '${path.abs("specs/invalid/invalid.json")}'`); + expect(err.errors.length).to.equal(1); + expect(err.errors).to.containSubset([ + { + name: ParserError.name, + message: "unexpected end of the stream within a flow collection", + path: [], + source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.json"), + } + ]); + } }); - it("should not throw an error for an invalid JSON file with YAML disabled", async () => { + it("should throw a grouped error for an invalid JSON file with YAML disabled", async () => { const parser = new $RefParser(); - const result = await parser.dereference(path.rel("specs/invalid/invalid.json"), { failFast: false, parse: { yaml: false }}); - expect(result).to.be.null; - expect(parser.errors.length).to.equal(1); - expect(parser.errors).to.containSubset([ - { - name: ParserError.name, - message: "CloseBraceExpected", - path: [], - source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.json"), - } - ]); + try { + await parser.dereference(path.rel("specs/invalid/invalid.json"), { failFast: false, parse: { yaml: false }}); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.files).to.equal(parser); + expect(err.message).to.equal(`1 error occurred while reading '${path.abs("specs/invalid/invalid.json")}'`); + expect(err.errors.length).to.equal(1); + expect(err.errors).to.containSubset([ + { + name: ParserError.name, + message: "CloseBraceExpected", + path: [], + source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.json"), + } + ]); + } }); it("should not throw an error for an invalid YAML file with JSON and YAML disabled", async () => { const parser = new $RefParser(); const result = await parser.dereference(path.rel("specs/invalid/invalid.yaml"), { failFast: false, parse: { yaml: false, json: false }}); expect(result).to.be.null; - expect(parser.errors).to.deep.equal([]); }); }); }); @@ -182,7 +210,7 @@ describe("Invalid syntax", () => { } }); - it("should NOT throw an error for an invalid YAML file with JSON and YAML disabled", async () => { + it("should throw a grouped error for an invalid YAML file with JSON and YAML disabled", async () => { const schema = await $RefParser .dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { parse: { yaml: false, json: false } @@ -195,71 +223,94 @@ describe("Invalid syntax", () => { }); describe("when failFast is false", () => { - it("should not throw an error for an invalid file path", async () => { - const parser = new $RefParser(); - const result = await parser.dereference({ foo: { $ref: "this file does not exist" }}, { failFast: false }); - expect(result).to.deep.equal({ foo: null }); - expect(parser.errors.length).to.equal(1); - expect(parser.errors).to.containSubset([ - { - name: ResolverError.name, - message: expectedValue => expectedValue.startsWith("Error opening file") || expectedValue.endsWith("HTTP ERROR 404"), - path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), - } - ]); + it("should throw a grouped error for an invalid file path", async () => { + try { + const parser = new $RefParser(); + await parser.dereference({ foo: { $ref: "this file does not exist" }}, { failFast: false }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.files.$refs._root$Ref.value).to.deep.equal({ foo: null }); + expect(err.errors.length).to.equal(1); + expect(err.errors).to.containSubset([ + { + name: ResolverError.name, + message: expectedValue => expectedValue.startsWith("Error opening file") || expectedValue.endsWith("HTTP ERROR 404"), + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), + } + ]); + } }); - it("should not throw an error for an invalid YAML file", async () => { - const parser = new $RefParser(); - const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { failFast: false }); - expect(result).to.deep.equal({ foo: null }); - expect(parser.errors.length).to.equal(1); - expect(parser.errors).to.containSubset([ - { - name: ParserError.name, - message: "incomplete explicit mapping pair; a key node is missed", - path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), - }, - ]); + it("should throw a grouped error for an invalid YAML file", async () => { + try { + const parser = new $RefParser(); + await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { failFast: false }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.files.$refs._root$Ref.value).to.deep.equal({ foo: null }); + expect(err.errors.length).to.equal(1); + expect(err.errors).to.containSubset([ + { + name: ParserError.name, + message: "incomplete explicit mapping pair; a key node is missed", + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), + }, + ]); + } }); - it("should not throw an error for an invalid JSON file", async () => { - const parser = new $RefParser(); - const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false }); - expect(result).to.deep.equal({ foo: null }); - expect(parser.errors.length).to.equal(1); - expect(parser.errors).to.containSubset([ - { - name: ParserError.name, - message: "unexpected end of the stream within a flow collection", - path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), - } - ]); + it("should throw a grouped error for an invalid JSON file", async () => { + try { + const parser = new $RefParser(); + await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.files.$refs._root$Ref.value).to.deep.equal({ foo: null }); + expect(err.errors.length).to.equal(1); + expect(err.errors).to.containSubset([ + { + name: ParserError.name, + message: "unexpected end of the stream within a flow collection", + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), + } + ]); + } }); - it("should not throw an error for an invalid JSON file with YAML disabled", async () => { - const parser = new $RefParser(); - const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false, parse: { yaml: false }}); - expect(result).to.deep.equal({ foo: null }); - expect(parser.errors.length).to.equal(1); - expect(parser.errors).to.containSubset([ - { - name: ParserError.name, - message: "CloseBraceExpected", - path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), - } - ]); + it("should throw a grouped error for an invalid JSON file with YAML disabled", async () => { + try { + const parser = new $RefParser(); + await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false, parse: { yaml: false }}); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.files.$refs._root$Ref.value).to.deep.equal({ foo: null }); + expect(err.errors.length).to.equal(1); + expect(err.errors).to.containSubset([ + { + name: ParserError.name, + message: "CloseBraceExpected", + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), + } + ]); + } }); it("should not throw an error for an invalid YAML file with JSON and YAML disabled", async () => { const parser = new $RefParser(); const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { failFast: false, parse: { yaml: false, json: false }}); expect(result).to.deep.equal({ foo: ":\n" }); - expect(parser.errors).to.deep.equal([]); }); }); }); diff --git a/test/specs/missing-pointers/missing-pointers.spec.js b/test/specs/missing-pointers/missing-pointers.spec.js index ce705445..d6a04718 100644 --- a/test/specs/missing-pointers/missing-pointers.spec.js +++ b/test/specs/missing-pointers/missing-pointers.spec.js @@ -5,8 +5,9 @@ const chaiSubset = require("chai-subset"); chai.use(chaiSubset); const { expect } = chai; const $RefParser = require("../../../lib"); +const { JSONParserErrorGroup, MissingPointerError } = require("../../../lib/util/errors"); const helper = require("../../utils/helper"); -const { MissingPointerError } = require("../../../lib/util/errors"); +const path = require("../../utils/path"); describe("Schema with missing pointers", () => { it("should throw an error for missing pointer", async () => { @@ -20,17 +21,25 @@ describe("Schema with missing pointers", () => { } }); - it("should not throw an error for missing pointer if failFast is false", async () => { + it("should throw a grouped error for missing pointer if failFast is false", async () => { const parser = new $RefParser(); - const result = await parser.dereference({ foo: { $ref: "#/baz" }}, { failFast: false }); - expect(result).to.deep.equal({ foo: null }); - expect(parser.errors).to.containSubset([ - { - name: MissingPointerError.name, - message: "Token \"baz\" does not exist.", - path: ["foo"], - source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), - } - ]); + try { + await parser.dereference({ foo: { $ref: "#/baz" }}, { failFast: false }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.files).to.equal(parser); + expect(err.files.$refs._root$Ref.value).to.deep.equal({ foo: null }); + expect(err.message).to.have.string("1 error occurred while reading '"); + expect(err.errors).to.containSubset([ + { + name: MissingPointerError.name, + message: "Token \"baz\" does not exist.", + path: ["foo"], + source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), + } + ]); + } }); }); diff --git a/test/specs/parsers/parsers.spec.js b/test/specs/parsers/parsers.spec.js index 2dcaf5a0..24758868 100644 --- a/test/specs/parsers/parsers.spec.js +++ b/test/specs/parsers/parsers.spec.js @@ -9,7 +9,7 @@ const helper = require("../../utils/helper"); const path = require("../../utils/path"); const parsedSchema = require("./parsed"); const dereferencedSchema = require("./dereferenced"); -const { StoplightParserError, ParserError, UnmatchedParserError } = require("../../../lib/util/errors"); +const { JSONParserErrorGroup, StoplightParserError, ParserError, UnmatchedParserError } = require("../../../lib/util/errors"); describe("References to non-JSON files", () => { it("should parse successfully", async () => { @@ -206,26 +206,31 @@ describe("References to non-JSON files", () => { } }); - it("should let no parser to be matched if fastFail is false", async () => { - const parser = new $RefParser(); - await parser.dereference(path.rel("specs/parsers/parsers.yaml"), { - parse: { - yaml: false, - json: false, - text: false, - binary: false, - }, - failFast: false, - }); - - expect(parser.errors.length).to.equal(1); - expect(parser.errors).to.containSubset([ - { - name: UnmatchedParserError.name, - message: expectedValue => expectedValue.startsWith("Could not find parser for"), - path: [], - source: expectedValue => expectedValue.endsWith("specs/parsers/parsers.yaml") || expectedValue.startsWith("http://localhost"), - }, - ]); + it("should throw a grouped error if no parser can be matched and fastFail is false", async () => { + try { + const parser = new $RefParser(); + await parser.dereference(path.rel("specs/parsers/parsers.yaml"), { + parse: { + yaml: false, + json: false, + text: false, + binary: false, + }, + failFast: false, + }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.errors.length).to.equal(1); + expect(err.errors).to.containSubset([ + { + name: UnmatchedParserError.name, + message: expectedValue => expectedValue.startsWith("Could not find parser for"), + path: [], + source: expectedValue => expectedValue.endsWith("specs/parsers/parsers.yaml") || expectedValue.startsWith("http://localhost"), + }, + ]); + } }); }); diff --git a/test/specs/resolvers/resolvers.spec.js b/test/specs/resolvers/resolvers.spec.js index 7f0db463..ccb22403 100644 --- a/test/specs/resolvers/resolvers.spec.js +++ b/test/specs/resolvers/resolvers.spec.js @@ -9,7 +9,7 @@ const helper = require("../../utils/helper"); const path = require("../../utils/path"); const parsedSchema = require("./parsed"); const dereferencedSchema = require("./dereferenced"); -const { ResolverError, UnmatchedResolverError } = require("../../../lib/util/errors"); +const { ResolverError, UnmatchedResolverError, JSONParserErrorGroup } = require("../../../lib/util/errors"); describe("options.resolve", () => { it('should not resolve external links if "resolve.external" is disabled', async () => { @@ -137,24 +137,29 @@ describe("options.resolve", () => { } }); - it("should let no resolver to be matched if fastFail is false", async () => { + it("should throw a grouped error if no resolver can be matched and fastFail is false", async () => { const parser = new $RefParser(); - await parser.dereference(path.abs("specs/resolvers/resolvers.yaml"), { - resolve: { - file: false, - http: false, - }, - failFast: false, - }); - - expect(parser.errors.length).to.equal(1); - expect(parser.errors).to.containSubset([ - { - name: UnmatchedResolverError.name, - message: expectedValue => expectedValue.startsWith("Could not find resolver for"), - path: [], - source: expectedValue => expectedValue.endsWith("specs/resolvers/resolvers.yaml"), - }, - ]); + try { + await parser.dereference(path.abs("specs/resolvers/resolvers.yaml"), { + resolve: { + file: false, + http: false, + }, + failFast: false, + }); + helper.shouldNotGetCalled(); + } + catch (err) { + expect(err).to.be.instanceof(JSONParserErrorGroup); + expect(err.errors.length).to.equal(1); + expect(err.errors).to.containSubset([ + { + name: UnmatchedResolverError.name, + message: expectedValue => expectedValue.startsWith("Could not find resolver for"), + path: [], + source: expectedValue => expectedValue.endsWith("specs/resolvers/resolvers.yaml"), + }, + ]); + } }); });