From 54dbaa564d7a7d5dbe3cf5889bb31616eba2c78f Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Wed, 22 Jan 2025 14:41:35 -0800 Subject: [PATCH] feat: creation of a new `onCircular` hook for accumulating circular refs --- docs/options.md | 4 +++ lib/dereference.ts | 5 ++- lib/options.ts | 7 ++++ .../circular-extended.spec.ts | 16 ++++----- .../circular-external.spec.ts | 2 +- test/specs/circular/circular.spec.ts | 34 ++++++++++++++----- .../specs/deep-circular/deep-circular.spec.ts | 2 +- 7 files changed, 51 insertions(+), 19 deletions(-) diff --git a/docs/options.md b/docs/options.md index f2e9492b..4dfc3107 100644 --- a/docs/options.md +++ b/docs/options.md @@ -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 @@ -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.

If set to `false`, then a `ReferenceError` will be thrown if the schema contains any circular references.

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. | diff --git a/lib/dereference.ts b/lib/dereference.ts index b56b58bb..886b2cbe 100644 --- a/lib/dereference.ts +++ b/lib/dereference.ts @@ -272,7 +272,8 @@ function dereference$Ref { 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"), { @@ -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 { @@ -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( @@ -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 { @@ -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( @@ -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 { @@ -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( @@ -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 { diff --git a/test/specs/circular-external/circular-external.spec.ts b/test/specs/circular-external/circular-external.spec.ts index 0f1b5379..4cdf62f6 100644 --- a/test/specs/circular-external/circular-external.spec.ts +++ b/test/specs/circular-external/circular-external.spec.ts @@ -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 { diff --git a/test/specs/circular/circular.spec.ts b/test/specs/circular/circular.spec.ts index 7c416287..284742bd 100644 --- a/test/specs/circular/circular.spec.ts +++ b/test/specs/circular/circular.spec.ts @@ -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"), { @@ -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 { @@ -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")); @@ -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"), { @@ -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 { @@ -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"), { @@ -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 { @@ -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"), { @@ -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 { diff --git a/test/specs/deep-circular/deep-circular.spec.ts b/test/specs/deep-circular/deep-circular.spec.ts index ec432869..d23e50b8 100644 --- a/test/specs/deep-circular/deep-circular.spec.ts +++ b/test/specs/deep-circular/deep-circular.spec.ts @@ -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 {