Skip to content
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

feat: creation of a new onCircular hook for accumulating circular refs #366

Merged
merged 2 commits into from
Jan 23, 2025
Merged
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
4 changes: 4 additions & 0 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ $RefParser.dereference("my-schema.yaml", {
excludedPathMatcher: (
path, // Skip dereferencing content under any 'example' key
) => path.includes("/example/"),
onCircular: (
path, // Callback invoked during circular $ref detection
) => console.log(path),
onDereference: (
path,
value, // Callback invoked during dereferencing
Expand Down Expand Up @@ -78,4 +81,5 @@ The `dereference` options control how JSON Schema $Ref Parser will dereference `
| :-------------------- | :--------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `circular` | `boolean` or `"ignore"` | Determines whether [circular `$ref` pointers](README.md#circular-refs) are handled.<br><br>If set to `false`, then a `ReferenceError` will be thrown if the schema contains any circular references.<br><br> If set to `"ignore"`, then circular references will simply be ignored. No error will be thrown, but the [`$Refs.circular`](refs.md#circular) property will still be set to `true`. |
| `excludedPathMatcher` | `(string) => boolean` | A function, called for each path, which can return true to stop this path and all subpaths from being dereferenced further. This is useful in schemas where some subpaths contain literal `$ref` keys that should not be dereferenced. |
| `onCircular` | `(string) => void` | A function, called immediately after detecting a circular `$ref` with the circular `$ref` in question. |
| `onDereference` | `(string, JSONSchemaObjectType, JSONSchemaObjectType, string) => void` | A function, called immediately after dereferencing, with: the resolved JSON Schema value, the `$ref` being dereferenced, the object holding the dereferenced prop, the dereferenced prop name. |
5 changes: 4 additions & 1 deletion lib/dereference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<

/**
* Called when a circular reference is found.
* It sets the {@link $Refs#circular} flag, and throws an error if options.dereference.circular is false.
* It sets the {@link $Refs#circular} flag, executes the options.dereference.onCircular callback,
* and throws an error if options.dereference.circular is false.
*
* @param keyPath - The JSON Reference path of the circular reference
* @param $refs
Expand All @@ -281,6 +282,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
*/
function foundCircularReference(keyPath: any, $refs: any, options: any) {
$refs.circular = true;
options?.dereference?.onCircular?.(keyPath);

if (!options.dereference.circular) {
throw ono.reference(`Circular $ref pointer found at ${keyPath}`);
}
Expand Down
7 changes: 7 additions & 0 deletions lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ export interface DereferenceOptions {
*/
excludedPathMatcher?(path: string): boolean;

/**
* Callback invoked during circular reference detection.
*
* @argument {string} path - The path that is circular (ie. the `$ref` string)
*/
onCircular?(path: string): void;

/**
* Callback invoked during dereferencing.
*
Expand Down
16 changes: 8 additions & 8 deletions test/specs/circular-extended/circular-extended.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe("Schema with circular $refs that extend each other", () => {
expect(parser.$refs.circular).to.equal(true);
});

it('should not dereference circular $refs if "options.$refs.circular" is "ignore"', async () => {
it('should not dereference circular $refs if "options.dereference.circular" is "ignore"', async () => {
const parser = new $RefParser();

const schema = await parser.dereference(path.rel("test/specs/circular-extended/circular-extended-self.yaml"), {
Expand All @@ -55,7 +55,7 @@ describe("Schema with circular $refs that extend each other", () => {
expect(parser.$refs.circular).to.equal(true);
});

it('should throw an error if "options.$refs.circular" is false', async () => {
it('should throw an error if "options.dereference.circular" is false', async () => {
const parser = new $RefParser();

try {
Expand Down Expand Up @@ -130,7 +130,7 @@ describe("Schema with circular $refs that extend each other", () => {
expect(schema.definitions.person.properties.pet.properties).to.equal(schema.definitions.pet.properties);
});

it('should not dereference circular $refs if "options.$refs.circular" is "ignore"', async () => {
it('should not dereference circular $refs if "options.dereference.circular" is "ignore"', async () => {
const parser = new $RefParser();

const schema = await parser.dereference(
Expand All @@ -145,7 +145,7 @@ describe("Schema with circular $refs that extend each other", () => {
expect(parser.$refs.circular).to.equal(true);
});

it('should throw an error if "options.$refs.circular" is false', async () => {
it('should throw an error if "options.dereference.circular" is false', async () => {
const parser = new $RefParser();

try {
Expand Down Expand Up @@ -232,7 +232,7 @@ describe("Schema with circular $refs that extend each other", () => {
expect(schema.definitions.child.properties.pet.properties).to.equal(schema.definitions.pet.properties);
});

it('should not dereference circular $refs if "options.$refs.circular" is "ignore"', async () => {
it('should not dereference circular $refs if "options.dereference.circular" is "ignore"', async () => {
const parser = new $RefParser();

const schema = await parser.dereference(
Expand All @@ -247,7 +247,7 @@ describe("Schema with circular $refs that extend each other", () => {
expect(parser.$refs.circular).to.equal(true);
});

it('should throw an error if "options.$refs.circular" is false', async () => {
it('should throw an error if "options.dereference.circular" is false', async () => {
const parser = new $RefParser();

try {
Expand Down Expand Up @@ -335,7 +335,7 @@ describe("Schema with circular $refs that extend each other", () => {
expect(schema.definitions.pet.properties).to.equal(schema.definitions.child.properties.pet.properties);
});

it('should not dereference circular $refs if "options.$refs.circular" is "ignore"', async () => {
it('should not dereference circular $refs if "options.dereference.circular" is "ignore"', async () => {
const parser = new $RefParser();

const schema = await parser.dereference(
Expand All @@ -348,7 +348,7 @@ describe("Schema with circular $refs that extend each other", () => {
expect(parser.$refs.circular).to.equal(true);
});

it('should throw an error if "options.$refs.circular" is false', async () => {
it('should throw an error if "options.dereference.circular" is false', async () => {
const parser = new $RefParser();

try {
Expand Down
2 changes: 1 addition & 1 deletion test/specs/circular-external/circular-external.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe("Schema with circular (recursive) external $refs", () => {
expect(schema.definitions.child.properties.parents.items).to.equal(schema.definitions.parent);
});

it('should throw an error if "options.$refs.circular" is false', async () => {
it('should throw an error if "options.dereference.circular" is false', async () => {
const parser = new $RefParser();

try {
Expand Down
34 changes: 26 additions & 8 deletions test/specs/circular/circular.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe("Schema with circular (recursive) $refs", () => {
expect(schema.definitions.child.properties.pet).to.equal(schema.definitions.pet);
});

it('should produce the same results if "options.$refs.circular" is "ignore"', async () => {
it('should produce the same results if "options.dereference.circular" is "ignore"', async () => {
const parser = new $RefParser();

const schema = await parser.dereference(path.rel("test/specs/circular/circular-self.yaml"), {
Expand All @@ -66,7 +66,7 @@ describe("Schema with circular (recursive) $refs", () => {
expect(parser.$refs.circular).to.equal(true);
});

it('should throw an error if "options.$refs.circular" is false', async () => {
it('should throw an error if "options.dereference.circular" is false', async () => {
const parser = new $RefParser();

try {
Expand All @@ -87,6 +87,24 @@ describe("Schema with circular (recursive) $refs", () => {
}
});

it("should call onCircular if `options.dereference.onCircular` is present", async () => {
const parser = new $RefParser();

const circularRefs: string[] = [];
const schema = await parser.dereference(path.rel("test/specs/circular/circular-self.yaml"), {
dereference: {
onCircular(path: string) {
circularRefs.push(path);
},
},
});
expect(schema).to.equal(parser.schema);
expect(schema).to.deep.equal(dereferencedSchema.self);
// The "circular" flag should be set
expect(parser.$refs.circular).to.equal(true);
expect(circularRefs).to.have.length(1);
});

it("should bundle successfully", async () => {
const parser = new $RefParser();
const schema = await parser.bundle(path.rel("test/specs/circular/circular-self.yaml"));
Expand Down Expand Up @@ -149,7 +167,7 @@ describe("Schema with circular (recursive) $refs", () => {
expect(schema.definitions.person.properties.pet).to.equal(schema.definitions.pet);
});

it('should not dereference circular $refs if "options.$refs.circular" is "ignore"', async () => {
it('should not dereference circular $refs if "options.dereference.circular" is "ignore"', async () => {
const parser = new $RefParser();

const schema = await parser.dereference(path.rel("test/specs/circular/circular-ancestor.yaml"), {
Expand All @@ -164,7 +182,7 @@ describe("Schema with circular (recursive) $refs", () => {
expect(schema.definitions.person.properties.pet).to.equal(schema.definitions.pet);
});

it('should throw an error if "options.$refs.circular" is false', async () => {
it('should throw an error if "options.dereference.circular" is false', async () => {
const parser = new $RefParser();

try {
Expand Down Expand Up @@ -247,7 +265,7 @@ describe("Schema with circular (recursive) $refs", () => {
expect(schema.definitions.child.properties.parents.items).to.equal(schema.definitions.parent);
});

it('should not dereference circular $refs if "options.$refs.circular" is "ignore"', async () => {
it('should not dereference circular $refs if "options.dereference.circular" is "ignore"', async () => {
const parser = new $RefParser();

const schema = await parser.dereference(path.rel("test/specs/circular/circular-indirect.yaml"), {
Expand All @@ -262,7 +280,7 @@ describe("Schema with circular (recursive) $refs", () => {
expect(schema.definitions.child.properties.pet).to.equal(schema.definitions.pet);
});

it('should throw an error if "options.$refs.circular" is false', async () => {
it('should throw an error if "options.dereference.circular" is false', async () => {
const parser = new $RefParser();

try {
Expand Down Expand Up @@ -347,7 +365,7 @@ describe("Schema with circular (recursive) $refs", () => {
expect(schema.definitions.child.properties.children.items).to.equal(schema.definitions.child);
});

it('should not dereference circular $refs if "options.$refs.circular" is "ignore"', async () => {
it('should not dereference circular $refs if "options.dereference.circular" is "ignore"', async () => {
const parser = new $RefParser();

const schema = await parser.dereference(path.rel("test/specs/circular/circular-indirect-ancestor.yaml"), {
Expand All @@ -362,7 +380,7 @@ describe("Schema with circular (recursive) $refs", () => {
expect(schema.definitions.child.properties.pet).to.equal(schema.definitions.pet);
});

it('should throw an error if "options.$refs.circular" is false', async () => {
it('should throw an error if "options.dereference.circular" is false', async () => {
const parser = new $RefParser();

try {
Expand Down
2 changes: 1 addition & 1 deletion test/specs/deep-circular/deep-circular.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe("Schema with deeply-nested circular $refs", () => {
.to.equal(schema.properties.level1.properties.level2.properties.level3.properties.level4.properties.name);
});

it('should throw an error if "options.$refs.circular" is false', async () => {
it('should throw an error if "options.dereference.circular" is false', async () => {
const parser = new $RefParser();

try {
Expand Down
Loading