Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add reference resolution option to allow root level dereferencing #305

Merged
merged 5 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/dereference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ function dereference$Ref(
) {
// console.log('Dereferencing $ref pointer "%s" at %s', $ref.$ref, path);

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

const cache = dereferencedCache.get($refPath);
if (cache) {
Expand Down
7 changes: 7 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ export class $RefParser {
if (url.isFileSystemPath(args.path)) {
args.path = url.fromFileSystemPath(args.path);
pathType = "file";
} else if (!args.path && args.schema && args.schema.$id) {
// when schema id has defined an URL should use that hostname to request the references,
// instead of using the current page URL
const params = url.parse(args.schema.$id);
const port = params.protocol === "https:" ? 443 : 80;

args.path = `${params.protocol}//${params.hostname}:${port}`;
}

// Resolve the absolute path of the schema
Expand Down
10 changes: 9 additions & 1 deletion lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ interface $RefParserOptions {
* @argument {JSONSchemaObject} object The JSON-Schema that the `$ref` resolved to.
*/
onDereference?(path: string, value: JSONSchemaObject): void;

/**
* Whether a reference should resolve relative to its directory/path, or from the cwd
*
* Default: `relative`
*/
externalReferenceResolution?: "relative" | "root";
};
}

Expand Down Expand Up @@ -149,8 +156,9 @@ const getDefaults = () => {
* @type {function}
*/
excludedPathMatcher: () => false,
referenceResolution: "relative",
},
};
} as $RefParserOptions;
return cloneDeep(defaults);
};

Expand Down
6 changes: 3 additions & 3 deletions lib/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default class $Refs {
* @param value The value to assign. Can be anything (object, string, number, etc.)
*/
set(path: any, value: JSONSchema4Type | JSONSchema6Type | JSONSchema7Type) {
const absPath = url.resolve(this._root$Ref.path, path);
const absPath = url.resolve(this._root$Ref.path!, path);
const withoutHash = url.stripHash(absPath);
const $ref = this._$refs[withoutHash];

Expand All @@ -113,7 +113,7 @@ export default class $Refs {
* @protected
*/
_get$Ref(path: any) {
path = url.resolve(this._root$Ref.path, path);
path = url.resolve(this._root$Ref.path!, path);
const withoutHash = url.stripHash(path);
return this._$refs[withoutHash];
}
Expand Down Expand Up @@ -145,7 +145,7 @@ export default class $Refs {
* @protected
*/
_resolve(path: string, pathFromRoot: string, options?: any) {
const absPath = url.resolve(this._root$Ref.path, path);
const absPath = url.resolve(this._root$Ref.path!, path);
const withoutHash = url.stripHash(absPath);
const $ref = this._$refs[withoutHash];

Expand Down
5 changes: 2 additions & 3 deletions lib/resolve-external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,8 @@ function crawl(
* including nested references that are contained in externally-referenced files.
*/
async function resolve$Ref($ref: JSONSchema, path: string, $refs: $Refs, options: Options) {
// console.log('Resolving $ref pointer "%s" at %s', $ref.$ref, path);

const resolvedPath = url.resolve(path, $ref.$ref);
const shouldResolveOnCwd = options.dereference.externalReferenceResolution === "root";
const resolvedPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, $ref.$ref!);
const withoutHash = url.stripHash(resolvedPath);

// $ref.$ref = url.relative($refs._root$Ref.path, resolvedPath);
Expand Down
9 changes: 5 additions & 4 deletions lib/util/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ const urlEncodePatterns = [/\?/g, "%3F", /#/g, "%23"];
// RegExp patterns to URL-decode special characters for local filesystem paths
const urlDecodePatterns = [/%23/g, "#", /%24/g, "$", /%26/g, "&", /%2C/g, ",", /%40/g, "@"];

export const parse = (u: any) => new URL(u);
export const parse = (u: string | URL) => new URL(u);

/**
* Returns resolved target URL relative to a base URL in a manner similar to that of a Web browser resolving an anchor tag HREF.
*
* @returns
*/
export function resolve(from: any, to: any) {
const resolvedUrl = new URL(to, new URL(from, "resolve:https://"));
export function resolve(from: string, to: string) {
const fromUrl = new URL(convertPathToPosix(from), "resolve:https://");
const resolvedUrl = new URL(convertPathToPosix(to), fromUrl);
if (resolvedUrl.protocol === "resolve:") {
// `from` is a relative URL.
const { pathname, search, hash } = resolvedUrl;
Expand Down Expand Up @@ -279,7 +280,7 @@ export function safePointerToPath(pointer: any) {
});
}

export function relative(from: string | undefined, to: string | undefined) {
export function relative(from: string, to: string) {
if (!isFileSystemPath(from) || !isFileSystemPath(to)) {
return resolve(from, to);
}
Expand Down
2 changes: 1 addition & 1 deletion test/specs/callbacks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe("Callback & Promise syntax", () => {
return async function () {
try {
await $RefParser[method](path.rel("test/specs/invalid/invalid.yaml"));
helper.shouldNotGetCalled;
helper.shouldNotGetCalled();
} catch (err: any) {
expect(err).to.be.an.instanceOf(ParserError);
}
Expand Down
1 change: 1 addition & 0 deletions test/specs/http.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference lib="dom" />
import { describe, it, beforeEach } from "vitest";
import $RefParser from "../../lib/index.js";

Expand Down
66 changes: 66 additions & 0 deletions test/specs/relative-path/root.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { afterAll, beforeAll, describe, it } from "vitest";
import $RefParser, { JSONParserError } from "../../../lib/index.js";
import path from "../../utils/path.js";

import { expect, vi } from "vitest";
import helper from "../../utils/helper";

describe.skipIf(process.env.BROWSER)("Schemas with imports in relative and absolute locations work", () => {
describe("Schemas with relative imports that should be resolved from the root", () => {
beforeAll(() => {
vi.spyOn(process, "cwd").mockImplementation(() => {
return __dirname;
});
});
afterAll(() => {
vi.restoreAllMocks();
});
it("should not parse successfully when set to resolve relative (default)", async () => {
const parser = new $RefParser();
try {
await parser.dereference(path.rel("schemas/accountList.json"));
helper.shouldNotGetCalled();
} catch (err) {
expect(err).to.be.an.instanceOf(JSONParserError);
}
});

it("should parse successfully when set to resolve relative (default)", async () => {
const parser = new $RefParser();
const schema = await parser.dereference(path.rel("schemas/accountList.json"), {
dereference: { externalReferenceResolution: "root" },
});
expect(schema).to.eql(parser.schema);
});
});

describe("Schemas with relative imports that should be resolved relatively", () => {
beforeAll(() => {
vi.spyOn(process, "cwd").mockImplementation(() => {
return __dirname;
});
});
afterAll(() => {
vi.restoreAllMocks();
});
it("should parse successfully when set to resolve relative (default)", async () => {
const parser = new $RefParser();
const schema = await parser.dereference(path.rel("schemas-relative/accountList.json"), {
dereference: { externalReferenceResolution: "relative" },
});
expect(schema).to.eql(parser.schema);
});

it("should not parse successfully when set to resolve relative (default)", async () => {
const parser = new $RefParser();
try {
await parser.dereference(path.rel("schemas-relative/accountList.json"), {
dereference: { externalReferenceResolution: "root" },
});
helper.shouldNotGetCalled();
} catch (err) {
expect(err).to.be.an.instanceOf(JSONParserError);
}
});
});
});
26 changes: 26 additions & 0 deletions test/specs/relative-path/schemas-relative/account.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$schema": "http:https://json-schema.org/draft-07/schema",
"title": "Account",
"$id": "account.json",
"type": "object",
"description": "An account.",
"additionalProperties": false,
"required": [
"accountOwner",
"accountId"
],
"properties": {
"accountOwner": {
"$ref": "user.json"
},
"accountId": {
"$id": "#/properties/accountId",
"type": "string",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"186383568343"
]
}
}
}
30 changes: 30 additions & 0 deletions test/specs/relative-path/schemas-relative/accountList.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "http:https://json-schema.org/draft-07/schema",
"title": "AccountList",
"$id": "accountList.json",
"type": "object",
"description": "An account list result.",
"additionalProperties": false,
"required": [
"data",
"total",
"pages"
],
"properties": {
"data": {
"type": "array",
"default": [],
"items": {
"$ref": "account.json"
}
},
"total": {
"type": "integer",
"description": "The number of total items found."
},
"pages": {
"type": "integer",
"description": "The number of pages found"
}
}
}
34 changes: 34 additions & 0 deletions test/specs/relative-path/schemas-relative/user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "http:https://json-schema.org/draft-07/schema",
"$id": "user.json",
"type": "object",
"title": "User",
"description": "A User",
"default": {},
"additionalProperties": false,
"required": [
"id",
"name",
"email"
],
"properties": {
"id": {
"$id": "#/user/properties/id",
"type": "string",
"description": "The users id.",
"default": ""
},
"name": {
"$id": "#/user/properties/name",
"type": "string",
"description": "The users full name with id.",
"default": ""
},
"email": {
"$id": "#/user/properties/email",
"type": "string",
"description": "The users email address.",
"default": ""
}
}
}
25 changes: 25 additions & 0 deletions test/specs/relative-path/schemas/account.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "http:https://json-schema.org/draft-07/schema",
"title": "Account",
"type": "object",
"description": "An account.",
"additionalProperties": false,
"required": [
"accountOwner",
"accountId"
],
"properties": {
"accountOwner": {
"$ref": "schemas/user.json"
},
"accountId": {
"$id": "#/properties/accountId",
"type": "string",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"186383568343"
]
}
}
}
29 changes: 29 additions & 0 deletions test/specs/relative-path/schemas/accountList.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "http:https://json-schema.org/draft-07/schema",
"title": "AccountList",
"type": "object",
"description": "An account list result.",
"additionalProperties": false,
"required": [
"data",
"total",
"pages"
],
"properties": {
"data": {
"type": "array",
"default": [],
"items": {
"$ref": "schemas/account.json"
}
},
"total": {
"type": "integer",
"description": "The number of total items found."
},
"pages": {
"type": "integer",
"description": "The number of pages found"
}
}
}
33 changes: 33 additions & 0 deletions test/specs/relative-path/schemas/user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "http:https://json-schema.org/draft-07/schema",
"type": "object",
"title": "User",
"description": "A User",
"default": {},
"additionalProperties": false,
"required": [
"id",
"name",
"email"
],
"properties": {
"id": {
"$id": "#/user/properties/id",
"type": "string",
"description": "The users id.",
"default": ""
},
"name": {
"$id": "#/user/properties/name",
"type": "string",
"description": "The users full name with id.",
"default": ""
},
"email": {
"$id": "#/user/properties/email",
"type": "string",
"description": "The users email address.",
"default": ""
}
}
}
Loading