Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 8 additions & 1 deletion lib/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,8 +66,10 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = 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 {
Expand Down Expand Up @@ -97,6 +100,10 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
} else {
crawl(obj, key, keyPath, keyPathFromRoot, indirections, inventory, $refs, options);
}

if (value["$ref"]) {
bundleOptions?.onBundle?.(value["$ref"], obj[key], obj as any, key);
}
}
}
}
Expand Down
39 changes: 39 additions & 0 deletions lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ export type DeepPartial<T> = T extends object
[P in keyof T]?: DeepPartial<T[P]>;
}
: 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.
Expand Down Expand Up @@ -107,6 +127,11 @@ export interface $RefParserOptions<S extends object = JSONSchema> {
*/
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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down
75 changes: 75 additions & 0 deletions test/specs/bundle-callback/bundle-callback.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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/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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is missing a comma and closing quote

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",
value: { $ref: "#/definitions/a" },
parent: {
c: {
type: "string",
},
d: {
$ref: "#/definitions/a",
},
},
parentPropName: "d",
},
]);
});
});
12 changes: 12 additions & 0 deletions test/specs/bundle-callback/bundle-callback.yaml
Original file line number Diff line number Diff line change
@@ -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"
Loading