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..f90da41d --- /dev/null +++ b/test/specs/bundle-callback/bundle-callback.spec.ts @@ -0,0 +1,64 @@ +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(JSON.parse(JSON.stringify({ path, value, parent, parentPropName }))); + }, + }, + } as Options; + await parser.bundle(schema, options); + + expect(calls).to.deep.equal([ + { + path: "#/definitions/b", + value: { $ref: "#/definitions/b" }, + parent: { + a: { + $ref: "#/definitions/b", + }, + b: { + $ref: "#/definitions/a", + }, + }, + parentPropName: "a", + }, + { + path: "#/definitions/a", + value: { $ref: "#/definitions/a" }, + parent: { + a: { + $ref: "#/definitions/b", + }, + b: { + $ref: "#/definitions/a", + }, + }, + parentPropName: "b", + }, + { + 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"