Skip to content

Commit 0dfff52

Browse files
Merge pull request APIDevTools#153 from stoplightio/feature/fail-fast
Introduce failFast option
2 parents 0fa6118 + de4c588 commit 0dfff52

32 files changed

+1009
-157
lines changed

docs/options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ $RefParser.dereference("my-schema.yaml", {
2727
withCredentials: true, // Include auth credentials when resolving HTTP references
2828
}
2929
},
30+
failFast: true, // Abort upon first exception
3031
dereference: {
3132
circular: false // Don't allow circular $refs
3233
}

lib/bundle.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ function crawl (parent, key, path, pathFromRoot, indirections, inventory, $refs,
9696
function inventory$Ref ($refParent, $refKey, path, pathFromRoot, indirections, inventory, $refs, options) {
9797
let $ref = $refKey === null ? $refParent : $refParent[$refKey];
9898
let $refPath = url.resolve(path, $ref.$ref);
99-
let pointer = $refs._resolve($refPath, options);
99+
let pointer = $refs._resolve($refPath, pathFromRoot, options);
100+
if (pointer === null) {
101+
return;
102+
}
103+
100104
let depth = Pointer.parse(pathFromRoot).length;
101105
let file = url.stripHash(pointer.path);
102106
let hash = url.getHash(pointer.path);

lib/dereference.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@ function dereference$Ref ($ref, path, pathFromRoot, parents, $refs, options) {
9696
// console.log('Dereferencing $ref pointer "%s" at %s', $ref.$ref, path);
9797

9898
let $refPath = url.resolve(path, $ref.$ref);
99-
let pointer = $refs._resolve($refPath, options);
99+
let pointer = $refs._resolve($refPath, pathFromRoot, options);
100+
101+
if (pointer === null) {
102+
return {
103+
circular: false,
104+
value: null,
105+
};
106+
}
100107

101108
// Check for circular references
102109
let directCircular = pointer.circular;

lib/index.d.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ declare namespace $RefParser {
210210
[key: string]: Partial<ResolverOptions>
211211
}
212212

213+
/**
214+
* Determines how lenient the processing should be.
215+
* If this option is enable, the processing will be performed in a bail mode - will abort upon the first exception.
216+
*/
217+
failFast?: boolean;
218+
213219
/**
214220
* The `dereference` options control how JSON Schema `$Ref` Parser will dereference `$ref` pointers within the JSON schema.
215221
*/
@@ -398,4 +404,60 @@ declare namespace $RefParser {
398404
set($ref: string, value: JSONSchema4Type | JSONSchema6Type): void
399405
}
400406

407+
export type JSONParserErrorType = "EUNKNOWN" | "EPARSER" | "EUNMATCHEDPARSER" | "ERESOLVER" | "EUNMATCHEDRESOLVER" | "EMISSINGPOINTER" | "EINVALIDPOINTER";
408+
409+
export class JSONParserError extends Error {
410+
readonly name: string;
411+
readonly message: string;
412+
readonly path: Array<string | number>;
413+
readonly errors: string;
414+
readonly code: JSONParserErrorType;
415+
}
416+
417+
export class JSONParserErrorGroup extends Error {
418+
/**
419+
* List of all errors
420+
*
421+
* See https://github.com/APIDevTools/json-schema-ref-parser/blob/master/docs/ref-parser.md#errors
422+
*/
423+
readonly errors: Array<$RefParser.JSONParserError | $RefParser.InvalidPointerError | $RefParser.ResolverError | $RefParser.ParserError | $RefParser.MissingPointerError | $RefParser.UnmatchedParserError | $RefParser.UnmatchedResolverError>;
424+
425+
/**
426+
* The fields property is a `$RefParser` instance
427+
*
428+
* See https://apitools.dev/json-schema-ref-parser/docs/ref-parser.html
429+
*/
430+
readonly files: $RefParser;
431+
432+
/**
433+
* User friendly message containing the total amount of errors, as well as the absolute path to the source document
434+
*/
435+
readonly message: string;
436+
}
437+
438+
export class ParserError extends JSONParserError {
439+
readonly name = "ParserError";
440+
readonly code = "EPARSER";
441+
}
442+
export class UnmatchedParserError extends JSONParserError {
443+
readonly name = "UnmatchedParserError";
444+
readonly code ="EUNMATCHEDPARSER";
445+
}
446+
export class ResolverError extends JSONParserError {
447+
readonly name = "ResolverError";
448+
readonly code ="ERESOLVER";
449+
readonly ioErrorCode?: string;
450+
}
451+
export class UnmatchedResolverError extends JSONParserError {
452+
readonly name = "UnmatchedResolverError";
453+
readonly code ="EUNMATCHEDRESOLVER";
454+
}
455+
export class MissingPointerError extends JSONParserError {
456+
readonly name = "MissingPointerError";
457+
readonly code ="EMISSINGPOINTER";
458+
}
459+
export class InvalidPointerError extends JSONParserError {
460+
readonly name = "InvalidPointerError";
461+
readonly code ="EINVALIDPOINTER";
462+
}
401463
}

lib/index.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@ const resolveExternal = require("./resolve-external");
88
const bundle = require("./bundle");
99
const dereference = require("./dereference");
1010
const url = require("./util/url");
11+
const { JSONParserError, InvalidPointerError, MissingPointerError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError, JSONParserErrorGroup } = require("./util/errors");
1112
const maybe = require("call-me-maybe");
1213
const { ono } = require("@jsdevtools/ono");
1314

1415
module.exports = $RefParser;
1516
module.exports.YAML = require("./util/yaml");
17+
module.exports.JSONParserError = JSONParserError;
18+
module.exports.InvalidPointerError = InvalidPointerError;
19+
module.exports.MissingPointerError = MissingPointerError;
20+
module.exports.ResolverError = ResolverError;
21+
module.exports.ParserError = ParserError;
22+
module.exports.UnmatchedParserError = UnmatchedParserError;
23+
module.exports.UnmatchedResolverError = UnmatchedResolverError;
1624

1725
/**
1826
* This class parses a JSON schema, builds a map of its JSON references and their resolved values,
@@ -111,16 +119,28 @@ $RefParser.prototype.parse = async function (path, schema, options, callback) {
111119
try {
112120
let result = await promise;
113121

114-
if (!result || typeof result !== "object" || Buffer.isBuffer(result)) {
115-
throw ono.syntax(`"${me.$refs._root$Ref.path || result}" is not a valid JSON Schema`);
116-
}
117-
else {
122+
if (result !== null && typeof result === "object" && !Buffer.isBuffer(result)) {
118123
me.schema = result;
119124
return maybe(args.callback, Promise.resolve(me.schema));
120125
}
126+
else if (!args.options.failFast) {
127+
me.schema = null; // it's already set to null at line 79, but let's set it again for the sake of readability
128+
return maybe(args.callback, Promise.resolve(me.schema));
129+
}
130+
else {
131+
throw ono.syntax(`"${me.$refs._root$Ref.path || result}" is not a valid JSON Schema`);
132+
}
121133
}
122-
catch (e) {
123-
return maybe(args.callback, Promise.reject(e));
134+
catch (err) {
135+
if (args.options.failFast || !isHandledError(err)) {
136+
return maybe(args.callback, Promise.reject(err));
137+
}
138+
139+
if (this.$refs._$refs[url.stripHash(args.path)]) {
140+
this.$refs._$refs[url.stripHash(args.path)].addError(err);
141+
}
142+
143+
return maybe(args.callback, Promise.resolve(null));
124144
}
125145
};
126146

@@ -163,6 +183,7 @@ $RefParser.prototype.resolve = async function (path, schema, options, callback)
163183
try {
164184
await this.parse(args.path, args.schema, args.options);
165185
await resolveExternal(me, args.options);
186+
finalize(me);
166187
return maybe(args.callback, Promise.resolve(me.$refs));
167188
}
168189
catch (err) {
@@ -205,6 +226,7 @@ $RefParser.prototype.bundle = async function (path, schema, options, callback) {
205226
try {
206227
await this.resolve(args.path, args.schema, args.options);
207228
bundle(me, args.options);
229+
finalize(me);
208230
return maybe(args.callback, Promise.resolve(me.schema));
209231
}
210232
catch (err) {
@@ -245,9 +267,17 @@ $RefParser.prototype.dereference = async function (path, schema, options, callba
245267
try {
246268
await this.resolve(args.path, args.schema, args.options);
247269
dereference(me, args.options);
270+
finalize(me);
248271
return maybe(args.callback, Promise.resolve(me.schema));
249272
}
250273
catch (err) {
251274
return maybe(args.callback, Promise.reject(err));
252275
}
253276
};
277+
278+
function finalize (parser) {
279+
const errors = JSONParserErrorGroup.getParserErrors(parser);
280+
if (errors.length > 0) {
281+
throw new JSONParserErrorGroup(parser);
282+
}
283+
}

lib/options.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ $RefParserOptions.defaults = {
2626
* Determines how different types of files will be parsed.
2727
*
2828
* You can add additional parsers of your own, replace an existing one with
29-
* your own implemenation, or disable any parser by setting it to false.
29+
* your own implementation, or disable any parser by setting it to false.
3030
*/
3131
parse: {
3232
json: jsonParser,
@@ -39,7 +39,7 @@ $RefParserOptions.defaults = {
3939
* Determines how JSON References will be resolved.
4040
*
4141
* You can add additional resolvers of your own, replace an existing one with
42-
* your own implemenation, or disable any resolver by setting it to false.
42+
* your own implementation, or disable any resolver by setting it to false.
4343
*/
4444
resolve: {
4545
file: fileResolver,
@@ -55,6 +55,12 @@ $RefParserOptions.defaults = {
5555
external: true,
5656
},
5757

58+
/**
59+
* Determines how lenient the processing should be.
60+
* If this option is enable, the processing will be performed in a bail mode - will abort upon the first exception.
61+
*/
62+
failFast: true,
63+
5864
/**
5965
* Determines the types of JSON references that are allowed.
6066
*/

lib/parse.js

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const { ono } = require("@jsdevtools/ono");
44
const url = require("./util/url");
55
const plugins = require("./util/plugins");
6+
const { StoplightParserError, ResolverError, ParserError, UnmatchedParserError, UnmatchedResolverError, isHandledError } = require("./util/errors");
67

78
module.exports = parse;
89

@@ -17,21 +18,21 @@ module.exports = parse;
1718
* The promise resolves with the parsed file contents, NOT the raw (Buffer) contents.
1819
*/
1920
async function parse (path, $refs, options) {
20-
try {
21-
// Remove the URL fragment, if any
22-
path = url.stripHash(path);
21+
// Remove the URL fragment, if any
22+
path = url.stripHash(path);
2323

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);
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);
2727

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

34-
// Read the file and then parse the data
34+
// Read the file and then parse the data
35+
try {
3536
const resolver = await readFile(file, options, $refs);
3637
$ref.pathType = resolver.plugin.name;
3738
file.data = resolver.result;
@@ -41,8 +42,12 @@ async function parse (path, $refs, options) {
4142

4243
return parser.result;
4344
}
44-
catch (e) {
45-
return Promise.reject(e);
45+
catch (err) {
46+
if (isHandledError(err)) {
47+
$ref.value = err;
48+
}
49+
50+
throw err;
4651
}
4752
}
4853

@@ -71,13 +76,20 @@ function readFile (file, options, $refs) {
7176
.then(resolve, onError);
7277

7378
function onError (err) {
79+
if (!err && !options.failFast) {
80+
// No resolver could be matched
81+
reject(new UnmatchedResolverError(file.url));
82+
}
83+
else if (!err || !("error" in err)) {
84+
// Throw a generic, friendly error.
85+
reject(ono.syntax(`Unable to resolve $ref pointer "${file.url}"`));
86+
}
7487
// Throw the original error, if it's one of our own (user-friendly) errors.
75-
// Otherwise, throw a generic, friendly error.
76-
if (err && !(err instanceof SyntaxError)) {
77-
reject(err);
88+
else if (err.error instanceof ResolverError) {
89+
reject(err.error);
7890
}
7991
else {
80-
reject(ono.syntax(`Unable to resolve $ref pointer "${file.url}"`));
92+
reject(new ResolverError(err, file.url));
8193
}
8294
}
8395
}));
@@ -112,7 +124,7 @@ function parseFile (file, options, $refs) {
112124
.then(onParsed, onError);
113125

114126
function onParsed (parser) {
115-
if (!parser.plugin.allowEmpty && isEmpty(parser.result)) {
127+
if ((!options.failFast || !parser.plugin.allowEmpty) && isEmpty(parser.result)) {
116128
reject(ono.syntax(`Error parsing "${file.url}" as ${parser.plugin.name}. \nParsed value is empty`));
117129
}
118130
else {
@@ -121,13 +133,19 @@ function parseFile (file, options, $refs) {
121133
}
122134

123135
function onError (err) {
124-
if (err) {
125-
err = err instanceof Error ? err : new Error(err);
126-
reject(ono.syntax(err, `Error parsing ${file.url}`));
136+
if (!err && !options.failFast) {
137+
// No resolver could be matched
138+
reject(new UnmatchedParserError(file.url));
127139
}
128-
else {
140+
else if (!err || !("error" in err)) {
129141
reject(ono.syntax(`Unable to parse ${file.url}`));
130142
}
143+
else if (err.error instanceof ParserError || err.error instanceof StoplightParserError) {
144+
reject(err.error);
145+
}
146+
else {
147+
reject(new ParserError(err.error.message, file.url));
148+
}
131149
}
132150
}));
133151
}

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)) {

0 commit comments

Comments
 (0)