Skip to content

Commit ae6977f

Browse files
committed
convert relative refs when resolving an external schema
1 parent 6e6ba8c commit ae6977f

File tree

4 files changed

+132
-64
lines changed

4 files changed

+132
-64
lines changed

lib/resolve-external.js

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,22 @@ module.exports = resolveExternal;
2121
* The promise resolves once all JSON references in the schema have been resolved,
2222
* including nested references that are contained in externally-referenced files.
2323
*/
24-
function resolveExternal (parser, options) {
24+
function resolveExternal(parser, options) {
2525
if (!options.resolve.external) {
2626
// Nothing to resolve, so exit early
2727
return Promise.resolve();
2828
}
2929

3030
try {
3131
// console.log('Resolving $ref pointers in %s', parser.$refs._root$Ref.path);
32-
let promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", parser.$refs, options);
32+
let promises = crawl(
33+
parser.schema,
34+
parser.$refs._root$Ref.path + "#",
35+
parser.$refs,
36+
options
37+
);
3338
return Promise.all(promises);
34-
}
35-
catch (e) {
39+
} catch (e) {
3640
return Promise.reject(e);
3741
}
3842
}
@@ -44,31 +48,45 @@ function resolveExternal (parser, options) {
4448
* @param {string} path - The full path of `obj`, possibly with a JSON Pointer in the hash
4549
* @param {$Refs} $refs
4650
* @param {$RefParserOptions} options
51+
* @param {Set} seen - Internal.
4752
*
4853
* @returns {Promise[]}
4954
* Returns an array of promises. There will be one promise for each JSON reference in `obj`.
5055
* If `obj` does not contain any JSON references, then the array will be empty.
5156
* If any of the JSON references point to files that contain additional JSON references,
5257
* then the corresponding promise will internally reference an array of promises.
5358
*/
54-
function crawl (obj, path, $refs, options) {
59+
function crawl(obj, path, $refs, options, external, seen) {
60+
seen = seen || new Set();
5561
let promises = [];
5662

57-
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) {
63+
if (
64+
obj &&
65+
typeof obj === "object" &&
66+
!ArrayBuffer.isView(obj) &&
67+
!seen.has(obj)
68+
) {
69+
seen.add(obj); // Track previously seen objects to avoid infinite recursion
5870
if ($Ref.isExternal$Ref(obj)) {
5971
promises.push(resolve$Ref(obj, path, $refs, options));
60-
}
61-
else {
72+
} else {
73+
if (external && $Ref.is$Ref(obj)) {
74+
/* Correct the reference in the external document so we can resolve it */
75+
let withoutHash = url.stripHash(path);
76+
if (url.isFileSystemPath(withoutHash)) {
77+
/* remove file:// from path */
78+
withoutHash = url.toFileSystemPath(withoutHash);
79+
}
80+
obj.$ref = withoutHash + obj.$ref;
81+
}
82+
6283
for (let key of Object.keys(obj)) {
6384
let keyPath = Pointer.join(path, key);
6485
let value = obj[key];
6586

66-
if ($Ref.isExternal$Ref(value)) {
67-
promises.push(resolve$Ref(value, keyPath, $refs, options));
68-
}
69-
else {
70-
promises = promises.concat(crawl(value, keyPath, $refs, options));
71-
}
87+
promises = promises.concat(
88+
crawl(value, keyPath, $refs, options, external, seen)
89+
);
7290
}
7391
}
7492
}
@@ -88,7 +106,7 @@ function crawl (obj, path, $refs, options) {
88106
* The promise resolves once all JSON references in the object have been resolved,
89107
* including nested references that are contained in externally-referenced files.
90108
*/
91-
async function resolve$Ref ($ref, path, $refs, options) {
109+
async function resolve$Ref($ref, path, $refs, options) {
92110
// console.log('Resolving $ref pointer "%s" at %s', $ref.$ref, path);
93111

94112
let resolvedPath = url.resolve(path, $ref.$ref);
@@ -107,17 +125,16 @@ async function resolve$Ref ($ref, path, $refs, options) {
107125

108126
// Crawl the parsed value
109127
// console.log('Resolving $ref pointers in %s', withoutHash);
110-
let promises = crawl(result, withoutHash + "#", $refs, options);
128+
let promises = crawl(result, withoutHash + "#", $refs, options, true);
111129

112130
return Promise.all(promises);
113-
}
114-
catch (err) {
131+
} catch (err) {
115132
if (!options.continueOnError || !isHandledError(err)) {
116133
throw err;
117134
}
118135

119136
if ($refs._$refs[withoutHash]) {
120-
err.source = url.stripHash(path);
137+
err.source = decodeURI(url.stripHash(path));
121138
err.path = url.safePointerToPath(url.getHash(path));
122139
}
123140

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/specs/absolute-root/absolute-root.spec.js

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,18 @@ describe("When executed in the context of root directory", () => {
2121
/**
2222
* A mock `process.cwd()` implementation that always returns the root diretory
2323
*/
24-
function mockProcessCwd () {
24+
function mockProcessCwd() {
2525
return root;
2626
}
2727

2828
/**
2929
* Temporarily mocks `process.cwd()` while calling the real `url.cwd()` implemenation
3030
*/
31-
function mockUrlCwd () {
31+
function mockUrlCwd() {
3232
try {
3333
process.cwd = mockProcessCwd;
3434
return originalUrlCwd.apply(null, arguments);
35-
}
36-
finally {
35+
} finally {
3736
process.cwd = originalProcessCwd;
3837
}
3938
}
@@ -47,44 +46,65 @@ describe("When executed in the context of root directory", () => {
4746
process.cwd = originalProcessCwd; // already restored by the finally block above, but just in case
4847
});
4948

50-
5149
it("should parse successfully from an absolute path", async () => {
5250
let parser = new $RefParser();
53-
const schema = await parser.parse(path.abs("specs/absolute-root/absolute-root.yaml"));
51+
const schema = await parser.parse(
52+
path.abs("specs/absolute-root/absolute-root.yaml")
53+
);
5454
expect(schema).to.equal(parser.schema);
5555
expect(schema).to.deep.equal(parsedSchema.schema);
5656
expect(parser.$refs.paths()).to.deep.equal([
57-
path.abs("specs/absolute-root/absolute-root.yaml")
57+
path.abs("specs/absolute-root/absolute-root.yaml"),
5858
]);
5959
});
6060

6161
it("should parse successfully from a url", async () => {
6262
let parser = new $RefParser();
63-
const schema = await parser.parse(path.url("specs/absolute-root/absolute-root.yaml"));
63+
const schema = await parser.parse(
64+
path.url("specs/absolute-root/absolute-root.yaml")
65+
);
6466
expect(schema).to.equal(parser.schema);
6567
expect(schema).to.deep.equal(parsedSchema.schema);
66-
expect(parser.$refs.paths()).to.deep.equal([path.url("specs/absolute-root/absolute-root.yaml")]);
68+
expect(parser.$refs.paths()).to.deep.equal([
69+
path.url("specs/absolute-root/absolute-root.yaml"),
70+
]);
6771
});
6872

69-
it("should resolve successfully from an absolute path", helper.testResolve(
70-
path.abs("specs/absolute-root/absolute-root.yaml"),
71-
path.abs("specs/absolute-root/absolute-root.yaml"), parsedSchema.schema,
72-
path.abs("specs/absolute-root/definitions/definitions.json"), parsedSchema.definitions,
73-
path.abs("specs/absolute-root/definitions/name.yaml"), parsedSchema.name,
74-
path.abs("specs/absolute-root/definitions/required-string.yaml"), parsedSchema.requiredString
75-
));
73+
it(
74+
"should resolve successfully from an absolute path",
75+
helper.testResolve(
76+
path.abs("specs/absolute-root/absolute-root.yaml"),
77+
path.abs("specs/absolute-root/absolute-root.yaml"),
78+
parsedSchema.schema,
79+
path.abs("specs/absolute-root/definitions/definitions.json"),
80+
parsedSchema.definitions,
81+
path.abs("specs/absolute-root/definitions/name.yaml"),
82+
parsedSchema.name,
83+
path.abs("specs/absolute-root/definitions/required-string.yaml"),
84+
parsedSchema.requiredString
85+
)
86+
);
7687

77-
it("should resolve successfully from a url", helper.testResolve(
78-
path.url("specs/absolute-root/absolute-root.yaml"),
79-
path.url("specs/absolute-root/absolute-root.yaml"), parsedSchema.schema,
80-
path.url("specs/absolute-root/definitions/definitions.json"), parsedSchema.definitions,
81-
path.url("specs/absolute-root/definitions/name.yaml"), parsedSchema.name,
82-
path.url("specs/absolute-root/definitions/required-string.yaml"), parsedSchema.requiredString
83-
));
88+
it(
89+
"should resolve successfully from a url",
90+
helper.testResolve(
91+
path.url("specs/absolute-root/absolute-root.yaml"),
92+
path.url("specs/absolute-root/absolute-root.yaml"),
93+
parsedSchema.schema,
94+
path.url("specs/absolute-root/definitions/definitions.json"),
95+
parsedSchema.definitions,
96+
path.url("specs/absolute-root/definitions/name.yaml"),
97+
parsedSchema.name,
98+
path.url("specs/absolute-root/definitions/required-string.yaml"),
99+
parsedSchema.requiredString
100+
)
101+
);
84102

85103
it("should dereference successfully", async () => {
86104
let parser = new $RefParser();
87-
const schema = await parser.dereference(path.abs("specs/absolute-root/absolute-root.yaml"));
105+
const schema = await parser.dereference(
106+
path.abs("specs/absolute-root/absolute-root.yaml")
107+
);
88108
expect(schema).to.equal(parser.schema);
89109
expect(schema).to.deep.equal(dereferencedSchema);
90110
// Reference equality
@@ -100,7 +120,9 @@ describe("When executed in the context of root directory", () => {
100120

101121
it("should bundle successfully", async () => {
102122
let parser = new $RefParser();
103-
const schema = await parser.bundle(path.abs("specs/absolute-root/absolute-root.yaml"));
123+
const schema = await parser.bundle(
124+
path.abs("specs/absolute-root/absolute-root.yaml")
125+
);
104126
expect(schema).to.equal(parser.schema);
105127
expect(schema).to.deep.equal(bundledSchema);
106128
});

test/utils/helper.js

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ const $RefParser = require("../../lib");
44
const { host } = require("@jsdevtools/host-environment");
55
const { expect } = require("chai");
66

7-
const helper = module.exports = {
7+
const helper = (module.exports = {
88
/**
99
* Throws an error if called.
1010
*/
11-
shouldNotGetCalled () {
11+
shouldNotGetCalled() {
1212
throw new Error("This function should not have gotten called.");
1313
},
1414

@@ -20,9 +20,11 @@ const helper = module.exports = {
2020
* @param {...*} [params] - The expected resolved file paths and values
2121
* @returns {Function}
2222
*/
23-
testResolve (filePath, params) {
23+
testResolve(filePath, params) {
2424
let parsedSchema = arguments[2];
25-
let expectedFiles = [], messages = [], actualFiles;
25+
let expectedFiles = [],
26+
messages = [],
27+
actualFiles;
2628

2729
for (let i = 1; i < arguments.length; i += 2) {
2830
expectedFiles.push(arguments[i]);
@@ -38,17 +40,21 @@ const helper = module.exports = {
3840

3941
// Resolved file paths
4042
try {
41-
expect((actualFiles = $refs.paths())).to.have.same.members(expectedFiles);
43+
expect((actualFiles = $refs.paths())).to.have.same.members(
44+
expectedFiles
45+
);
4246
if (host.node) {
43-
expect((actualFiles = $refs.paths(["file"]))).to.have.same.members(expectedFiles);
47+
expect((actualFiles = $refs.paths(["file"]))).to.have.same.members(
48+
expectedFiles
49+
);
4450
expect($refs.paths("http")).to.be.an("array").with.lengthOf(0);
45-
}
46-
else {
47-
expect((actualFiles = $refs.paths(["http"]))).to.have.same.members(expectedFiles);
51+
} else {
52+
expect((actualFiles = $refs.paths(["http"]))).to.have.same.members(
53+
expectedFiles
54+
);
4855
expect($refs.paths("file")).to.be.an("array").with.lengthOf(0);
4956
}
50-
}
51-
catch (e) {
57+
} catch (e) {
5258
console.log("Expected Files:", JSON.stringify(expectedFiles, null, 2));
5359
console.log("Actual Files:", JSON.stringify(actualFiles, null, 2));
5460
throw e;
@@ -60,37 +66,60 @@ const helper = module.exports = {
6066
for (let [i, file] of expectedFiles.entries()) {
6167
let actual = helper.convertNodeBuffersToPOJOs(values[file]);
6268
let expected = messages[i];
69+
if (file !== filePath && !file.endsWith(filePath)) {
70+
this.addAbsolutePathToRefs(file, expected);
71+
}
6372
expect(actual).to.deep.equal(expected, file);
6473
}
6574
};
6675
},
6776

77+
addAbsolutePathToRefs(filePath, expected) {
78+
if (typeof expected === "object") {
79+
for (let [i, entry] of Object.entries(expected)) {
80+
if (typeof entry === "object") {
81+
if (typeof entry.$ref === "string") {
82+
if (entry.$ref.startsWith("#")) {
83+
expected[i].$ref = filePath + entry.$ref;
84+
}
85+
continue;
86+
} else {
87+
expected[i] = this.addAbsolutePathToRefs(filePath, entry);
88+
}
89+
}
90+
}
91+
}
92+
return expected;
93+
},
94+
6895
/**
6996
* Converts Buffer objects to POJOs, so they can be compared using Chai
7097
*/
71-
convertNodeBuffersToPOJOs (value) {
72-
if (value && (value._isBuffer || (value.constructor && value.constructor.name === "Buffer"))) {
98+
convertNodeBuffersToPOJOs(value) {
99+
if (
100+
value &&
101+
(value._isBuffer ||
102+
(value.constructor && value.constructor.name === "Buffer"))
103+
) {
73104
// Convert Buffers to POJOs for comparison
74105
value = value.toJSON();
75106

76107
if (host.node && host.node.version === 0.1) {
77108
// Node v0.10 serializes buffers differently
78109
value = { type: "Buffer", data: value };
79110
}
80-
}
81-
else if (ArrayBuffer.isView(value)) {
111+
} else if (ArrayBuffer.isView(value)) {
82112
value = { type: "Buffer", data: Array.from(value) };
83113
}
84-
85114
return value;
86115
},
87116

88117
/**
89118
* Creates a deep clone of the given value.
90119
*/
91-
cloneDeep (value) {
120+
cloneDeep(value) {
92121
let clone = value;
93-
if (value && typeof (value) === "object") {
122+
if (value && typeof value === "object") {
94123
clone = value instanceof Array ? [] : {};
95124
let keys = Object.keys(value);
96125
for (let i = 0; i < keys.length; i++) {
@@ -99,4 +128,4 @@ const helper = module.exports = {
99128
}
100129
return clone;
101130
},
102-
};
131+
});

0 commit comments

Comments
 (0)