Skip to content

Commit d3ac54f

Browse files
committed
Allow pointer errors
1 parent e3b3562 commit d3ac54f

10 files changed

Lines changed: 180 additions & 26 deletions

File tree

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module.exports = Pointer;
44

55
const $Ref = require("./ref");
66
const url = require("./util/url");
7-
const { ono } = require("ono");
7+
const { GenericError, MissingPointerError, isHandledError } = require("./util/errors");
88
const slashes = /\//g;
99
const tildes = /~/g;
1010
const escapedSlash = /~1/g;
@@ -75,16 +75,18 @@ Pointer.prototype.resolve = function (obj, options) {
7575
let tokens = Pointer.parse(this.path);
7676

7777
// Crawl the object, one token at a time
78-
this.value = obj;
78+
this.value = unwrapOrThrow(obj);
79+
7980
for (let i = 0; i < tokens.length; i++) {
8081
if (resolveIf$Ref(this, options)) {
8182
// The $ref path has changed, so append the remaining tokens to the path
8283
this.path = Pointer.join(this.path, tokens.slice(i));
8384
}
8485

8586
let token = tokens[i];
86-
if (this.value[token] === undefined) {
87-
throw ono.syntax(`Error resolving $ref pointer "${this.originalPath}". \nToken "${token}" does not exist.`);
87+
if (this.value[token] === undefined || this.value[token] === null) {
88+
this.value = null;
89+
throw new MissingPointerError(token, this.originalPath);
8890
}
8991
else {
9092
this.value = this.value[token];
@@ -117,7 +119,8 @@ Pointer.prototype.set = function (obj, value, options) {
117119
}
118120

119121
// Crawl the object, one token at a time
120-
this.value = obj;
122+
this.value = unwrapOrThrow(obj);
123+
121124
for (let i = 0; i < tokens.length - 1; i++) {
122125
resolveIf$Ref(this, options);
123126

@@ -171,7 +174,7 @@ Pointer.parse = function (path) {
171174
}
172175

173176
if (pointer[0] !== "") {
174-
throw ono.syntax(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`);
177+
throw new GenericError(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`);
175178
}
176179

177180
return pointer.slice(1);
@@ -222,7 +225,7 @@ function resolveIf$Ref (pointer, options) {
222225
pointer.circular = true;
223226
}
224227
else {
225-
let resolved = pointer.$ref.$refs._resolve($refPath, options);
228+
let resolved = pointer.$ref.$refs._resolve($refPath, url.getHash(pointer.path), options);
226229
pointer.indirections += resolved.indirections + 1;
227230

228231
if ($Ref.isExtended$Ref(pointer.value)) {
@@ -264,7 +267,16 @@ function setValue (pointer, token, value) {
264267
}
265268
}
266269
else {
267-
throw ono.syntax(`Error assigning $ref pointer "${pointer.path}". \nCannot set "${token}" of a non-object.`);
270+
throw new GenericError(`Error assigning $ref pointer "${pointer.path}". \nCannot set "${token}" of a non-object.`);
271+
}
272+
return value;
273+
}
274+
275+
276+
function unwrapOrThrow (value) {
277+
if (isHandledError(value)) {
278+
throw value;
268279
}
280+
269281
return value;
270282
}

lib/ref.js

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

55
const Pointer = require("./pointer");
6-
const { GenericError, GenericErrorGroup, ParserError, MissingPointerError, ResolverError } = require("./util/errors");
6+
const { GenericError, GenericErrorGroup, ParserError, MissingPointerError, ResolverError, isHandledError } = require("./util/errors");
7+
const { safePointerToPath } = require("./util/url");
78

89
/**
910
* This class represents a single JSON reference and its resolved value.
@@ -102,12 +103,24 @@ $Ref.prototype.get = function (path, options) {
102103
*
103104
* @param {string} path - The full path being resolved, optionally with a JSON pointer in the hash
104105
* @param {$RefParserOptions} options
105-
* @param {string} [friendlyPath] - The original user-specified path (used for error messages)
106+
* @param {string} friendlyPath - The original user-specified path (used for error messages)
107+
* @param {string} pathFromRoot - The path of `obj` from the schema root
106108
* @returns {Pointer}
107109
*/
108-
$Ref.prototype.resolve = function (path, options, friendlyPath) {
110+
$Ref.prototype.resolve = function (path, options, friendlyPath, pathFromRoot) {
109111
let pointer = new Pointer(this, path, friendlyPath);
110-
return pointer.resolve(this.value, options);
112+
try {
113+
return pointer.resolve(this.value, options);
114+
}
115+
catch (err) {
116+
if (!options || options.failFast || !isHandledError(err)) {
117+
throw err;
118+
}
119+
120+
err.path = safePointerToPath(pathFromRoot);
121+
this.addError(err);
122+
return null;
123+
}
111124
};
112125

113126
/**

lib/refs.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ $Refs.prototype.toJSON = $Refs.prototype.values;
7979
*/
8080
$Refs.prototype.exists = function (path, options) {
8181
try {
82-
this._resolve(path, options);
82+
this._resolve(path, "", options);
8383
return true;
8484
}
8585
catch (e) {
@@ -95,7 +95,7 @@ $Refs.prototype.exists = function (path, options) {
9595
* @returns {*} - Returns the resolved value
9696
*/
9797
$Refs.prototype.get = function (path, options) {
98-
return this._resolve(path, options).value;
98+
return this._resolve(path, "", options).value;
9999
};
100100

101101
/**
@@ -139,11 +139,12 @@ $Refs.prototype._add = function (path) {
139139
* Resolves the given JSON reference.
140140
*
141141
* @param {string} path - The path being resolved, optionally with a JSON pointer in the hash
142+
* @param {string} pathFromRoot - The path of `obj` from the schema root
142143
* @param {$RefParserOptions} [options]
143144
* @returns {Pointer}
144145
* @protected
145146
*/
146-
$Refs.prototype._resolve = function (path, options) {
147+
$Refs.prototype._resolve = function (path, pathFromRoot, options) {
147148
let absPath = url.resolve(this._root$Ref.path, path);
148149
let withoutHash = url.stripHash(absPath);
149150
let $ref = this._$refs[withoutHash];
@@ -152,7 +153,7 @@ $Refs.prototype._resolve = function (path, options) {
152153
throw ono(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`);
153154
}
154155

155-
return $ref.resolve(absPath, options, path);
156+
return $ref.resolve(absPath, options, path, pathFromRoot);
156157
};
157158

158159
/**

lib/resolve-external.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ async function resolve$Ref ($ref, path, $refs, options) {
118118

119119
if ($refs._$refs[withoutHash]) {
120120
err.source = url.stripHash(path);
121+
err.path = url.safePointerToPath(url.getHash(path));
121122
}
122123

123124
return [];

lib/util/url.js

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

3+
const { pointerToPath } = require("@stoplight/json");
4+
35
let isWindows = /^win/.test(process.platform),
46
forwardSlashPattern = /\//g,
57
protocolPattern = /^(\w{2,}):\/\//i,
@@ -220,3 +222,19 @@ exports.toFileSystemPath = function toFileSystemPath (path, keepFileProtocol) {
220222

221223
return path;
222224
};
225+
226+
/**
227+
* Converts a $ref pointer to a valid JSON Path.
228+
* It _does not_ throw.
229+
*
230+
* @param {string} pointer
231+
* @returns {Array<number | string>}
232+
*/
233+
exports.safePointerToPath = function safePointerToPath (pointer) {
234+
try {
235+
return pointerToPath(pointer);
236+
}
237+
catch (ex) {
238+
return [];
239+
}
240+
};

test/specs/invalid/invalid.spec.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,5 +193,73 @@ describe("Invalid syntax", () => {
193193
foo: ":\n"
194194
});
195195
});
196+
197+
describe("when failFast is false", () => {
198+
it("should not throw an error for an invalid file path", async () => {
199+
const parser = new $RefParser();
200+
const result = await parser.dereference({ foo: { $ref: "this file does not exist" }}, { failFast: false });
201+
expect(parser.errors.length).to.equal(1);
202+
expect(parser.errors).to.containSubset([
203+
{
204+
name: ResolverError.name,
205+
message: expectedValue => expectedValue.startsWith("Error opening file"),
206+
path: ["foo"],
207+
source: expectedValue => expectedValue.endsWith("/test/"),
208+
}
209+
]);
210+
});
211+
212+
it("should not throw an error for an invalid YAML file", async () => {
213+
const parser = new $RefParser();
214+
const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { failFast: false });
215+
expect(result).to.deep.equal({ foo: null });
216+
expect(parser.errors.length).to.equal(1);
217+
expect(parser.errors).to.containSubset([
218+
{
219+
name: ParserError.name,
220+
message: "incomplete explicit mapping pair; a key node is missed",
221+
path: ["foo"],
222+
source: expectedValue => expectedValue.endsWith("/test/"),
223+
},
224+
]);
225+
});
226+
227+
it("should not throw an error for an invalid JSON file", async () => {
228+
const parser = new $RefParser();
229+
const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false });
230+
expect(result).to.deep.equal({ foo: null });
231+
expect(parser.errors.length).to.equal(1);
232+
expect(parser.errors).to.containSubset([
233+
{
234+
name: ParserError.name,
235+
message: "unexpected end of the stream within a flow collection",
236+
path: ["foo"],
237+
source: expectedValue => expectedValue.endsWith("/test/"),
238+
}
239+
]);
240+
});
241+
242+
it("should not throw an error for an invalid JSON file with YAML disabled", async () => {
243+
const parser = new $RefParser();
244+
const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.json") }}, { failFast: false, parse: { yaml: false }});
245+
expect(result).to.deep.equal({ foo: null });
246+
expect(parser.errors.length).to.equal(1);
247+
expect(parser.errors).to.containSubset([
248+
{
249+
name: ParserError.name,
250+
message: "CloseBraceExpected",
251+
path: ["foo"],
252+
source: expectedValue => expectedValue.endsWith("/test/"),
253+
}
254+
]);
255+
});
256+
257+
it("should not throw an error for an invalid YAML file with JSON and YAML disabled", async () => {
258+
const parser = new $RefParser();
259+
const result = await parser.dereference({ foo: { $ref: path.rel("specs/invalid/invalid.yaml") }}, { failFast: false, parse: { yaml: false, json: false }});
260+
expect(result).to.deep.equal({ foo: ":\n" });
261+
expect(parser.errors).to.deep.equal([]);
262+
});
263+
});
196264
});
197265
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use strict";
2+
3+
const chai = require("chai");
4+
const chaiSubset = require("chai-subset");
5+
chai.use(chaiSubset);
6+
const { expect } = chai;
7+
const $RefParser = require("../../../lib");
8+
const helper = require("../../utils/helper");
9+
const { MissingPointerError } = require("../../../lib/util/errors");
10+
11+
describe("Schema with missing pointers", () => {
12+
it("should throw an error for missing pointer", async () => {
13+
try {
14+
await $RefParser.dereference({ foo: { $ref: "#/baz" }});
15+
helper.shouldNotGetCalled();
16+
}
17+
catch (err) {
18+
expect(err).to.be.an.instanceOf(MissingPointerError);
19+
expect(err.message).to.contain("Token \"baz\" does not exist.");
20+
}
21+
});
22+
23+
it("should not throw an error for missing pointer if failFast is false", async () => {
24+
const parser = new $RefParser();
25+
const result = await parser.dereference({ foo: { $ref: "#/baz" }}, { failFast: false });
26+
expect(result).to.deep.equal({ foo: null });
27+
expect(parser.errors).to.containSubset([
28+
{
29+
name: MissingPointerError.name,
30+
message: "Token \"baz\" does not exist.",
31+
path: ["foo"],
32+
source: expectedValue => expectedValue.endsWith("/test/"),
33+
}
34+
]);
35+
});
36+
});

test/specs/refs.spec.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,7 @@ describe("$Refs object", () => {
216216
}
217217
catch (err) {
218218
expect(err).to.be.an.instanceOf(Error);
219-
expect(err.message).to.equal(
220-
'Error resolving $ref pointer "definitions/name.yaml#/". ' +
221-
'\nToken "" does not exist.'
222-
);
219+
expect(err.message).to.equal('Token "" does not exist.');
223220
}
224221
});
225222

@@ -257,10 +254,7 @@ describe("$Refs object", () => {
257254
}
258255
catch (err) {
259256
expect(err).to.be.an.instanceOf(Error);
260-
expect(err.message).to.equal(
261-
'Error resolving $ref pointer "external.yaml#/foo/bar". ' +
262-
'\nToken "foo" does not exist.'
263-
);
257+
expect(err.message).to.equal('Token "foo" does not exist.');
264258
}
265259
});
266260
});

0 commit comments

Comments
 (0)