Skip to content

Commit 948175e

Browse files
committed
Foundation for error handling
1 parent ee4498e commit 948175e

7 files changed

Lines changed: 168 additions & 9 deletions

File tree

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/index.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ const resolveExternal = require("./resolve-external");
88
const bundle = require("./bundle");
99
const dereference = require("./dereference");
1010
const url = require("./util/url");
11+
const { GenericError, MissingPointerError, ResolverError, ParserError, isHandledError } = require("./util/errors");
1112
const maybe = require("call-me-maybe");
1213
const { ono } = require("ono");
1314

1415
module.exports = $RefParser;
1516
module.exports.YAML = require("./util/yaml");
17+
module.exports.GenericError = GenericError;
18+
module.exports.MissingPointerError = MissingPointerError;
19+
module.exports.ResolverError = ResolverError;
20+
module.exports.ParserError = ParserError;
1621

1722
/**
1823
* This class parses a JSON schema, builds a map of its JSON references and their resolved values,
@@ -38,6 +43,25 @@ function $RefParser () {
3843
this.$refs = new $Refs();
3944
}
4045

46+
/**
47+
* List of all errors
48+
* @type {Array<GenericError | ResolverError | ParserError | MissingPointerError>}
49+
*/
50+
Object.defineProperty($RefParser.prototype, "errors", {
51+
get () {
52+
const errors = [];
53+
54+
for (const $ref of Object.values(this.$refs._$refs)) {
55+
if ($ref.errors) {
56+
errors.push(...$ref.errors);
57+
}
58+
}
59+
60+
return errors;
61+
},
62+
enumerable: true,
63+
});
64+
4165
/**
4266
* Parses the given JSON schema.
4367
* This method does not resolve any JSON references.
@@ -119,8 +143,16 @@ $RefParser.prototype.parse = async function (path, schema, options, callback) {
119143
return maybe(args.callback, Promise.resolve(me.schema));
120144
}
121145
}
122-
catch (e) {
123-
return maybe(args.callback, Promise.reject(e));
146+
catch (err) {
147+
if (args.options.failFast || !isHandledError(err)) {
148+
return maybe(args.callback, Promise.reject(err));
149+
}
150+
151+
if (this.$refs._$refs[url.stripHash(args.path)]) {
152+
this.$refs._$refs[url.stripHash(args.path)].addError(err);
153+
}
154+
155+
return maybe(args.callback, Promise.resolve(null));
124156
}
125157
};
126158

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const { ono } = require("ono");
44
const url = require("./util/url");
55
const plugins = require("./util/plugins");
6+
const { StoplightParserError, ResolverError, ParserError } = require("./util/errors");
67

78
module.exports = parse;
89

lib/ref.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
module.exports = $Ref;
44

55
const Pointer = require("./pointer");
6+
const { GenericError, GenericErrorGroup, ParserError, MissingPointerError, ResolverError } = require("./util/errors");
67

78
/**
89
* This class represents a single JSON reference and its resolved value.
@@ -40,8 +41,34 @@ function $Ref () {
4041
* @type {?string}
4142
*/
4243
this.pathType = undefined;
44+
45+
/**
46+
* List of all errors. Undefined if no errors.
47+
* @type {Array<GenericError | ResolverError | ParserError | MissingPointerError>}
48+
*/
49+
this.errors = undefined;
4350
}
4451

52+
/**
53+
* Pushes an error to errors array.
54+
*
55+
* @param {Array<GenericError | GenericErrorGroup>} error - The error to be pushed
56+
* @returns {void}
57+
*/
58+
$Ref.prototype.addError = function (err) {
59+
if (this.errors === undefined) {
60+
this.errors = [];
61+
}
62+
63+
if (Array.isArray(err.errors)) {
64+
this.errors.push(...err.errors);
65+
}
66+
else {
67+
this.errors.push(err);
68+
}
69+
};
70+
71+
4572
/**
4673
* Determines whether the given JSON reference exists within this {@link $Ref#value}.
4774
*

lib/resolve-external.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const $Ref = require("./ref");
44
const Pointer = require("./pointer");
55
const parse = require("./parse");
66
const url = require("./util/url");
7+
const { isHandledError } = require("./util/errors");
78

89
module.exports = resolveExternal;
910

@@ -101,11 +102,24 @@ async function resolve$Ref ($ref, path, $refs, options) {
101102
}
102103

103104
// Parse the $referenced file/url
104-
const result = await parse(resolvedPath, $refs, options);
105+
try {
106+
const result = await parse(resolvedPath, $refs, options);
107+
108+
// Crawl the parsed value
109+
// console.log('Resolving $ref pointers in %s', withoutHash);
110+
let promises = crawl(result, withoutHash + "#", $refs, options);
111+
112+
return Promise.all(promises);
113+
}
114+
catch (err) {
115+
if (options.failFast || !isHandledError(err)) {
116+
throw err;
117+
}
105118

106-
// Crawl the parsed value
107-
// console.log('Resolving $ref pointers in %s', withoutHash);
108-
let promises = crawl(result, withoutHash + "#", $refs, options);
119+
if ($refs._$refs[withoutHash]) {
120+
err.source = url.stripHash(path);
121+
}
109122

110-
return Promise.all(promises);
123+
return [];
124+
}
111125
}

lib/util/errors.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"use strict";
2+
3+
const { stripHash } = require("./url");
4+
5+
const GenericError = exports.GenericError = class GenericError extends Error {
6+
constructor (message, source) {
7+
super();
8+
9+
this.message = message;
10+
this.source = source;
11+
this.path = [];
12+
}
13+
};
14+
15+
setErrorName(GenericError);
16+
17+
const GenericErrorGroup = exports.GenericErrorGroup = class GenericErrorGroup extends Error {
18+
constructor (errors, source) {
19+
super();
20+
21+
this.source = source;
22+
this.errors = errors;
23+
}
24+
};
25+
26+
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;
36+
}));
37+
38+
this.message = `Error parsing ${source}`;
39+
}
40+
};
41+
42+
const ParserError = exports.ParserError = class ParserError extends GenericError {
43+
constructor (message, source) {
44+
super(`Error parsing ${source}: ${message}`, source);
45+
}
46+
};
47+
48+
setErrorName(ParserError);
49+
50+
const ResolverError = exports.ResolverError = class ResolverError extends GenericError {
51+
constructor (ex, source) {
52+
super(ex.message || `Error reading file ${source}`, source);
53+
if ("code" in ex) {
54+
this.code = String(ex.code);
55+
}
56+
}
57+
};
58+
59+
setErrorName(ResolverError);
60+
61+
const MissingPointerError = exports.MissingPointerError = class MissingPointerError extends GenericError {
62+
constructor (token, path) {
63+
super(`Token "${token}" does not exist.`, stripHash(path));
64+
}
65+
};
66+
67+
setErrorName(MissingPointerError);
68+
69+
function setErrorName (err) {
70+
Object.defineProperty(err.prototype, "name", {
71+
value: err.name,
72+
enumerable: true,
73+
});
74+
}
75+
76+
exports.isHandledError = function (err) {
77+
return err instanceof GenericError || err instanceof GenericErrorGroup;
78+
};

0 commit comments

Comments
 (0)