Skip to content

Commit f05ba07

Browse files
authored
Merge pull request #10 from hey-api/fix/improve-ref-pointer-inventory-check
fix: improve inventory check for pointers and add tests for multiple references
2 parents 7b37bdf + 4e36205 commit f05ba07

File tree

5 files changed

+114
-35
lines changed

5 files changed

+114
-35
lines changed

lib/__tests__/bundle.test.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,48 @@
1-
import path from 'node:path';
1+
import path from "path";
22

3-
import { describe, expect, it } from 'vitest';
3+
import { describe, expect, it } from "vitest";
44

5-
import { $RefParser } from '..';
5+
import { $RefParser } from "..";
66

7-
describe('bundle', () => {
8-
it('handles circular reference with description', async () => {
7+
describe("bundle", () => {
8+
it("handles circular reference with description", async () => {
99
const refParser = new $RefParser();
10-
const pathOrUrlOrSchema = path.resolve('lib', '__tests__', 'spec', 'circular-ref-with-description.json');
10+
const pathOrUrlOrSchema = path.resolve("lib", "__tests__", "spec", "circular-ref-with-description.json");
1111
const schema = await refParser.bundle({ pathOrUrlOrSchema });
1212
expect(schema).not.toBeUndefined();
1313
});
14+
15+
it("bundles multiple references to the same file correctly", async () => {
16+
const refParser = new $RefParser();
17+
const pathOrUrlOrSchema = path.resolve("lib", "__tests__", "spec", "multiple-refs.json");
18+
const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any;
19+
20+
// First reference should be fully resolved (no $ref)
21+
expect(schema.paths["/test1/{pathId}"].get.parameters[0].name).toBe("pathId");
22+
expect(schema.paths["/test1/{pathId}"].get.parameters[0].schema.type).toBe("string");
23+
expect(schema.paths["/test1/{pathId}"].get.parameters[0].schema.format).toBe("uuid");
24+
expect(schema.paths["/test1/{pathId}"].get.parameters[0].$ref).toBeUndefined();
25+
26+
// Second reference should be remapped to point to the first reference
27+
expect(schema.paths["/test2/{pathId}"].get.parameters[0].$ref).toBe(
28+
"#/paths/~1test1~1%7BpathId%7D/get/parameters/0",
29+
);
30+
31+
// Both should effectively resolve to the same data
32+
const firstParam = schema.paths["/test1/{pathId}"].get.parameters[0];
33+
const secondParam = schema.paths["/test2/{pathId}"].get.parameters[0];
34+
35+
// The second parameter should resolve to the same data as the first
36+
expect(secondParam.$ref).toBeDefined();
37+
expect(firstParam).toEqual({
38+
name: "pathId",
39+
in: "path",
40+
required: true,
41+
schema: {
42+
type: "string",
43+
format: "uuid",
44+
description: "Unique identifier for the path",
45+
},
46+
});
47+
});
1448
});

lib/__tests__/index.test.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,45 @@
1-
import path from 'node:path';
1+
import path from "node:path";
22

3-
import { describe, expect, it } from 'vitest';
3+
import { describe, expect, it } from "vitest";
44

5-
import { getResolvedInput } from '../index';
5+
import { getResolvedInput } from "../index";
66

7-
describe('getResolvedInput', () => {
8-
it('handles url', async () => {
9-
const pathOrUrlOrSchema = 'https://foo.com';
7+
describe("getResolvedInput", () => {
8+
it("handles url", async () => {
9+
const pathOrUrlOrSchema = "https://foo.com";
1010
const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
11-
expect(resolvedInput.type).toBe('url');
11+
expect(resolvedInput.type).toBe("url");
1212
expect(resolvedInput.schema).toBeUndefined();
13-
expect(resolvedInput.path).toBe('https://foo.com/');
13+
expect(resolvedInput.path).toBe("https://foo.com/");
1414
});
1515

16-
it('handles file', async () => {
17-
const pathOrUrlOrSchema = './path/to/openapi.json';
16+
it("handles file", async () => {
17+
const pathOrUrlOrSchema = "./path/to/openapi.json";
1818
const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
19-
expect(resolvedInput.type).toBe('file');
19+
expect(resolvedInput.type).toBe("file");
2020
expect(resolvedInput.schema).toBeUndefined();
21-
expect(resolvedInput.path).toBe(path.resolve('./path/to/openapi.json'));
21+
expect(path.normalize(resolvedInput.path).toLowerCase()).toBe(
22+
path.normalize(path.resolve("./path/to/openapi.json")).toLowerCase(),
23+
);
2224
});
2325

24-
it('handles raw spec', async () => {
25-
const pathOrUrlOrSchema = {
26+
it("handles raw spec", async () => {
27+
const pathOrUrlOrSchema = {
2628
info: {
27-
version: '1.0.0',
29+
version: "1.0.0",
2830
},
29-
openapi: '3.1.0',
31+
openapi: "3.1.0",
3032
paths: {},
3133
};
3234
const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
33-
expect(resolvedInput.type).toBe('json');
35+
expect(resolvedInput.type).toBe("json");
3436
expect(resolvedInput.schema).toEqual({
3537
info: {
36-
version: '1.0.0',
38+
version: "1.0.0",
3739
},
38-
openapi: '3.1.0',
40+
openapi: "3.1.0",
3941
paths: {},
4042
});
41-
expect(resolvedInput.path).toBe('');
43+
expect(resolvedInput.path).toBe("");
4244
});
4345
});

lib/__tests__/spec/multiple-refs.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"paths": {
3+
"/test1/{pathId}": {
4+
"get": {
5+
"summary": "First endpoint using the same pathId schema",
6+
"parameters": [
7+
{
8+
"$ref": "path-parameter.json#/pathId"
9+
}
10+
],
11+
"responses": {
12+
"200": {
13+
"description": "Test 1 response"
14+
}
15+
}
16+
}
17+
},
18+
"/test2/{pathId}": {
19+
"get": {
20+
"summary": "Second endpoint using the same pathId schema",
21+
"parameters": [
22+
{
23+
"$ref": "path-parameter.json#/pathId"
24+
}
25+
],
26+
"responses": {
27+
"200": {
28+
"description": "Test 2 response"
29+
}
30+
}
31+
}
32+
}
33+
}
34+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"pathId": {
3+
"name": "pathId",
4+
"in": "path",
5+
"required": true,
6+
"schema": {
7+
"type": "string",
8+
"format": "uuid",
9+
"description": "Unique identifier for the path"
10+
}
11+
}
12+
}

lib/bundle.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,10 @@ const inventory$Ref = <S extends object = JSONSchema>({
9494
const extended = $Ref.isExtended$Ref($ref);
9595
indirections += pointer.indirections;
9696

97+
// Check if this exact location (parent + key + pathFromRoot) has already been inventoried
9798
const existingEntry = findInInventory(inventory, $refParent, $refKey);
98-
if (existingEntry) {
99-
// This $Ref has already been inventoried, so we don't need to process it again
99+
if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) {
100+
// This exact location has already been inventoried, so we don't need to process it again
100101
if (depth < existingEntry.depth || indirections < existingEntry.indirections) {
101102
removeFromInventory(inventory, existingEntry);
102103
} else {
@@ -172,7 +173,7 @@ const crawl = <S extends object = JSONSchema>({
172173
pathFromRoot: string;
173174
}) => {
174175
const obj = key === null ? parent : parent[key as keyof typeof parent];
175-
176+
176177
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) {
177178
if ($Ref.isAllowed$Ref(obj)) {
178179
inventory$Ref({
@@ -359,17 +360,13 @@ function removeFromInventory(inventory: InventoryEntry[], entry: any) {
359360
* @param parser
360361
* @param options
361362
*/
362-
export const bundle = (
363-
parser: $RefParser,
364-
options: ParserOptions,
365-
) => {
363+
export const bundle = (parser: $RefParser, options: ParserOptions) => {
366364
// console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
367-
368365
// Build an inventory of all $ref pointers in the JSON Schema
369366
const inventory: InventoryEntry[] = [];
370367
crawl<JSONSchema>({
371368
parent: parser,
372-
key: 'schema',
369+
key: "schema",
373370
path: parser.$refs._root$Ref.path + "#",
374371
pathFromRoot: "#",
375372
indirections: 0,

0 commit comments

Comments
 (0)