From a67f38c312839f69866f7da9c7bae149439135fc Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Thu, 27 Feb 2025 12:46:55 -0800 Subject: [PATCH 1/3] fix: allow `$ref` pointers to point to a `null` value --- lib/pointer.ts | 8 ++++++++ .../dereference-null-ref.spec.ts | 14 ++++++++++++++ .../dereference-null-ref/dereference-null-ref.yaml | 8 ++++++++ test/specs/dereference-null-ref/dereferenced.ts | 14 ++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 test/specs/dereference-null-ref/dereference-null-ref.spec.ts create mode 100644 test/specs/dereference-null-ref/dereference-null-ref.yaml create mode 100644 test/specs/dereference-null-ref/dereferenced.ts diff --git a/lib/pointer.ts b/lib/pointer.ts index 028115f5..f1660a54 100644 --- a/lib/pointer.ts +++ b/lib/pointer.ts @@ -121,6 +121,14 @@ class Pointer = Parser continue; } + // If the token we're looking for ended up not containing any slashes but is + // actually instead pointing to an existing `null` value then we should use that + // `null` value. + if (token in this.value && this.value[token] === null) { + this.value = null; + continue; + } + this.value = null; const path = this.$ref.path || ""; diff --git a/test/specs/dereference-null-ref/dereference-null-ref.spec.ts b/test/specs/dereference-null-ref/dereference-null-ref.spec.ts new file mode 100644 index 00000000..ebb7b740 --- /dev/null +++ b/test/specs/dereference-null-ref/dereference-null-ref.spec.ts @@ -0,0 +1,14 @@ +import { describe, it } from "vitest"; +import { expect } from "vitest"; +import $RefParser from "../../../lib/index.js"; +import path from "../../utils/path"; +import dereferenced from "./dereferenced.js"; + +describe("dereferencing a `$ref` that points to a `null` value", () => { + it.only("should dereference successfully", async () => { + const parser = new $RefParser(); + const schema = await parser.dereference(path.rel("test/specs/dereference-null-ref/dereference-null-ref.yaml")); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(dereferenced); + }); +}); diff --git a/test/specs/dereference-null-ref/dereference-null-ref.yaml b/test/specs/dereference-null-ref/dereference-null-ref.yaml new file mode 100644 index 00000000..504de6e1 --- /dev/null +++ b/test/specs/dereference-null-ref/dereference-null-ref.yaml @@ -0,0 +1,8 @@ +components: + examples: + product: + value: + data: + admission: + $ref: "#/components/examples/product/value/data/pas" + pas: null diff --git a/test/specs/dereference-null-ref/dereferenced.ts b/test/specs/dereference-null-ref/dereferenced.ts new file mode 100644 index 00000000..8323d8ba --- /dev/null +++ b/test/specs/dereference-null-ref/dereferenced.ts @@ -0,0 +1,14 @@ +export default { + components: { + examples: { + product: { + value: { + data: { + admission: null, + pas: null, + }, + }, + }, + }, + }, +}; From f84c1ff8336a51f16986e5273413e6f47ffe9212 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Thu, 27 Feb 2025 13:00:22 -0800 Subject: [PATCH 2/3] fix: removing a `.only()` test designation --- test/specs/dereference-null-ref/dereference-null-ref.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/specs/dereference-null-ref/dereference-null-ref.spec.ts b/test/specs/dereference-null-ref/dereference-null-ref.spec.ts index ebb7b740..5d611581 100644 --- a/test/specs/dereference-null-ref/dereference-null-ref.spec.ts +++ b/test/specs/dereference-null-ref/dereference-null-ref.spec.ts @@ -5,7 +5,7 @@ import path from "../../utils/path"; import dereferenced from "./dereferenced.js"; describe("dereferencing a `$ref` that points to a `null` value", () => { - it.only("should dereference successfully", async () => { + it("should dereference successfully", async () => { const parser = new $RefParser(); const schema = await parser.dereference(path.rel("test/specs/dereference-null-ref/dereference-null-ref.yaml")); expect(schema).to.equal(parser.schema); From 768bb57ad6e5f8c89db63edd6848d059192dd418 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Thu, 27 Feb 2025 14:11:37 -0800 Subject: [PATCH 3/3] fix: pr feedback --- lib/pointer.ts | 6 +++++- lib/ref.ts | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/pointer.ts b/lib/pointer.ts index f1660a54..d8180b79 100644 --- a/lib/pointer.ts +++ b/lib/pointer.ts @@ -5,6 +5,8 @@ import * as url from "./util/url.js"; import { JSONParserError, InvalidPointerError, MissingPointerError, isHandledError } from "./util/errors.js"; import type { JSONSchema } from "./types"; +export const nullSymbol = Symbol('null'); + const slashes = /\//g; const tildes = /~/g; const escapedSlash = /~1/g; @@ -125,7 +127,9 @@ class Pointer = Parser // actually instead pointing to an existing `null` value then we should use that // `null` value. if (token in this.value && this.value[token] === null) { - this.value = null; + // We use a `null` symbol for internal tracking to differntiate between a general `null` + // value and our expected `null` value. + this.value = nullSymbol; continue; } diff --git a/lib/ref.ts b/lib/ref.ts index 0ad40b4f..15cb5680 100644 --- a/lib/ref.ts +++ b/lib/ref.ts @@ -1,4 +1,4 @@ -import Pointer from "./pointer.js"; +import Pointer, { nullSymbol } from "./pointer.js"; import type { JSONParserError, MissingPointerError, ParserError, ResolverError } from "./util/errors.js"; import { InvalidPointerError, isHandledError, normalizeError } from "./util/errors.js"; import { safePointerToPath, stripHash, getHash } from "./util/url.js"; @@ -119,7 +119,12 @@ class $Ref = ParserOpt resolve(path: string, options?: O, friendlyPath?: string, pathFromRoot?: string) { const pointer = new Pointer(this, path, friendlyPath); try { - return pointer.resolve(this.value, options, pathFromRoot); + const resolved = pointer.resolve(this.value, options, pathFromRoot); + if (resolved.value === nullSymbol) { + resolved.value = null; + } + + return resolved; } catch (err: any) { if (!options || !options.continueOnError || !isHandledError(err)) { throw err; @@ -148,6 +153,9 @@ class $Ref = ParserOpt set(path: string, value: any) { const pointer = new Pointer(this, path); this.value = pointer.set(this.value, value); + if (this.value === nullSymbol) { + this.value = null; + } } /**