Skip to content

Commit 50929bb

Browse files
authored
feat: creation of a new onCircular hook for accumulating circular refs (#366)
* feat: creation of a new `onCircular` hook for accumulating circular refs * fix: removing a test `.only`
1 parent 70626d3 commit 50929bb

File tree

7 files changed

+51
-19
lines changed

7 files changed

+51
-19
lines changed

docs/options.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ $RefParser.dereference("my-schema.yaml", {
3131
excludedPathMatcher: (
3232
path, // Skip dereferencing content under any 'example' key
3333
) => path.includes("/example/"),
34+
onCircular: (
35+
path, // Callback invoked during circular $ref detection
36+
) => console.log(path),
3437
onDereference: (
3538
path,
3639
value, // Callback invoked during dereferencing
@@ -78,4 +81,5 @@ The `dereference` options control how JSON Schema $Ref Parser will dereference `
7881
| :-------------------- | :--------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7982
| `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`. |
8083
| `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. |
84+
| `onCircular` | `(string) => void` | A function, called immediately after detecting a circular `$ref` with the circular `$ref` in question. |
8185
| `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. |

lib/dereference.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
272272

273273
/**
274274
* Called when a circular reference is found.
275-
* It sets the {@link $Refs#circular} flag, and throws an error if options.dereference.circular is false.
275+
* It sets the {@link $Refs#circular} flag, executes the options.dereference.onCircular callback,
276+
* and throws an error if options.dereference.circular is false.
276277
*
277278
* @param keyPath - The JSON Reference path of the circular reference
278279
* @param $refs
@@ -281,6 +282,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
281282
*/
282283
function foundCircularReference(keyPath: any, $refs: any, options: any) {
283284
$refs.circular = true;
285+
options?.dereference?.onCircular?.(keyPath);
286+
284287
if (!options.dereference.circular) {
285288
throw ono.reference(`Circular $ref pointer found at ${keyPath}`);
286289
}

lib/options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ export interface DereferenceOptions {
2929
*/
3030
excludedPathMatcher?(path: string): boolean;
3131

32+
/**
33+
* Callback invoked during circular reference detection.
34+
*
35+
* @argument {string} path - The path that is circular (ie. the `$ref` string)
36+
*/
37+
onCircular?(path: string): void;
38+
3239
/**
3340
* Callback invoked during dereferencing.
3441
*

test/specs/circular-extended/circular-extended.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe("Schema with circular $refs that extend each other", () => {
4343
expect(parser.$refs.circular).to.equal(true);
4444
});
4545

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

4949
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", () => {
5555
expect(parser.$refs.circular).to.equal(true);
5656
});
5757

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

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

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

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

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

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

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

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

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

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

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

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

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

354354
try {

test/specs/circular-external/circular-external.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe("Schema with circular (recursive) external $refs", () => {
5353
expect(schema.definitions.child.properties.parents.items).to.equal(schema.definitions.parent);
5454
});
5555

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

5959
try {

test/specs/circular/circular.spec.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe("Schema with circular (recursive) $refs", () => {
5454
expect(schema.definitions.child.properties.pet).to.equal(schema.definitions.pet);
5555
});
5656

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

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

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

7272
try {
@@ -87,6 +87,24 @@ describe("Schema with circular (recursive) $refs", () => {
8787
}
8888
});
8989

90+
it("should call onCircular if `options.dereference.onCircular` is present", async () => {
91+
const parser = new $RefParser();
92+
93+
const circularRefs: string[] = [];
94+
const schema = await parser.dereference(path.rel("test/specs/circular/circular-self.yaml"), {
95+
dereference: {
96+
onCircular(path: string) {
97+
circularRefs.push(path);
98+
},
99+
},
100+
});
101+
expect(schema).to.equal(parser.schema);
102+
expect(schema).to.deep.equal(dereferencedSchema.self);
103+
// The "circular" flag should be set
104+
expect(parser.$refs.circular).to.equal(true);
105+
expect(circularRefs).to.have.length(1);
106+
});
107+
90108
it("should bundle successfully", async () => {
91109
const parser = new $RefParser();
92110
const schema = await parser.bundle(path.rel("test/specs/circular/circular-self.yaml"));
@@ -149,7 +167,7 @@ describe("Schema with circular (recursive) $refs", () => {
149167
expect(schema.definitions.person.properties.pet).to.equal(schema.definitions.pet);
150168
});
151169

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

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

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

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

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

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

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

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

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

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

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

368386
try {

test/specs/deep-circular/deep-circular.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe("Schema with deeply-nested circular $refs", () => {
5555
.to.equal(schema.properties.level1.properties.level2.properties.level3.properties.level4.properties.name);
5656
});
5757

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

6161
try {

0 commit comments

Comments
 (0)