Skip to content

Commit

Permalink
feat: creation of a new onCircular hook for accumulating circular refs
Browse files Browse the repository at this point in the history
  • Loading branch information
erunion committed Jan 22, 2025
1 parent 70626d3 commit 54dbaa5
Show file tree
Hide file tree
Showing 7 changed files with 51 additions and 19 deletions.
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.only("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

0 comments on commit 54dbaa5

Please sign in to comment.