Skip to content

feat: add onBundle callback #391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
64 changes: 64 additions & 0 deletions test/specs/bundle-callback/bundle-callback.spec.ts
Original file line number Diff line number Diff line change
@@ -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",
},
]);
});
});
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"