diff --git a/lib/index.d.ts b/lib/index.d.ts index 8a0f1436..40745e31 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -410,6 +410,7 @@ declare namespace $RefParser { export class JSONParserError extends Error { readonly name: string; readonly message: string; + readonly source: string; readonly path: Array; readonly errors: string; readonly code: JSONParserErrorType; diff --git a/lib/parse.js b/lib/parse.js index b5d9ab90..8cfecd51 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -3,7 +3,7 @@ const { ono } = require("@jsdevtools/ono"); const url = require("./util/url"); const plugins = require("./util/plugins"); -const { StoplightParserError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError } = require("./util/errors"); +const { ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError } = require("./util/errors"); module.exports = parse; @@ -140,7 +140,7 @@ function parseFile (file, options, $refs) { else if (!err || !("error" in err)) { reject(ono.syntax(`Unable to parse ${file.url}`)); } - else if (err.error instanceof ParserError || err.error instanceof StoplightParserError) { + else if (err.error instanceof ParserError) { reject(err.error); } else { diff --git a/lib/parsers/json.js b/lib/parsers/json.js index dac373d6..1b6925e6 100644 --- a/lib/parsers/json.js +++ b/lib/parsers/json.js @@ -1,8 +1,6 @@ "use strict"; -const { parseWithPointers } = require("@stoplight/json"); -const { StoplightParserError } = require("../util/errors"); - +const { ParserError } = require("../util/errors"); module.exports = { /** @@ -46,18 +44,15 @@ module.exports = { if (typeof data === "string") { if (data.trim().length === 0) { - return; + return; // This mirrors the YAML behavior } else { - let result = parseWithPointers(data, { - ignoreDuplicateKeys: false, - }); - - if (StoplightParserError.hasErrors(result.diagnostics)) { - throw new StoplightParserError(result.diagnostics, file.url); + try { + return JSON.parse(data); + } + catch (e) { + throw new ParserError(e.message, file.url); } - - return result.data; } } else { diff --git a/lib/parsers/yaml.js b/lib/parsers/yaml.js index 47b0e3e6..67efc038 100644 --- a/lib/parsers/yaml.js +++ b/lib/parsers/yaml.js @@ -1,7 +1,7 @@ "use strict"; -const { parseWithPointers } = require("@stoplight/yaml"); -const { StoplightParserError } = require("../util/errors"); +const YAML = require("../util/yaml"); +const { ParserError } = require("../util/errors"); module.exports = { /** @@ -44,17 +44,12 @@ module.exports = { } if (typeof data === "string") { - let result = parseWithPointers(data, { - json: true, - mergeKeys: true, - ignoreDuplicateKeys: false, - }); - - if (StoplightParserError.hasErrors(result.diagnostics)) { - throw new StoplightParserError(result.diagnostics, file.url); + try { + return YAML.parse(data); + } + catch (e) { + throw new ParserError(e.message, file.url); } - - return result.data; } else { // data is already a JavaScript value (object, array, number, null, NaN, etc.) diff --git a/lib/util/errors.js b/lib/util/errors.js index 2354522b..b2371593 100644 --- a/lib/util/errors.js +++ b/lib/util/errors.js @@ -48,58 +48,6 @@ const JSONParserErrorGroup = exports.JSONParserErrorGroup = class JSONParserErro setErrorName(JSONParserErrorGroup); -exports.StoplightParserError = class StoplightParserError extends JSONParserError { - constructor (diagnostics, source) { - 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; - }); - } - - static pickError (diagnostic) { - return diagnostic.severity === 0; - } - - 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 { constructor (message, source) { super(`Error parsing ${source}: ${message}`, source); diff --git a/lib/util/url.js b/lib/util/url.js index 4d1b380d..39b39482 100644 --- a/lib/util/url.js +++ b/lib/util/url.js @@ -1,11 +1,11 @@ "use strict"; -const { pointerToPath } = require("@stoplight/json"); - let isWindows = /^win/.test(process.platform), forwardSlashPattern = /\//g, protocolPattern = /^(\w{2,}):\/\//i, - url = module.exports; + url = module.exports, + jsonPointerSlash = /~1/g, + jsonPointerTilde = /~0/g; // RegExp patterns to URL-encode special characters in local filesystem paths let urlEncodePatterns = [ @@ -237,16 +237,21 @@ exports.toFileSystemPath = function toFileSystemPath (path, keepFileProtocol) { /** * 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) { + if (pointer.length <= 1 || pointer[0] !== "#" || pointer[1] !== "/") { return []; } + + return pointer + .slice(2) + .split("/") + .map((value) => { + return decodeURIComponent(value) + .replace(jsonPointerSlash, "/") + .replace(jsonPointerTilde, "~"); + }); }; diff --git a/lib/util/yaml.js b/lib/util/yaml.js new file mode 100644 index 00000000..bdca34f4 --- /dev/null +++ b/lib/util/yaml.js @@ -0,0 +1,34 @@ +/* eslint lines-around-comment: [2, {beforeBlockComment: false}] */ +"use strict"; + +const yaml = require("js-yaml"); +const { ono } = require("@jsdevtools/ono"); + +/** + * Simple YAML parsing functions, similar to {@link JSON.parse} and {@link JSON.stringify} + */ +module.exports = { + /** + * Parses a YAML string and returns the value. + * + * @param {string} text - The YAML string to be parsed + * @param {function} [reviver] - Not currently supported. Provided for consistency with {@link JSON.parse} + * @returns {*} + */ + parse (text, reviver) { + return yaml.safeLoad(text); + }, + + /** + * Converts a JavaScript value to a YAML string. + * + * @param {*} value - The value to convert to YAML + * @param {function|array} replacer - Not currently supported. Provided for consistency with {@link JSON.stringify} + * @param {string|number} space - The number of spaces to use for indentation, or a string containing the number of spaces. + * @returns {string} + */ + stringify (value, replacer, space) { + let indent = (typeof space === "string" ? space.length : space) || 2; + return yaml.safeDump(value, { indent }); + } +}; diff --git a/package-lock.json b/package-lock.json index 04c60b28..a3be7e54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1270,48 +1270,6 @@ "integrity": "sha512-D5H5RjqqE+YxI2oeTgSRuIjdy/hli90H5mMd81bBrYlOfB/f4TBsKMoaWfzI5E4bmFzLfQJuvvepTaWrxVfBug==", "dev": true }, - "@stoplight/json": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.7.0.tgz", - "integrity": "sha512-qiEe/kDi1N7Gq2CJ8g7RdKs+h5NY3z2k5LVK5G+NijXKez0m+W89/N4z45aDwl7RmihGxCOv0OuFye8Fa2AGLA==", - "requires": { - "@stoplight/ordered-object-literal": "^1.0.0", - "@stoplight/types": "^11.4.0", - "jsonc-parser": "~2.2.0", - "lodash": "^4.17.15", - "safe-stable-stringify": "^1.1" - } - }, - "@stoplight/ordered-object-literal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.0.tgz", - "integrity": "sha512-nnyPqCeWUqBY2hmeF9psftty35anYytP5Gz1iI7qL/hiSb+hkFAV6k6FuLYZZShaQdmHAPsanG3f6rvhA8sOzg==" - }, - "@stoplight/types": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-11.6.0.tgz", - "integrity": "sha512-J2wOl6FlN4IeY99MZTbgLVbIqrE9eVcHIvWmSEFzxfnbHCh4reXcGkvxlQ7I/pTKScd5/F/HJKSYnNXRjCnM2A==", - "requires": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - } - }, - "@stoplight/yaml": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-3.8.0.tgz", - "integrity": "sha512-8aqwCRJd4ZGQM+6veAYU/TcELtqd/wiPjAEIPAfHJLiZr5wZice2fRkefT3/Ntr6zLlLIhQcNfXTUlCrfdQ8xQ==", - "requires": { - "@stoplight/ordered-object-literal": "^1.0.0", - "@stoplight/types": "^11.1.1", - "@stoplight/yaml-ast-parser": "0.0.45", - "lodash": "^4.17.15" - } - }, - "@stoplight/yaml-ast-parser": { - "version": "0.0.45", - "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.45.tgz", - "integrity": "sha512-0MTEvgp3XMdeMUSTCGiNECuC+YlLbzytDEIOJVDHrrmzVZpIR3gGnHI6mmPI4P7saPxUiHxFF2uuoTuCNlKjrw==" - }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -1321,7 +1279,8 @@ "@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==" + "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "dev": true }, "@types/node": { "version": "13.13.0", @@ -1719,7 +1678,6 @@ "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" } @@ -3855,8 +3813,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.1.0", @@ -6126,7 +6083,6 @@ "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" @@ -6165,11 +6121,6 @@ "minimist": "^1.2.5" } }, - "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", @@ -6720,7 +6671,8 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true }, "lodash.camelcase": { "version": "4.3.0", @@ -9187,11 +9139,6 @@ "ret": "~0.1.10" } }, - "safe-stable-stringify": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", - "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==" - }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9820,8 +9767,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "ssri": { "version": "6.0.1", @@ -10689,11 +10635,6 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, - "utility-types": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", - "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" - }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/package.json b/package.json index d6756b84..a07697a5 100644 --- a/package.json +++ b/package.json @@ -71,8 +71,7 @@ }, "dependencies": { "@jsdevtools/ono": "^7.1.2", - "@stoplight/json": "^3.7.0", - "@stoplight/yaml": "^3.8.0", - "call-me-maybe": "^1.0.1" + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" } } diff --git a/test/specs/callbacks.spec.js b/test/specs/callbacks.spec.js index 59920efa..4ea7513a 100644 --- a/test/specs/callbacks.spec.js +++ b/test/specs/callbacks.spec.js @@ -4,7 +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"); +const { ParserError } = require("../../lib/util/errors"); describe("Callback & Promise syntax", () => { for (let method of ["parse", "resolve", "dereference", "bundle"]) { @@ -43,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(StoplightParserError); + expect(err).to.be.an.instanceOf(ParserError); expect(result).to.be.undefined; done(); } @@ -76,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(StoplightParserError); + expect(err).to.be.an.instanceOf(ParserError); }); }; } diff --git a/test/specs/invalid/invalid.spec.js b/test/specs/invalid/invalid.spec.js index 32b6467b..b815e1d5 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 { JSONParserErrorGroup, StoplightParserError, ParserError, ResolverError } = require("../../../lib/util/errors"); +const { JSONParserErrorGroup, ParserError, ResolverError } = require("../../../lib/util/errors"); describe("Invalid syntax", () => { describe("in main file", () => { @@ -32,7 +32,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(StoplightParserError); + expect(err).to.be.an.instanceOf(ParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.yaml"); } @@ -44,7 +44,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(StoplightParserError); + expect(err).to.be.an.instanceOf(ParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.json"); } @@ -56,7 +56,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(StoplightParserError); + expect(err).to.be.an.instanceOf(ParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.json"); } @@ -111,7 +111,7 @@ describe("Invalid syntax", () => { expect(err.errors).to.containSubset([ { name: ParserError.name, - message: "incomplete explicit mapping pair; a key node is missed", + message: `Error parsing ${path.abs("specs/invalid/invalid.yaml")}: incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line at line 1, column 1:\n :\n ^`, path: [], source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.yaml"), }, @@ -133,7 +133,7 @@ describe("Invalid syntax", () => { expect(err.errors).to.containSubset([ { name: ParserError.name, - message: "unexpected end of the stream within a flow collection", + message: `Error parsing ${path.abs("specs/invalid/invalid.json")}: unexpected end of the stream within a flow collection at line 2, column 1:\n \n ^`, path: [], source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.json"), } @@ -155,7 +155,10 @@ describe("Invalid syntax", () => { expect(err.errors).to.containSubset([ { name: ParserError.name, - message: "CloseBraceExpected", + message: expectedValue => ( + expectedValue === `Error parsing ${path.abs("specs/invalid/invalid.json")}: Unexpected end of JSON input` || + expectedValue === `Error parsing ${path.abs("specs/invalid/invalid.json")}: JSON.parse: end of data while reading object contents at line 2 column 1 of the JSON data` // this is thrown on Firefox + ), path: [], source: expectedValue => expectedValue.endsWith("test/specs/invalid/invalid.json"), } @@ -178,7 +181,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(StoplightParserError); + expect(err).to.be.an.instanceOf(ParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.yaml"); } @@ -190,7 +193,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(StoplightParserError); + expect(err).to.be.an.instanceOf(ParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.json"); } @@ -204,7 +207,7 @@ describe("Invalid syntax", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(StoplightParserError); + expect(err).to.be.an.instanceOf(ParserError); expect(err.message).to.contain("Error parsing "); expect(err.message).to.contain("invalid/invalid.json"); } @@ -257,7 +260,7 @@ describe("Invalid syntax", () => { expect(err.errors).to.containSubset([ { name: ParserError.name, - message: "incomplete explicit mapping pair; a key node is missed", + message: `Error parsing ${path.abs("specs/invalid/invalid.yaml")}: incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line at line 1, column 1:\n :\n ^`, path: ["foo"], source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), }, @@ -278,7 +281,7 @@ describe("Invalid syntax", () => { expect(err.errors).to.containSubset([ { name: ParserError.name, - message: "unexpected end of the stream within a flow collection", + message: `Error parsing ${path.abs("specs/invalid/invalid.json")}: unexpected end of the stream within a flow collection at line 2, column 1:\n \n ^`, path: ["foo"], source: expectedValue => expectedValue.endsWith("/test/") || expectedValue.startsWith("http://localhost"), } @@ -299,7 +302,10 @@ describe("Invalid syntax", () => { expect(err.errors).to.containSubset([ { name: ParserError.name, - message: "CloseBraceExpected", + message: expectedValue => ( + expectedValue === `Error parsing ${path.abs("specs/invalid/invalid.json")}: Unexpected end of JSON input` || + expectedValue === `Error parsing ${path.abs("specs/invalid/invalid.json")}: JSON.parse: end of data while reading object contents at line 2 column 1 of the JSON data` // this is thrown on Firefox + ), 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 ec911f01..ece4a394 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 { JSONParserErrorGroup, StoplightParserError, ParserError, UnmatchedParserError } = require("../../../lib/util/errors"); +const { JSONParserErrorGroup, ParserError, UnmatchedParserError } = require("../../../lib/util/errors"); describe("References to non-JSON files", () => { it("should parse successfully", async () => { @@ -92,7 +92,7 @@ describe("References to non-JSON files", () => { helper.shouldNotGetCalled(); } catch (err) { - expect(err).to.be.an.instanceOf(StoplightParserError); + expect(err).to.be.an.instanceOf(ParserError); expect(err.message).to.contain("Error parsing "); } });