diff --git a/lib/pointer.ts b/lib/pointer.ts index b2a1e0d2..a7b3105e 100644 --- a/lib/pointer.ts +++ b/lib/pointer.ts @@ -83,12 +83,12 @@ class Pointer { this.value = unwrapOrThrow(obj); for (let i = 0; i < tokens.length; i++) { - if (resolveIf$Ref(this, options)) { + if (resolveIf$Ref(this, options, pathFromRoot)) { // The $ref path has changed, so append the remaining tokens to the path this.path = Pointer.join(this.path, tokens.slice(i)); } - if (typeof this.value === "object" && this.value !== null && "$ref" in this.value) { + if (typeof this.value === "object" && this.value !== null && !isRootPath(pathFromRoot) && "$ref" in this.value) { return this; } @@ -103,7 +103,7 @@ class Pointer { // Resolve the final value if (!this.value || (this.value.$ref && url.resolve(this.path, this.value.$ref) !== pathFromRoot)) { - resolveIf$Ref(this, options); + resolveIf$Ref(this, options, pathFromRoot); } return this; @@ -224,15 +224,16 @@ class Pointer { * * @param pointer * @param options + * @param [pathFromRoot] - the path of place that initiated resolving * @returns - Returns `true` if the resolution path changed */ -function resolveIf$Ref(pointer: any, options: any) { +function resolveIf$Ref(pointer: any, options: any, pathFromRoot?: any) { // Is the value a JSON reference? (and allowed?) if ($Ref.isAllowed$Ref(pointer.value, options)) { const $refPath = url.resolve(pointer.path, pointer.value.$ref); - if ($refPath === pointer.path) { + if ($refPath === pointer.path && !isRootPath(pathFromRoot)) { // The value is a reference to itself, so there's nothing to do. pointer.circular = true; } else { @@ -294,3 +295,7 @@ function unwrapOrThrow(value: any) { return value; } + +function isRootPath(pathFromRoot: any): boolean { + return typeof pathFromRoot == "string" && Pointer.parse(pathFromRoot).length == 0; +} diff --git a/test/specs/internal-root-ref/bundled.ts b/test/specs/internal-root-ref/bundled.ts new file mode 100644 index 00000000..6e9ff7dc --- /dev/null +++ b/test/specs/internal-root-ref/bundled.ts @@ -0,0 +1,14 @@ +export default { + $ref: "#/definitions/user", + definitions: { + user: { + properties: { + userId: { + type: "integer", + }, + }, + required: ["userId"], + type: "object", + }, + }, +}; diff --git a/test/specs/internal-root-ref/dereferenced.ts b/test/specs/internal-root-ref/dereferenced.ts new file mode 100644 index 00000000..f8792463 --- /dev/null +++ b/test/specs/internal-root-ref/dereferenced.ts @@ -0,0 +1,20 @@ +export default { + definitions: { + user: { + type: "object", + properties: { + userId: { + type: "integer", + }, + }, + required: ["userId"], + }, + }, + type: "object", + properties: { + userId: { + type: "integer", + }, + }, + required: ["userId"], +}; diff --git a/test/specs/internal-root-ref/internal-root-ref.spec.ts b/test/specs/internal-root-ref/internal-root-ref.spec.ts new file mode 100644 index 00000000..e7897074 --- /dev/null +++ b/test/specs/internal-root-ref/internal-root-ref.spec.ts @@ -0,0 +1,46 @@ +import { describe, it } from "vitest"; +import $RefParser from "../../../lib/index.js"; +import helper from "../../utils/helper.js"; +import path from "../../utils/path.js"; +import parsedSchema from "./parsed.js"; +import dereferencedSchema from "./dereferenced.js"; +import bundledSchema from "./bundled.js"; + +import { expect } from "vitest"; + +describe("Schema with $ref at root level", () => { + it("should parse successfully", async () => { + const parser = new $RefParser(); + const schema = await parser.parse(path.rel("test/specs/internal-root-ref/internal-root-ref.yaml")); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(parsedSchema); + expect(parser.$refs.paths()).to.deep.equal([path.abs("test/specs/internal-root-ref/internal-root-ref.yaml")]); + }); + + it( + "should resolve successfully", + helper.testResolve( + path.rel("test/specs/internal-root-ref/internal-root-ref.yaml"), + path.abs("test/specs/internal-root-ref/internal-root-ref.yaml"), + parsedSchema, + ), + ); + + it("should dereference successfully", async () => { + const parser = new $RefParser(); + const schema = await parser.dereference(path.rel("test/specs/internal-root-ref/internal-root-ref.yaml")); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(dereferencedSchema); + // Reference equality + // @ts-expect-error TS(2532): Object is possibly 'undefined'. + expect(schema.properties.userId).to.equal(schema.definitions.user.properties.userId); + expect(parser.$refs.circular).to.equal(false); + }); + + it("should bundle successfully", async () => { + const parser = new $RefParser(); + const schema = await parser.bundle(path.rel("test/specs/internal-root-ref/internal-root-ref.yaml")); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(bundledSchema); + }); +}); diff --git a/test/specs/internal-root-ref/internal-root-ref.yaml b/test/specs/internal-root-ref/internal-root-ref.yaml new file mode 100644 index 00000000..3b41be66 --- /dev/null +++ b/test/specs/internal-root-ref/internal-root-ref.yaml @@ -0,0 +1,9 @@ +$ref: "#/definitions/user" +definitions: + user: + type: object + properties: + userId: + type: integer + required: + - userId diff --git a/test/specs/internal-root-ref/parsed.ts b/test/specs/internal-root-ref/parsed.ts new file mode 100644 index 00000000..6e9ff7dc --- /dev/null +++ b/test/specs/internal-root-ref/parsed.ts @@ -0,0 +1,14 @@ +export default { + $ref: "#/definitions/user", + definitions: { + user: { + properties: { + userId: { + type: "integer", + }, + }, + required: ["userId"], + type: "object", + }, + }, +};