Skip to content

Commit 4d328fa

Browse files
authored
feat: add reference resolution option to allow root level dereferencing (#305)
* feat: add reference resolution option to allow root level dereferencing * feat: add reference resolution option to allow root level dereferencing * skip if browser * convert path to posix for url first
1 parent b9f91b2 commit 4d328fa

15 files changed

+274
-13
lines changed

lib/dereference.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ function dereference$Ref(
169169
) {
170170
// console.log('Dereferencing $ref pointer "%s" at %s', $ref.$ref, path);
171171

172-
const $refPath = url.resolve(path, $ref.$ref);
172+
const isExternalRef = $Ref.isExternal$Ref($ref);
173+
const shouldResolveOnCwd = isExternalRef && options?.dereference.externalReferenceResolution === "root";
174+
const $refPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, $ref.$ref);
173175

174176
const cache = dereferencedCache.get($refPath);
175177
if (cache) {

lib/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ export class $RefParser {
100100
if (url.isFileSystemPath(args.path)) {
101101
args.path = url.fromFileSystemPath(args.path);
102102
pathType = "file";
103+
} else if (!args.path && args.schema && args.schema.$id) {
104+
// when schema id has defined an URL should use that hostname to request the references,
105+
// instead of using the current page URL
106+
const params = url.parse(args.schema.$id);
107+
const port = params.protocol === "https:" ? 443 : 80;
108+
109+
args.path = `${params.protocol}//${params.hostname}:${port}`;
103110
}
104111

105112
// Resolve the absolute path of the schema

lib/options.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ interface $RefParserOptions {
8383
* @argument {JSONSchemaObject} object The JSON-Schema that the `$ref` resolved to.
8484
*/
8585
onDereference?(path: string, value: JSONSchemaObject): void;
86+
87+
/**
88+
* Whether a reference should resolve relative to its directory/path, or from the cwd
89+
*
90+
* Default: `relative`
91+
*/
92+
externalReferenceResolution?: "relative" | "root";
8693
};
8794
}
8895

@@ -149,8 +156,9 @@ const getDefaults = () => {
149156
* @type {function}
150157
*/
151158
excludedPathMatcher: () => false,
159+
referenceResolution: "relative",
152160
},
153-
};
161+
} as $RefParserOptions;
154162
return cloneDeep(defaults);
155163
};
156164

lib/refs.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export default class $Refs {
9595
* @param value The value to assign. Can be anything (object, string, number, etc.)
9696
*/
9797
set(path: any, value: JSONSchema4Type | JSONSchema6Type | JSONSchema7Type) {
98-
const absPath = url.resolve(this._root$Ref.path, path);
98+
const absPath = url.resolve(this._root$Ref.path!, path);
9999
const withoutHash = url.stripHash(absPath);
100100
const $ref = this._$refs[withoutHash];
101101

@@ -113,7 +113,7 @@ export default class $Refs {
113113
* @protected
114114
*/
115115
_get$Ref(path: any) {
116-
path = url.resolve(this._root$Ref.path, path);
116+
path = url.resolve(this._root$Ref.path!, path);
117117
const withoutHash = url.stripHash(path);
118118
return this._$refs[withoutHash];
119119
}
@@ -145,7 +145,7 @@ export default class $Refs {
145145
* @protected
146146
*/
147147
_resolve(path: string, pathFromRoot: string, options?: any) {
148-
const absPath = url.resolve(this._root$Ref.path, path);
148+
const absPath = url.resolve(this._root$Ref.path!, path);
149149
const withoutHash = url.stripHash(absPath);
150150
const $ref = this._$refs[withoutHash];
151151

lib/resolve-external.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,8 @@ function crawl(
9292
* including nested references that are contained in externally-referenced files.
9393
*/
9494
async function resolve$Ref($ref: JSONSchema, path: string, $refs: $Refs, options: Options) {
95-
// console.log('Resolving $ref pointer "%s" at %s', $ref.$ref, path);
96-
97-
const resolvedPath = url.resolve(path, $ref.$ref);
95+
const shouldResolveOnCwd = options.dereference.externalReferenceResolution === "root";
96+
const resolvedPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, $ref.$ref!);
9897
const withoutHash = url.stripHash(resolvedPath);
9998

10099
// $ref.$ref = url.relative($refs._root$Ref.path, resolvedPath);

lib/util/url.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ const urlEncodePatterns = [/\?/g, "%3F", /#/g, "%23"];
1616
// RegExp patterns to URL-decode special characters for local filesystem paths
1717
const urlDecodePatterns = [/%23/g, "#", /%24/g, "$", /%26/g, "&", /%2C/g, ",", /%40/g, "@"];
1818

19-
export const parse = (u: any) => new URL(u);
19+
export const parse = (u: string | URL) => new URL(u);
2020

2121
/**
2222
* Returns resolved target URL relative to a base URL in a manner similar to that of a Web browser resolving an anchor tag HREF.
2323
*
2424
* @returns
2525
*/
26-
export function resolve(from: any, to: any) {
27-
const resolvedUrl = new URL(to, new URL(from, "resolve://"));
26+
export function resolve(from: string, to: string) {
27+
const fromUrl = new URL(convertPathToPosix(from), "resolve://");
28+
const resolvedUrl = new URL(convertPathToPosix(to), fromUrl);
2829
if (resolvedUrl.protocol === "resolve:") {
2930
// `from` is a relative URL.
3031
const { pathname, search, hash } = resolvedUrl;
@@ -279,7 +280,7 @@ export function safePointerToPath(pointer: any) {
279280
});
280281
}
281282

282-
export function relative(from: string | undefined, to: string | undefined) {
283+
export function relative(from: string, to: string) {
283284
if (!isFileSystemPath(from) || !isFileSystemPath(to)) {
284285
return resolve(from, to);
285286
}

test/specs/callbacks.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ describe("Callback & Promise syntax", () => {
7373
return async function () {
7474
try {
7575
await $RefParser[method](path.rel("test/specs/invalid/invalid.yaml"));
76-
helper.shouldNotGetCalled;
76+
helper.shouldNotGetCalled();
7777
} catch (err: any) {
7878
expect(err).to.be.an.instanceOf(ParserError);
7979
}

test/specs/http.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// <reference lib="dom" />
12
import { describe, it, beforeEach } from "vitest";
23
import $RefParser from "../../lib/index.js";
34

test/specs/relative-path/root.spec.ts

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { afterAll, beforeAll, describe, it } from "vitest";
2+
import $RefParser, { JSONParserError } from "../../../lib/index.js";
3+
import path from "../../utils/path.js";
4+
5+
import { expect, vi } from "vitest";
6+
import helper from "../../utils/helper";
7+
8+
describe.skipIf(process.env.BROWSER)("Schemas with imports in relative and absolute locations work", () => {
9+
describe("Schemas with relative imports that should be resolved from the root", () => {
10+
beforeAll(() => {
11+
vi.spyOn(process, "cwd").mockImplementation(() => {
12+
return __dirname;
13+
});
14+
});
15+
afterAll(() => {
16+
vi.restoreAllMocks();
17+
});
18+
it("should not parse successfully when set to resolve relative (default)", async () => {
19+
const parser = new $RefParser();
20+
try {
21+
await parser.dereference(path.rel("schemas/accountList.json"));
22+
helper.shouldNotGetCalled();
23+
} catch (err) {
24+
expect(err).to.be.an.instanceOf(JSONParserError);
25+
}
26+
});
27+
28+
it("should parse successfully when set to resolve relative (default)", async () => {
29+
const parser = new $RefParser();
30+
const schema = await parser.dereference(path.rel("schemas/accountList.json"), {
31+
dereference: { externalReferenceResolution: "root" },
32+
});
33+
expect(schema).to.eql(parser.schema);
34+
});
35+
});
36+
37+
describe("Schemas with relative imports that should be resolved relatively", () => {
38+
beforeAll(() => {
39+
vi.spyOn(process, "cwd").mockImplementation(() => {
40+
return __dirname;
41+
});
42+
});
43+
afterAll(() => {
44+
vi.restoreAllMocks();
45+
});
46+
it("should parse successfully when set to resolve relative (default)", async () => {
47+
const parser = new $RefParser();
48+
const schema = await parser.dereference(path.rel("schemas-relative/accountList.json"), {
49+
dereference: { externalReferenceResolution: "relative" },
50+
});
51+
expect(schema).to.eql(parser.schema);
52+
});
53+
54+
it("should not parse successfully when set to resolve relative (default)", async () => {
55+
const parser = new $RefParser();
56+
try {
57+
await parser.dereference(path.rel("schemas-relative/accountList.json"), {
58+
dereference: { externalReferenceResolution: "root" },
59+
});
60+
helper.shouldNotGetCalled();
61+
} catch (err) {
62+
expect(err).to.be.an.instanceOf(JSONParserError);
63+
}
64+
});
65+
});
66+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"title": "Account",
4+
"$id": "account.json",
5+
"type": "object",
6+
"description": "An account.",
7+
"additionalProperties": false,
8+
"required": [
9+
"accountOwner",
10+
"accountId"
11+
],
12+
"properties": {
13+
"accountOwner": {
14+
"$ref": "user.json"
15+
},
16+
"accountId": {
17+
"$id": "#/properties/accountId",
18+
"type": "string",
19+
"description": "An explanation about the purpose of this instance.",
20+
"default": "",
21+
"examples": [
22+
"186383568343"
23+
]
24+
}
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"title": "AccountList",
4+
"$id": "accountList.json",
5+
"type": "object",
6+
"description": "An account list result.",
7+
"additionalProperties": false,
8+
"required": [
9+
"data",
10+
"total",
11+
"pages"
12+
],
13+
"properties": {
14+
"data": {
15+
"type": "array",
16+
"default": [],
17+
"items": {
18+
"$ref": "account.json"
19+
}
20+
},
21+
"total": {
22+
"type": "integer",
23+
"description": "The number of total items found."
24+
},
25+
"pages": {
26+
"type": "integer",
27+
"description": "The number of pages found"
28+
}
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "user.json",
4+
"type": "object",
5+
"title": "User",
6+
"description": "A User",
7+
"default": {},
8+
"additionalProperties": false,
9+
"required": [
10+
"id",
11+
"name",
12+
"email"
13+
],
14+
"properties": {
15+
"id": {
16+
"$id": "#/user/properties/id",
17+
"type": "string",
18+
"description": "The users id.",
19+
"default": ""
20+
},
21+
"name": {
22+
"$id": "#/user/properties/name",
23+
"type": "string",
24+
"description": "The users full name with id.",
25+
"default": ""
26+
},
27+
"email": {
28+
"$id": "#/user/properties/email",
29+
"type": "string",
30+
"description": "The users email address.",
31+
"default": ""
32+
}
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"title": "Account",
4+
"type": "object",
5+
"description": "An account.",
6+
"additionalProperties": false,
7+
"required": [
8+
"accountOwner",
9+
"accountId"
10+
],
11+
"properties": {
12+
"accountOwner": {
13+
"$ref": "schemas/user.json"
14+
},
15+
"accountId": {
16+
"$id": "#/properties/accountId",
17+
"type": "string",
18+
"description": "An explanation about the purpose of this instance.",
19+
"default": "",
20+
"examples": [
21+
"186383568343"
22+
]
23+
}
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"title": "AccountList",
4+
"type": "object",
5+
"description": "An account list result.",
6+
"additionalProperties": false,
7+
"required": [
8+
"data",
9+
"total",
10+
"pages"
11+
],
12+
"properties": {
13+
"data": {
14+
"type": "array",
15+
"default": [],
16+
"items": {
17+
"$ref": "schemas/account.json"
18+
}
19+
},
20+
"total": {
21+
"type": "integer",
22+
"description": "The number of total items found."
23+
},
24+
"pages": {
25+
"type": "integer",
26+
"description": "The number of pages found"
27+
}
28+
}
29+
}
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"type": "object",
4+
"title": "User",
5+
"description": "A User",
6+
"default": {},
7+
"additionalProperties": false,
8+
"required": [
9+
"id",
10+
"name",
11+
"email"
12+
],
13+
"properties": {
14+
"id": {
15+
"$id": "#/user/properties/id",
16+
"type": "string",
17+
"description": "The users id.",
18+
"default": ""
19+
},
20+
"name": {
21+
"$id": "#/user/properties/name",
22+
"type": "string",
23+
"description": "The users full name with id.",
24+
"default": ""
25+
},
26+
"email": {
27+
"$id": "#/user/properties/email",
28+
"type": "string",
29+
"description": "The users email address.",
30+
"default": ""
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)