@hey-api/json-schema-ref-parser 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,14 +3,41 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const node_path_1 = __importDefault(require("node:path"));
6
+ const path_1 = __importDefault(require("path"));
7
7
  const vitest_1 = require("vitest");
8
8
  const __1 = require("..");
9
- (0, vitest_1.describe)('bundle', () => {
10
- (0, vitest_1.it)('handles circular reference with description', async () => {
9
+ (0, vitest_1.describe)("bundle", () => {
10
+ (0, vitest_1.it)("handles circular reference with description", async () => {
11
11
  const refParser = new __1.$RefParser();
12
- const pathOrUrlOrSchema = node_path_1.default.resolve('lib', '__tests__', 'spec', 'circular-ref-with-description.json');
12
+ const pathOrUrlOrSchema = path_1.default.resolve("lib", "__tests__", "spec", "circular-ref-with-description.json");
13
13
  const schema = await refParser.bundle({ pathOrUrlOrSchema });
14
14
  (0, vitest_1.expect)(schema).not.toBeUndefined();
15
15
  });
16
+ (0, vitest_1.it)("bundles multiple references to the same file correctly", async () => {
17
+ const refParser = new __1.$RefParser();
18
+ const pathOrUrlOrSchema = path_1.default.resolve("lib", "__tests__", "spec", "multiple-refs.json");
19
+ const schema = (await refParser.bundle({ pathOrUrlOrSchema }));
20
+ // First reference should be fully resolved (no $ref)
21
+ (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].name).toBe("pathId");
22
+ (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].schema.type).toBe("string");
23
+ (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].schema.format).toBe("uuid");
24
+ (0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].$ref).toBeUndefined();
25
+ // Second reference should be remapped to point to the first reference
26
+ (0, vitest_1.expect)(schema.paths["/test2/{pathId}"].get.parameters[0].$ref).toBe("#/paths/~1test1~1%7BpathId%7D/get/parameters/0");
27
+ // Both should effectively resolve to the same data
28
+ const firstParam = schema.paths["/test1/{pathId}"].get.parameters[0];
29
+ const secondParam = schema.paths["/test2/{pathId}"].get.parameters[0];
30
+ // The second parameter should resolve to the same data as the first
31
+ (0, vitest_1.expect)(secondParam.$ref).toBeDefined();
32
+ (0, vitest_1.expect)(firstParam).toEqual({
33
+ name: "pathId",
34
+ in: "path",
35
+ required: true,
36
+ schema: {
37
+ type: "string",
38
+ format: "uuid",
39
+ description: "Unique identifier for the path",
40
+ },
41
+ });
42
+ });
16
43
  });
@@ -6,38 +6,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const node_path_1 = __importDefault(require("node:path"));
7
7
  const vitest_1 = require("vitest");
8
8
  const index_1 = require("../index");
9
- (0, vitest_1.describe)('getResolvedInput', () => {
10
- (0, vitest_1.it)('handles url', async () => {
11
- const pathOrUrlOrSchema = 'https://foo.com';
9
+ (0, vitest_1.describe)("getResolvedInput", () => {
10
+ (0, vitest_1.it)("handles url", async () => {
11
+ const pathOrUrlOrSchema = "https://foo.com";
12
12
  const resolvedInput = await (0, index_1.getResolvedInput)({ pathOrUrlOrSchema });
13
- (0, vitest_1.expect)(resolvedInput.type).toBe('url');
13
+ (0, vitest_1.expect)(resolvedInput.type).toBe("url");
14
14
  (0, vitest_1.expect)(resolvedInput.schema).toBeUndefined();
15
- (0, vitest_1.expect)(resolvedInput.path).toBe('https://foo.com/');
15
+ (0, vitest_1.expect)(resolvedInput.path).toBe("https://foo.com/");
16
16
  });
17
- (0, vitest_1.it)('handles file', async () => {
18
- const pathOrUrlOrSchema = './path/to/openapi.json';
17
+ (0, vitest_1.it)("handles file", async () => {
18
+ const pathOrUrlOrSchema = "./path/to/openapi.json";
19
19
  const resolvedInput = await (0, index_1.getResolvedInput)({ pathOrUrlOrSchema });
20
- (0, vitest_1.expect)(resolvedInput.type).toBe('file');
20
+ (0, vitest_1.expect)(resolvedInput.type).toBe("file");
21
21
  (0, vitest_1.expect)(resolvedInput.schema).toBeUndefined();
22
- (0, vitest_1.expect)(resolvedInput.path).toBe(node_path_1.default.resolve('./path/to/openapi.json'));
22
+ (0, vitest_1.expect)(node_path_1.default.normalize(resolvedInput.path).toLowerCase()).toBe(node_path_1.default.normalize(node_path_1.default.resolve("./path/to/openapi.json")).toLowerCase());
23
23
  });
24
- (0, vitest_1.it)('handles raw spec', async () => {
24
+ (0, vitest_1.it)("handles raw spec", async () => {
25
25
  const pathOrUrlOrSchema = {
26
26
  info: {
27
- version: '1.0.0',
27
+ version: "1.0.0",
28
28
  },
29
- openapi: '3.1.0',
29
+ openapi: "3.1.0",
30
30
  paths: {},
31
31
  };
32
32
  const resolvedInput = await (0, index_1.getResolvedInput)({ pathOrUrlOrSchema });
33
- (0, vitest_1.expect)(resolvedInput.type).toBe('json');
33
+ (0, vitest_1.expect)(resolvedInput.type).toBe("json");
34
34
  (0, vitest_1.expect)(resolvedInput.schema).toEqual({
35
35
  info: {
36
- version: '1.0.0',
36
+ version: "1.0.0",
37
37
  },
38
- openapi: '3.1.0',
38
+ openapi: "3.1.0",
39
39
  paths: {},
40
40
  });
41
- (0, vitest_1.expect)(resolvedInput.path).toBe('');
41
+ (0, vitest_1.expect)(resolvedInput.path).toBe("");
42
42
  });
43
43
  });
@@ -74,9 +74,10 @@ const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, op
74
74
  const external = file !== $refs._root$Ref.path;
75
75
  const extended = ref_js_1.default.isExtended$Ref($ref);
76
76
  indirections += pointer.indirections;
77
+ // Check if this exact location (parent + key + pathFromRoot) has already been inventoried
77
78
  const existingEntry = findInInventory(inventory, $refParent, $refKey);
78
- if (existingEntry) {
79
- // This $Ref has already been inventoried, so we don't need to process it again
79
+ if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) {
80
+ // This exact location has already been inventoried, so we don't need to process it again
80
81
  if (depth < existingEntry.depth || indirections < existingEntry.indirections) {
81
82
  removeFromInventory(inventory, existingEntry);
82
83
  }
@@ -312,7 +313,7 @@ const bundle = (parser, options) => {
312
313
  const inventory = [];
313
314
  crawl({
314
315
  parent: parser,
315
- key: 'schema',
316
+ key: "schema",
316
317
  path: parser.$refs._root$Ref.path + "#",
317
318
  pathFromRoot: "#",
318
319
  indirections: 0,
@@ -50,8 +50,8 @@ exports.urlResolver = {
50
50
  if (response.status !== 405 || fetchOptions?.method !== 'HEAD') {
51
51
  throw (0, ono_1.ono)({ status: response.status }, `HTTP ERROR ${response.status}`);
52
52
  }
53
- data = response.body ? await response.arrayBuffer() : new ArrayBuffer(0);
54
53
  }
54
+ data = response.body ? await response.arrayBuffer() : new ArrayBuffer(0);
55
55
  }
56
56
  catch (error) {
57
57
  throw new errors_js_1.ResolverError((0, ono_1.ono)(error, `Error requesting ${file.url}`), file.url);
@@ -1,14 +1,48 @@
1
- import path from 'node:path';
1
+ import path from "path";
2
2
 
3
- import { describe, expect, it } from 'vitest';
3
+ import { describe, expect, it } from "vitest";
4
4
 
5
- import { $RefParser } from '..';
5
+ import { $RefParser } from "..";
6
6
 
7
- describe('bundle', () => {
8
- it('handles circular reference with description', async () => {
7
+ describe("bundle", () => {
8
+ it("handles circular reference with description", async () => {
9
9
  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");
11
11
  const schema = await refParser.bundle({ pathOrUrlOrSchema });
12
12
  expect(schema).not.toBeUndefined();
13
13
  });
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
+ });
14
48
  });
@@ -1,43 +1,45 @@
1
- import path from 'node:path';
1
+ import path from "node:path";
2
2
 
3
- import { describe, expect, it } from 'vitest';
3
+ import { describe, expect, it } from "vitest";
4
4
 
5
- import { getResolvedInput } from '../index';
5
+ import { getResolvedInput } from "../index";
6
6
 
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";
10
10
  const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
11
- expect(resolvedInput.type).toBe('url');
11
+ expect(resolvedInput.type).toBe("url");
12
12
  expect(resolvedInput.schema).toBeUndefined();
13
- expect(resolvedInput.path).toBe('https://foo.com/');
13
+ expect(resolvedInput.path).toBe("https://foo.com/");
14
14
  });
15
15
 
16
- it('handles file', async () => {
17
- const pathOrUrlOrSchema = './path/to/openapi.json';
16
+ it("handles file", async () => {
17
+ const pathOrUrlOrSchema = "./path/to/openapi.json";
18
18
  const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
19
- expect(resolvedInput.type).toBe('file');
19
+ expect(resolvedInput.type).toBe("file");
20
20
  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
+ );
22
24
  });
23
25
 
24
- it('handles raw spec', async () => {
25
- const pathOrUrlOrSchema = {
26
+ it("handles raw spec", async () => {
27
+ const pathOrUrlOrSchema = {
26
28
  info: {
27
- version: '1.0.0',
29
+ version: "1.0.0",
28
30
  },
29
- openapi: '3.1.0',
31
+ openapi: "3.1.0",
30
32
  paths: {},
31
33
  };
32
34
  const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
33
- expect(resolvedInput.type).toBe('json');
35
+ expect(resolvedInput.type).toBe("json");
34
36
  expect(resolvedInput.schema).toEqual({
35
37
  info: {
36
- version: '1.0.0',
38
+ version: "1.0.0",
37
39
  },
38
- openapi: '3.1.0',
40
+ openapi: "3.1.0",
39
41
  paths: {},
40
42
  });
41
- expect(resolvedInput.path).toBe('');
43
+ expect(resolvedInput.path).toBe("");
42
44
  });
43
45
  });
@@ -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
+ }
@@ -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
+ }
package/lib/bundle.ts CHANGED
@@ -94,9 +94,10 @@ const inventory$Ref = <S extends object = JSONSchema>({
94
94
  const extended = $Ref.isExtended$Ref($ref);
95
95
  indirections += pointer.indirections;
96
96
 
97
+ // Check if this exact location (parent + key + pathFromRoot) has already been inventoried
97
98
  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
100
101
  if (depth < existingEntry.depth || indirections < existingEntry.indirections) {
101
102
  removeFromInventory(inventory, existingEntry);
102
103
  } else {
@@ -172,7 +173,7 @@ const crawl = <S extends object = JSONSchema>({
172
173
  pathFromRoot: string;
173
174
  }) => {
174
175
  const obj = key === null ? parent : parent[key as keyof typeof parent];
175
-
176
+
176
177
  if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) {
177
178
  if ($Ref.isAllowed$Ref(obj)) {
178
179
  inventory$Ref({
@@ -359,17 +360,13 @@ function removeFromInventory(inventory: InventoryEntry[], entry: any) {
359
360
  * @param parser
360
361
  * @param options
361
362
  */
362
- export const bundle = (
363
- parser: $RefParser,
364
- options: ParserOptions,
365
- ) => {
363
+ export const bundle = (parser: $RefParser, options: ParserOptions) => {
366
364
  // console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
367
-
368
365
  // Build an inventory of all $ref pointers in the JSON Schema
369
366
  const inventory: InventoryEntry[] = [];
370
367
  crawl<JSONSchema>({
371
368
  parent: parser,
372
- key: 'schema',
369
+ key: "schema",
373
370
  path: parser.$refs._root$Ref.path + "#",
374
371
  pathFromRoot: "#",
375
372
  indirections: 0,
@@ -82,9 +82,9 @@ export const urlResolver = {
82
82
  if (response.status !== 405 || fetchOptions?.method !== 'HEAD') {
83
83
  throw ono({ status: response.status }, `HTTP ERROR ${response.status}`);
84
84
  }
85
-
86
- data = response.body ? await response.arrayBuffer() : new ArrayBuffer(0)
87
85
  }
86
+
87
+ data = response.body ? await response.arrayBuffer() : new ArrayBuffer(0);
88
88
  } catch (error: any) {
89
89
  throw new ResolverError(ono(error, `Error requesting ${file.url}`), file.url);
90
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hey-api/json-schema-ref-parser",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Parse, Resolve, and Dereference JSON Schema $ref pointers",
5
5
  "homepage": "https://heyapi.dev/",
6
6
  "repository": {