Skip to content

Commit c0cc8bf

Browse files
committed
Allow parsing errors
1 parent 948175e commit c0cc8bf

15 files changed

Lines changed: 337 additions & 113 deletions

File tree

lib/index.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,17 @@ $RefParser.prototype.parse = async function (path, schema, options, callback) {
135135
try {
136136
let result = await promise;
137137

138-
if (!result || typeof result !== "object" || Buffer.isBuffer(result)) {
139-
throw ono.syntax(`"${me.$refs._root$Ref.path || result}" is not a valid JSON Schema`);
140-
}
141-
else {
138+
if (result !== null && typeof result === "object" && !Buffer.isBuffer(result)) {
142139
me.schema = result;
143140
return maybe(args.callback, Promise.resolve(me.schema));
144141
}
142+
else if (!args.options.failFast) {
143+
me.schema = null; // it's already set to null at line 79, but let's set it again for the sake of readability
144+
return maybe(args.callback, Promise.resolve(me.schema));
145+
}
146+
else {
147+
throw ono.syntax(`"${me.$refs._root$Ref.path || result}" is not a valid JSON Schema`);
148+
}
145149
}
146150
catch (err) {
147151
if (args.options.failFast || !isHandledError(err)) {

lib/parse.js

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ module.exports = parse;
1818
* The promise resolves with the parsed file contents, NOT the raw (Buffer) contents.
1919
*/
2020
async function parse (path, $refs, options) {
21-
try {
22-
// Remove the URL fragment, if any
23-
path = url.stripHash(path);
21+
// Remove the URL fragment, if any
22+
path = url.stripHash(path);
2423

25-
// Add a new $Ref for this file, even though we don't have the value yet.
26-
// This ensures that we don't simultaneously read & parse the same file multiple times
27-
let $ref = $refs._add(path);
24+
// Add a new $Ref for this file, even though we don't have the value yet.
25+
// This ensures that we don't simultaneously read & parse the same file multiple times
26+
let $ref = $refs._add(path);
2827

29-
// This "file object" will be passed to all resolvers and parsers.
30-
let file = {
31-
url: path,
32-
extension: url.getExtension(path),
33-
};
28+
// This "file object" will be passed to all resolvers and parsers.
29+
let file = {
30+
url: path,
31+
extension: url.getExtension(path),
32+
};
3433

35-
// Read the file and then parse the data
34+
// Read the file and then parse the data
35+
try {
3636
const resolver = await readFile(file, options, $refs);
3737
$ref.pathType = resolver.plugin.name;
3838
file.data = resolver.result;
@@ -42,8 +42,14 @@ async function parse (path, $refs, options) {
4242

4343
return parser.result;
4444
}
45-
catch (e) {
46-
return Promise.reject(e);
45+
catch (ex) {
46+
if (!("error" in ex)) {
47+
throw ex;
48+
}
49+
else {
50+
$ref.value = ex.error;
51+
throw ex.error;
52+
}
4753
}
4854
}
4955

@@ -113,7 +119,7 @@ function parseFile (file, options, $refs) {
113119
.then(onParsed, onError);
114120

115121
function onParsed (parser) {
116-
if (!parser.plugin.allowEmpty && isEmpty(parser.result)) {
122+
if ((!options.failFast || !parser.plugin.allowEmpty) && isEmpty(parser.result)) {
117123
reject(ono.syntax(`Error parsing "${file.url}" as ${parser.plugin.name}. \nParsed value is empty`));
118124
}
119125
else {
@@ -122,12 +128,15 @@ function parseFile (file, options, $refs) {
122128
}
123129

124130
function onError (err) {
125-
if (err) {
126-
err = err instanceof Error ? err : new Error(err);
127-
reject(ono.syntax(err, `Error parsing ${file.url}`));
131+
if (!err || !("error" in err)) {
132+
reject(ono.syntax(`Unable to parse ${file.url}`));
133+
}
134+
else if (err.error instanceof ParserError || err.error instanceof StoplightParserError) {
135+
reject(err);
128136
}
129137
else {
130-
reject(ono.syntax(`Unable to parse ${file.url}`));
138+
err.error = new ParserError(err.error.message, file.url);
139+
reject(err);
131140
}
132141
}
133142
}));

lib/parsers/binary.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ module.exports = {
4141
* @param {string} file.url - The full URL of the referenced file
4242
* @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
4343
* @param {*} file.data - The file contents. This will be whatever data type was returned by the resolver
44-
* @returns {Promise<Buffer>}
44+
* @returns {Buffer}
4545
*/
4646
parse (file) {
4747
if (Buffer.isBuffer(file.data)) {

lib/parsers/json.js

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
"use strict";
22

3+
const { parseWithPointers } = require("@stoplight/json");
4+
const { StoplightParserError } = require("../util/errors");
5+
6+
37
module.exports = {
48
/**
59
* The order that this parser will run, in relation to other parsers.
@@ -21,7 +25,7 @@ module.exports = {
2125
* Parsers that don't match will be skipped, UNLESS none of the parsers match, in which case
2226
* every parser will be tried.
2327
*
24-
* @type {RegExp|string[]|function}
28+
* @type {RegExp|string|string[]|function}
2529
*/
2630
canParse: ".json",
2731

@@ -34,25 +38,31 @@ module.exports = {
3438
* @param {*} file.data - The file contents. This will be whatever data type was returned by the resolver
3539
* @returns {Promise}
3640
*/
37-
parse (file) {
38-
return new Promise(((resolve, reject) => {
39-
let data = file.data;
40-
if (Buffer.isBuffer(data)) {
41-
data = data.toString();
42-
}
41+
async parse (file) {
42+
let data = file.data;
43+
if (Buffer.isBuffer(data)) {
44+
data = data.toString();
45+
}
4346

44-
if (typeof data === "string") {
45-
if (data.trim().length === 0) {
46-
resolve(undefined); // This mirrors the YAML behavior
47-
}
48-
else {
49-
resolve(JSON.parse(data));
50-
}
47+
if (typeof data === "string") {
48+
if (data.trim().length === 0) {
49+
return;
5150
}
5251
else {
53-
// data is already a JavaScript value (object, array, number, null, NaN, etc.)
54-
resolve(data);
52+
let result = parseWithPointers(data, {
53+
ignoreDuplicateKeys: false,
54+
});
55+
56+
if (StoplightParserError.hasErrors(result.diagnostics)) {
57+
throw new StoplightParserError(result.diagnostics, file.url);
58+
}
59+
60+
return result.data;
5561
}
56-
}));
62+
}
63+
else {
64+
// data is already a JavaScript value (object, array, number, null, NaN, etc.)
65+
return data;
66+
}
5767
}
5868
};

lib/parsers/text.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use strict";
22

3+
const { ParserError } = require("../util/errors");
4+
35
let TEXT_REGEXP = /\.(txt|htm|html|md|xml|js|min|map|css|scss|less|svg)$/i;
46

57
module.exports = {
@@ -48,7 +50,7 @@ module.exports = {
4850
* @param {string} file.url - The full URL of the referenced file
4951
* @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
5052
* @param {*} file.data - The file contents. This will be whatever data type was returned by the resolver
51-
* @returns {Promise<string>}
53+
* @returns {string}
5254
*/
5355
parse (file) {
5456
if (typeof file.data === "string") {
@@ -58,7 +60,7 @@ module.exports = {
5860
return file.data.toString(this.encoding);
5961
}
6062
else {
61-
throw new Error("data is not text");
63+
throw new ParserError("data is not text", file.url);
6264
}
6365
}
6466
};

lib/parsers/yaml.js

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use strict";
22

33
const YAML = require("../util/yaml");
4+
const { StoplightParserError } = require("../util/errors");
45

56
module.exports = {
67
/**
@@ -36,20 +37,23 @@ module.exports = {
3637
* @param {*} file.data - The file contents. This will be whatever data type was returned by the resolver
3738
* @returns {Promise}
3839
*/
39-
parse (file) {
40-
return new Promise(((resolve, reject) => {
41-
let data = file.data;
42-
if (Buffer.isBuffer(data)) {
43-
data = data.toString();
44-
}
40+
async parse (file) {
41+
let data = file.data;
42+
if (Buffer.isBuffer(data)) {
43+
data = data.toString();
44+
}
4545

46-
if (typeof data === "string") {
47-
resolve(YAML.parse(data));
48-
}
49-
else {
50-
// data is already a JavaScript value (object, array, number, null, NaN, etc.)
51-
resolve(data);
46+
if (typeof data === "string") {
47+
let result = YAML.parse(data);
48+
if (StoplightParserError.hasErrors(result.diagnostics)) {
49+
throw new StoplightParserError(result.diagnostics, file.url);
5250
}
53-
}));
51+
52+
return result.data;
53+
}
54+
else {
55+
// data is already a JavaScript value (object, array, number, null, NaN, etc.)
56+
return data;
57+
}
5458
}
5559
};

lib/util/errors.js

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,54 @@ const GenericErrorGroup = exports.GenericErrorGroup = class GenericErrorGroup ex
1818
constructor (errors, source) {
1919
super();
2020

21-
this.source = source;
21+
this._path = undefined;
22+
this._source = source;
2223
this.errors = errors;
2324
}
25+
26+
get source () {
27+
return this._source;
28+
}
29+
30+
set source (source) {
31+
this._source = source;
32+
33+
for (let error of this.errors) {
34+
error.source = source;
35+
}
36+
}
37+
38+
get path () {
39+
return this.path;
40+
}
41+
42+
set path (path) {
43+
this._path = path;
44+
45+
for (let error of this.errors) {
46+
error.path = path;
47+
}
48+
}
2449
};
2550

2651
exports.StoplightParserError = class StoplightParserError extends GenericErrorGroup {
27-
constructor (errors, source) {
28-
super(errors.filter(error => error.severity === 0).map(error => {
29-
const parsingError = new ParserError(error.message, source);
30-
parsingError.message = error.message;
31-
if (error.path) {
32-
parsingError.path = error.path;
33-
}
34-
35-
return parsingError;
52+
constructor (diagnostics, source) {
53+
super(diagnostics.filter(StoplightParserError.pickError).map(error => {
54+
let parserError = new ParserError(error.message, source);
55+
parserError.message = error.message;
56+
return parserError;
3657
}));
3758

3859
this.message = `Error parsing ${source}`;
3960
}
61+
62+
static pickError (diagnostic) {
63+
return diagnostic.severity === 0;
64+
}
65+
66+
static hasErrors (diagnostics) {
67+
return diagnostics.some(StoplightParserError.pickError);
68+
}
4069
};
4170

4271
const ParserError = exports.ParserError = class ParserError extends GenericError {

lib/util/plugins.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,12 @@ exports.run = function (plugins, method, file, $refs) {
108108
});
109109
}
110110

111-
function onError (err) {
111+
function onError (error) {
112112
// console.log(' %s', err.message || err);
113-
lastError = err;
113+
lastError = {
114+
plugin,
115+
error,
116+
};
114117
runNextPlugin();
115118
}
116119
}));

lib/util/yaml.js

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/* eslint lines-around-comment: [2, {beforeBlockComment: false}] */
22
"use strict";
33

4-
const yaml = require("js-yaml");
5-
const { ono } = require("ono");
4+
const { parseWithPointers, safeStringify } = require("@stoplight/yaml");
65

76
/**
87
* Simple YAML parsing functions, similar to {@link JSON.parse} and {@link JSON.stringify}
@@ -16,18 +15,11 @@ module.exports = {
1615
* @returns {*}
1716
*/
1817
parse (text, reviver) {
19-
try {
20-
return yaml.safeLoad(text);
21-
}
22-
catch (e) {
23-
if (e instanceof Error) {
24-
throw e;
25-
}
26-
else {
27-
// https://github.com/nodeca/js-yaml/issues/153
28-
throw ono(e, e.message);
29-
}
30-
}
18+
return parseWithPointers(text, {
19+
json: true,
20+
mergeKeys: true,
21+
ignoreDuplicateKeys: false,
22+
});
3123
},
3224

3325
/**
@@ -39,18 +31,7 @@ module.exports = {
3931
* @returns {string}
4032
*/
4133
stringify (value, replacer, space) {
42-
try {
43-
let indent = (typeof space === "string" ? space.length : space) || 2;
44-
return yaml.safeDump(value, { indent });
45-
}
46-
catch (e) {
47-
if (e instanceof Error) {
48-
throw e;
49-
}
50-
else {
51-
// https://github.com/nodeca/js-yaml/issues/153
52-
throw ono(e, e.message);
53-
}
54-
}
34+
let indent = (typeof space === "string" ? space.length : space) || 2;
35+
return safeStringify(value, { indent });
5536
}
5637
};

0 commit comments

Comments
 (0)