From d552628157bb7ee1606166a0e07a7da419b2d111 Mon Sep 17 00:00:00 2001 From: Viacheslav Turovskyi Date: Wed, 2 Jul 2025 06:40:34 +0000 Subject: [PATCH 1/5] feat: add `onBundle` callback --- lib/bundle.ts | 9 +++- lib/options.ts | 39 ++++++++++++++ .../bundle-callback/bundle-callback.spec.ts | 51 +++++++++++++++++++ .../bundle-callback/bundle-callback.yaml | 12 +++++ 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 test/specs/bundle-callback/bundle-callback.spec.ts create mode 100644 test/specs/bundle-callback/bundle-callback.yaml diff --git a/lib/bundle.ts b/lib/bundle.ts index 0c9fb7cd..d062efd9 100644 --- a/lib/bundle.ts +++ b/lib/bundle.ts @@ -5,6 +5,7 @@ import type $Refs from "./refs.js"; import type $RefParser from "./index"; import type { ParserOptions } from "./index"; import type { JSONSchema } from "./index"; +import type { BundleOptions } from "./options"; export interface InventoryEntry { $ref: any; @@ -65,8 +66,10 @@ function crawl = Parse options: O, ) { const obj = key === null ? parent : parent[key as keyof typeof parent]; + const bundleOptions = (options.bundle || {}) as BundleOptions; + const isExcludedPath = bundleOptions.excludedPathMatcher || (() => false); - if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) { + if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) { if ($Ref.isAllowed$Ref(obj)) { inventory$Ref(parent, key, path, pathFromRoot, indirections, inventory, $refs, options); } else { @@ -97,6 +100,10 @@ function crawl = Parse } else { crawl(obj, key, keyPath, keyPathFromRoot, indirections, inventory, $refs, options); } + + if (value["$ref"]) { + bundleOptions?.onBundle?.(value["$ref"], obj[key], obj as any, key); + } } } } diff --git a/lib/options.ts b/lib/options.ts index a66b1e00..d6a98ead 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -12,6 +12,26 @@ export type DeepPartial = T extends object [P in keyof T]?: DeepPartial; } : T; + +export interface BundleOptions { + /** + * A function, called for each path, which can return true to stop this path and all + * subpaths from being processed further. This is useful in schemas where some + * subpaths contain literal $ref keys that should not be changed. + */ + excludedPathMatcher?(path: string): boolean; + + /** + * Callback invoked during bundling. + * + * @argument {string} path - The path being processed (ie. the `$ref` string) + * @argument {JSONSchemaObject} value - The JSON-Schema that the `$ref` resolved to + * @argument {JSONSchemaObject} parent - The parent of the processed object + * @argument {string} parentPropName - The prop name of the parent object whose value was processed + */ + onBundle?(path: string, value: JSONSchemaObject, parent?: JSONSchemaObject, parentPropName?: string): void; +} + export interface DereferenceOptions { /** * Determines whether circular `$ref` pointers are handled. @@ -107,6 +127,11 @@ export interface $RefParserOptions { */ continueOnError: boolean; + /** + * The `bundle` options control how JSON Schema `$Ref` Parser will process `$ref` pointers within the JSON schema. + */ + bundle: BundleOptions; + /** * The `dereference` options control how JSON Schema `$Ref` Parser will dereference `$ref` pointers within the JSON schema. */ @@ -168,6 +193,20 @@ export const getJsonSchemaRefParserDefaultOptions = () => { */ continueOnError: false, + /** + * Determines the types of JSON references that are allowed. + */ + bundle: { + /** + * A function, called for each path, which can return true to stop this path and all + * subpaths from being processed further. This is useful in schemas where some + * subpaths contain literal $ref keys that should not be changed. + * + * @type {function} + */ + excludedPathMatcher: () => false, + }, + /** * Determines the types of JSON references that are allowed. */ diff --git a/test/specs/bundle-callback/bundle-callback.spec.ts b/test/specs/bundle-callback/bundle-callback.spec.ts new file mode 100644 index 00000000..5b17c7ec --- /dev/null +++ b/test/specs/bundle-callback/bundle-callback.spec.ts @@ -0,0 +1,51 @@ +import { describe, it } from "vitest"; +import $RefParser from "../../../lib/index.js"; +import pathUtils from "../../utils/path.js"; + +import { expect } from "vitest"; +import type { Options } from "../../../lib/options"; + +describe("Schema with a $ref", () => { + it("should call onBundle", async () => { + const parser = new $RefParser(); + const calls: any = []; + const schema = pathUtils.rel("test/specs/bundle-callback/bundle-callback.yaml"); + const options = { + bundle: { + onBundle(path, value, parent, parentPropName) { + calls.push({ path, value, parent, parentPropName }); + }, + }, + } as Options; + await parser.bundle(schema, options); + + expect(calls).to.deep.equal([ + { + path: "#/definitions/b", + value: { $ref: "#/definitions/a" }, + parent: { + a: { + $ref: "#/definitions/a", + }, + b: { + $ref: "#/definitions/a", + }, + }, + parentPropName: "a", + }, + { + path: "#/definitions/a", + value: { $ref: "#/definitions/a" }, + parent: { + c: { + type: "string", + }, + d: { + $ref: "#/definitions/a", + }, + }, + parentPropName: "d", + }, + ]); + }); +}); diff --git a/test/specs/bundle-callback/bundle-callback.yaml b/test/specs/bundle-callback/bundle-callback.yaml new file mode 100644 index 00000000..f9c40dd9 --- /dev/null +++ b/test/specs/bundle-callback/bundle-callback.yaml @@ -0,0 +1,12 @@ +title: test +type: object +definitions: + a: + $ref: "#/definitions/b" + b: + $ref: "#/definitions/a" +properties: + c: + type: string + d: + $ref: "#/definitions/a" From 5794d18e56089bdc3081222f1699ee6816035322 Mon Sep 17 00:00:00 2001 From: Viacheslav Turovskyi Date: Wed, 2 Jul 2025 10:15:39 +0000 Subject: [PATCH 2/5] chore(tests): fix test for `onBundle` callback --- .../bundle-callback/bundle-callback.spec.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/test/specs/bundle-callback/bundle-callback.spec.ts b/test/specs/bundle-callback/bundle-callback.spec.ts index 5b17c7ec..a581abfc 100644 --- a/test/specs/bundle-callback/bundle-callback.spec.ts +++ b/test/specs/bundle-callback/bundle-callback.spec.ts @@ -21,17 +21,41 @@ describe("Schema with a $ref", () => { expect(calls).to.deep.equal([ { - path: "#/definitions/b", - value: { $ref: "#/definitions/a" }, + path: "#/definitions/a", + value: { $ref: "#/definitions/b" }, parent: { a: { $ref: "#/definitions/a", }, + b: { + $ref: "#/definitions/b", + }, + }, + parentPropName: "a", + }, + { + path: "#/definitions/a", + value: { $ref: "#/definitions/apath: "#/definitions/b", + value: { $ref: "#/definitions/b" }, + parent: { + a: { + $ref: "#/definitions/b", + }, b: { $ref: "#/definitions/a", }, }, parentPropName: "a", + }," }, + parent: { + a: { + $ref: "#/definitions/b", + }, + b: { + $ref: "#/definitions/a", + }, + }, + parentPropName: "b", }, { path: "#/definitions/a", From 6e99154245f183d8e25bd7b2053f8c6f39a001ea Mon Sep 17 00:00:00 2001 From: Viacheslav Turovskyi Date: Thu, 3 Jul 2025 05:38:46 +0000 Subject: [PATCH 3/5] chore(tests): fix test for `onBundle` callback --- .../bundle-callback/bundle-callback.spec.ts | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/test/specs/bundle-callback/bundle-callback.spec.ts b/test/specs/bundle-callback/bundle-callback.spec.ts index a581abfc..352a58dc 100644 --- a/test/specs/bundle-callback/bundle-callback.spec.ts +++ b/test/specs/bundle-callback/bundle-callback.spec.ts @@ -18,35 +18,24 @@ describe("Schema with a $ref", () => { }, } as Options; await parser.bundle(schema, options); - + console.log(calls) expect(calls).to.deep.equal([ { - path: "#/definitions/a", + path: "#/definitions/b", value: { $ref: "#/definitions/b" }, parent: { a: { - $ref: "#/definitions/a", + $ref: "#/definitions/b", }, b: { - $ref: "#/definitions/b", + $ref: "#/definitions/a", }, }, parentPropName: "a", }, { path: "#/definitions/a", - value: { $ref: "#/definitions/apath: "#/definitions/b", - value: { $ref: "#/definitions/b" }, - parent: { - a: { - $ref: "#/definitions/b", - }, - b: { - $ref: "#/definitions/a", - }, - }, - parentPropName: "a", - }," }, + value: { $ref: "#/definitions/a" }, parent: { a: { $ref: "#/definitions/b", From f8d23810150545bdde1f9adc88c610ad6b775b31 Mon Sep 17 00:00:00 2001 From: Viacheslav Turovskyi Date: Thu, 3 Jul 2025 05:59:59 +0000 Subject: [PATCH 4/5] chore(tests): fix test for `onBundle` callback --- test/specs/bundle-callback/bundle-callback.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/specs/bundle-callback/bundle-callback.spec.ts b/test/specs/bundle-callback/bundle-callback.spec.ts index 352a58dc..e4b33b92 100644 --- a/test/specs/bundle-callback/bundle-callback.spec.ts +++ b/test/specs/bundle-callback/bundle-callback.spec.ts @@ -18,7 +18,7 @@ describe("Schema with a $ref", () => { }, } as Options; await parser.bundle(schema, options); - console.log(calls) + expect(calls).to.deep.equal([ { path: "#/definitions/b", From 6c6937dc4a36cab71cc9d9b174cdd6563caa3da9 Mon Sep 17 00:00:00 2001 From: Viacheslav Turovskyi Date: Fri, 4 Jul 2025 03:04:53 +0000 Subject: [PATCH 5/5] chore(tests): fix test for `onBundle` callback --- test/specs/bundle-callback/bundle-callback.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/specs/bundle-callback/bundle-callback.spec.ts b/test/specs/bundle-callback/bundle-callback.spec.ts index e4b33b92..f90da41d 100644 --- a/test/specs/bundle-callback/bundle-callback.spec.ts +++ b/test/specs/bundle-callback/bundle-callback.spec.ts @@ -13,7 +13,7 @@ describe("Schema with a $ref", () => { const options = { bundle: { onBundle(path, value, parent, parentPropName) { - calls.push({ path, value, parent, parentPropName }); + calls.push(JSON.parse(JSON.stringify({ path, value, parent, parentPropName }))); }, }, } as Options;