@hey-api/json-schema-ref-parser 1.2.4 → 1.3.1
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.
- package/README.md +9 -84
- package/dist/index.d.mts +629 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1920 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -78
- package/{lib/__tests__/spec → src/__tests__/__snapshots__}/circular-ref-with-description.json +1 -1
- package/src/__tests__/__snapshots__/main-with-external-siblings.json +78 -0
- package/{lib/__tests__/spec → src/__tests__/__snapshots__}/multiple-refs.json +17 -3
- package/src/__tests__/__snapshots__/redfish-like.json +87 -0
- package/src/__tests__/bundle.test.ts +393 -0
- package/src/__tests__/index.test.ts +43 -0
- package/src/__tests__/pointer.test.ts +34 -0
- package/src/__tests__/utils.ts +3 -0
- package/{lib → src}/bundle.ts +191 -231
- package/{lib → src}/dereference.ts +20 -43
- package/{lib → src}/index.ts +129 -127
- package/{lib → src}/options.ts +13 -9
- package/{lib → src}/parse.ts +19 -15
- package/src/parsers/binary.ts +13 -0
- package/{lib → src}/parsers/json.ts +5 -6
- package/src/parsers/text.ts +21 -0
- package/{lib → src}/parsers/yaml.ts +9 -9
- package/{lib → src}/pointer.ts +42 -23
- package/{lib → src}/ref.ts +25 -21
- package/{lib → src}/refs.ts +23 -26
- package/{lib → src}/resolve-external.ts +91 -60
- package/{lib → src}/resolvers/file.ts +7 -10
- package/{lib → src}/resolvers/url.ts +12 -8
- package/{lib → src}/types/index.ts +9 -2
- package/src/util/convert-path-to-posix.ts +8 -0
- package/{lib → src}/util/errors.ts +38 -36
- package/{lib → src}/util/is-windows.ts +1 -1
- package/{lib → src}/util/plugins.ts +7 -8
- package/{lib → src}/util/url.ts +41 -42
- package/dist/lib/__tests__/bundle.test.d.ts +0 -1
- package/dist/lib/__tests__/bundle.test.js +0 -50
- package/dist/lib/__tests__/index.test.d.ts +0 -1
- package/dist/lib/__tests__/index.test.js +0 -43
- package/dist/lib/__tests__/pointer.test.d.ts +0 -1
- package/dist/lib/__tests__/pointer.test.js +0 -27
- package/dist/lib/bundle.d.ts +0 -26
- package/dist/lib/bundle.js +0 -600
- package/dist/lib/dereference.d.ts +0 -11
- package/dist/lib/dereference.js +0 -226
- package/dist/lib/index.d.ts +0 -92
- package/dist/lib/index.js +0 -525
- package/dist/lib/options.d.ts +0 -61
- package/dist/lib/options.js +0 -45
- package/dist/lib/parse.d.ts +0 -13
- package/dist/lib/parse.js +0 -87
- package/dist/lib/parsers/binary.d.ts +0 -2
- package/dist/lib/parsers/binary.js +0 -12
- package/dist/lib/parsers/json.d.ts +0 -2
- package/dist/lib/parsers/json.js +0 -38
- package/dist/lib/parsers/text.d.ts +0 -2
- package/dist/lib/parsers/text.js +0 -18
- package/dist/lib/parsers/yaml.d.ts +0 -2
- package/dist/lib/parsers/yaml.js +0 -28
- package/dist/lib/pointer.d.ts +0 -88
- package/dist/lib/pointer.js +0 -297
- package/dist/lib/ref.d.ts +0 -180
- package/dist/lib/ref.js +0 -226
- package/dist/lib/refs.d.ts +0 -127
- package/dist/lib/refs.js +0 -232
- package/dist/lib/resolve-external.d.ts +0 -13
- package/dist/lib/resolve-external.js +0 -151
- package/dist/lib/resolvers/file.d.ts +0 -6
- package/dist/lib/resolvers/file.js +0 -61
- package/dist/lib/resolvers/url.d.ts +0 -17
- package/dist/lib/resolvers/url.js +0 -62
- package/dist/lib/types/index.d.ts +0 -43
- package/dist/lib/types/index.js +0 -2
- package/dist/lib/util/convert-path-to-posix.d.ts +0 -1
- package/dist/lib/util/convert-path-to-posix.js +0 -14
- package/dist/lib/util/errors.d.ts +0 -56
- package/dist/lib/util/errors.js +0 -112
- package/dist/lib/util/is-windows.d.ts +0 -1
- package/dist/lib/util/is-windows.js +0 -6
- package/dist/lib/util/plugins.d.ts +0 -16
- package/dist/lib/util/plugins.js +0 -45
- package/dist/lib/util/url.d.ts +0 -79
- package/dist/lib/util/url.js +0 -285
- package/dist/vite.config.d.ts +0 -2
- package/dist/vite.config.js +0 -19
- package/lib/__tests__/bundle.test.ts +0 -52
- package/lib/__tests__/index.test.ts +0 -45
- package/lib/__tests__/pointer.test.ts +0 -26
- package/lib/__tests__/spec/openapi-paths-ref.json +0 -46
- package/lib/__tests__/spec/path-parameter.json +0 -16
- package/lib/parsers/binary.ts +0 -13
- package/lib/parsers/text.ts +0 -21
- package/lib/util/convert-path-to-posix.ts +0 -11
- /package/{LICENSE → LICENSE.md} +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.0",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Redfish-like API",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "Test API simulating Redfish structure with versioned schemas"
|
|
7
|
+
},
|
|
8
|
+
"paths": {
|
|
9
|
+
"/redfish/v1/Systems": {
|
|
10
|
+
"get": {
|
|
11
|
+
"summary": "Get Systems",
|
|
12
|
+
"responses": {
|
|
13
|
+
"200": {
|
|
14
|
+
"description": "Success",
|
|
15
|
+
"content": {
|
|
16
|
+
"application/json": {
|
|
17
|
+
"schema": {
|
|
18
|
+
"$ref": "#/components/schemas/ResolutionStep_v1_0_1_ResolutionStep"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"default": {
|
|
24
|
+
"description": "Error"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"/redfish/v1/Actions": {
|
|
30
|
+
"post": {
|
|
31
|
+
"summary": "Submit Action",
|
|
32
|
+
"responses": {
|
|
33
|
+
"200": {
|
|
34
|
+
"description": "Success",
|
|
35
|
+
"content": {
|
|
36
|
+
"application/json": {
|
|
37
|
+
"schema": {
|
|
38
|
+
"$ref": "#/components/schemas/ResolutionStep_v1_0_1_ActionParameters"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"components": {
|
|
48
|
+
"schemas": {
|
|
49
|
+
"ResolutionStep_v1_0_1_ActionParameters": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"ActionId": {
|
|
53
|
+
"type": "string"
|
|
54
|
+
},
|
|
55
|
+
"ActionType": {
|
|
56
|
+
"$ref": "#/components/schemas/ResolutionStep_v1_0_1_ResolutionType"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"ResolutionStep_v1_0_1_ResolutionStep": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"ResolutionType": {
|
|
64
|
+
"oneOf": [
|
|
65
|
+
{
|
|
66
|
+
"$ref": "#/components/schemas/ResolutionStep_v1_0_1_ResolutionType"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"ActionName": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Name of the action"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"ResolutionStep_v1_0_1_ResolutionType": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"enum": [
|
|
79
|
+
"ContactVendor",
|
|
80
|
+
"ResetToDefaults",
|
|
81
|
+
"RetryOperation"
|
|
82
|
+
],
|
|
83
|
+
"description": "Types of resolution actions"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { $RefParser } from '..';
|
|
6
|
+
import { getSpecsPath } from './utils';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const getSnapshotsPath = () => path.join(__dirname, '__snapshots__');
|
|
12
|
+
const getTempSnapshotsPath = () => path.join(__dirname, '.gen', 'snapshots');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper function to compare a bundled schema with a snapshot file.
|
|
16
|
+
* Handles writing the schema to a temp file and comparing with the snapshot.
|
|
17
|
+
*
|
|
18
|
+
* @param schema - The bundled schema to compare
|
|
19
|
+
* @param snapshotName - The name of the snapshot file (e.g., 'circular-ref-with-description.json')
|
|
20
|
+
*/
|
|
21
|
+
const expectBundledSchemaToMatchSnapshot = async (schema: unknown, snapshotName: string) => {
|
|
22
|
+
const outputPath = path.join(getTempSnapshotsPath(), snapshotName);
|
|
23
|
+
const snapshotPath = path.join(getSnapshotsPath(), snapshotName);
|
|
24
|
+
|
|
25
|
+
// Ensure directory exists
|
|
26
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
27
|
+
|
|
28
|
+
// Write the bundled result
|
|
29
|
+
const content = JSON.stringify(schema, null, 2);
|
|
30
|
+
fs.writeFileSync(outputPath, content);
|
|
31
|
+
|
|
32
|
+
// Compare with snapshot
|
|
33
|
+
await expect(content).toMatchFileSnapshot(snapshotPath);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('bundle', () => {
|
|
37
|
+
it('handles circular reference with description', async () => {
|
|
38
|
+
const refParser = new $RefParser();
|
|
39
|
+
const pathOrUrlOrSchema = path.join(
|
|
40
|
+
getSpecsPath(),
|
|
41
|
+
'json-schema-ref-parser',
|
|
42
|
+
'circular-ref-with-description.json',
|
|
43
|
+
);
|
|
44
|
+
const schema = await refParser.bundle({ pathOrUrlOrSchema });
|
|
45
|
+
|
|
46
|
+
await expectBundledSchemaToMatchSnapshot(schema, 'circular-ref-with-description.json');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('bundles multiple references to the same file correctly', async () => {
|
|
50
|
+
const refParser = new $RefParser();
|
|
51
|
+
const pathOrUrlOrSchema = path.join(
|
|
52
|
+
getSpecsPath(),
|
|
53
|
+
'json-schema-ref-parser',
|
|
54
|
+
'multiple-refs.json',
|
|
55
|
+
);
|
|
56
|
+
const schema = await refParser.bundle({ pathOrUrlOrSchema });
|
|
57
|
+
|
|
58
|
+
await expectBundledSchemaToMatchSnapshot(schema, 'multiple-refs.json');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('hoists sibling schemas from external files', async () => {
|
|
62
|
+
const refParser = new $RefParser();
|
|
63
|
+
const pathOrUrlOrSchema = path.join(
|
|
64
|
+
getSpecsPath(),
|
|
65
|
+
'json-schema-ref-parser',
|
|
66
|
+
'main-with-external-siblings.json',
|
|
67
|
+
);
|
|
68
|
+
const schema = await refParser.bundle({ pathOrUrlOrSchema });
|
|
69
|
+
|
|
70
|
+
await expectBundledSchemaToMatchSnapshot(schema, 'main-with-external-siblings.json');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('hoists sibling schemas from YAML files with versioned names (Redfish-like)', async () => {
|
|
74
|
+
const refParser = new $RefParser();
|
|
75
|
+
const pathOrUrlOrSchema = path.join(
|
|
76
|
+
getSpecsPath(),
|
|
77
|
+
'json-schema-ref-parser',
|
|
78
|
+
'redfish-like.yaml',
|
|
79
|
+
);
|
|
80
|
+
const schema = await refParser.bundle({ pathOrUrlOrSchema });
|
|
81
|
+
|
|
82
|
+
await expectBundledSchemaToMatchSnapshot(schema, 'redfish-like.json');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('sibling schema resolution', () => {
|
|
86
|
+
const specsDir = path.join(getSpecsPath(), 'json-schema-ref-parser');
|
|
87
|
+
|
|
88
|
+
const findSchemaByValue = (
|
|
89
|
+
schemas: Record<string, any>,
|
|
90
|
+
predicate: (value: any) => boolean,
|
|
91
|
+
): [string, any] | undefined => {
|
|
92
|
+
for (const [name, value] of Object.entries(schemas)) {
|
|
93
|
+
if (predicate(value)) {
|
|
94
|
+
return [name, value];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
it('hoists sibling schemas through a bare $ref wrapper chain', async () => {
|
|
101
|
+
const refParser = new $RefParser();
|
|
102
|
+
const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-root.json');
|
|
103
|
+
const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any;
|
|
104
|
+
|
|
105
|
+
expect(schema.components).toBeDefined();
|
|
106
|
+
expect(schema.components.schemas).toBeDefined();
|
|
107
|
+
|
|
108
|
+
const schemas = schema.components.schemas;
|
|
109
|
+
|
|
110
|
+
const mainSchema = findSchemaByValue(
|
|
111
|
+
schemas,
|
|
112
|
+
(v) => v.type === 'object' && v.properties?.name,
|
|
113
|
+
);
|
|
114
|
+
expect(mainSchema).toBeDefined();
|
|
115
|
+
const [mainName, mainValue] = mainSchema!;
|
|
116
|
+
expect(mainValue.type).toBe('object');
|
|
117
|
+
expect(mainValue.properties.name).toEqual({ type: 'string' });
|
|
118
|
+
|
|
119
|
+
const enumSchema = findSchemaByValue(
|
|
120
|
+
schemas,
|
|
121
|
+
(v) => Array.isArray(v.enum) && v.enum.includes('active'),
|
|
122
|
+
);
|
|
123
|
+
expect(enumSchema).toBeDefined();
|
|
124
|
+
const [enumName, enumValue] = enumSchema!;
|
|
125
|
+
expect(enumValue.type).toBe('string');
|
|
126
|
+
expect(enumValue.enum).toEqual(['active', 'inactive', 'pending']);
|
|
127
|
+
|
|
128
|
+
// The main schema's status property should reference the hoisted enum
|
|
129
|
+
expect(mainValue.properties.status.$ref).toBe(`#/components/schemas/${enumName}`);
|
|
130
|
+
|
|
131
|
+
// The root path's schema ref should point to the hoisted main schema
|
|
132
|
+
const rootRef = schema.paths['/test'].get.responses['200'].content['application/json'].schema;
|
|
133
|
+
expect(rootRef.$ref).toBe(`#/components/schemas/${mainName}`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('hoists sibling schemas through an extended $ref wrapper chain', async () => {
|
|
137
|
+
const refParser = new $RefParser();
|
|
138
|
+
const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-extended-root.json');
|
|
139
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any;
|
|
143
|
+
|
|
144
|
+
expect(schema.components).toBeDefined();
|
|
145
|
+
expect(schema.components.schemas).toBeDefined();
|
|
146
|
+
|
|
147
|
+
const schemas = schema.components.schemas;
|
|
148
|
+
|
|
149
|
+
// The main schema should be hoisted (with the extra description merged in)
|
|
150
|
+
const mainSchema = findSchemaByValue(
|
|
151
|
+
schemas,
|
|
152
|
+
(v) =>
|
|
153
|
+
v.description === 'Wrapper that extends the versioned schema' ||
|
|
154
|
+
(v.type === 'object' && v.properties?.name),
|
|
155
|
+
);
|
|
156
|
+
expect(mainSchema).toBeDefined();
|
|
157
|
+
|
|
158
|
+
// The sibling enum must also be hoisted (this was the bug — it was lost before the fix)
|
|
159
|
+
const enumSchema = findSchemaByValue(
|
|
160
|
+
schemas,
|
|
161
|
+
(v) => Array.isArray(v.enum) && v.enum.includes('active'),
|
|
162
|
+
);
|
|
163
|
+
expect(enumSchema).toBeDefined();
|
|
164
|
+
const [, enumValue] = enumSchema!;
|
|
165
|
+
expect(enumValue.type).toBe('string');
|
|
166
|
+
expect(enumValue.enum).toEqual(['active', 'inactive', 'pending']);
|
|
167
|
+
|
|
168
|
+
// No "Skipping unresolvable $ref" warnings should have been emitted
|
|
169
|
+
const unresolvableWarnings = warnSpy.mock.calls.filter(
|
|
170
|
+
(args) => typeof args[0] === 'string' && args[0].includes('Skipping unresolvable $ref'),
|
|
171
|
+
);
|
|
172
|
+
expect(unresolvableWarnings).toHaveLength(0);
|
|
173
|
+
} finally {
|
|
174
|
+
warnSpy.mockRestore();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('hoists sibling schemas from a direct reference (no wrapper)', async () => {
|
|
179
|
+
const refParser = new $RefParser();
|
|
180
|
+
const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-direct-root.json');
|
|
181
|
+
const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any;
|
|
182
|
+
|
|
183
|
+
expect(schema.components).toBeDefined();
|
|
184
|
+
expect(schema.components.schemas).toBeDefined();
|
|
185
|
+
|
|
186
|
+
const schemas = schema.components.schemas;
|
|
187
|
+
|
|
188
|
+
const mainSchema = findSchemaByValue(
|
|
189
|
+
schemas,
|
|
190
|
+
(v) => v.type === 'object' && v.properties?.name,
|
|
191
|
+
);
|
|
192
|
+
expect(mainSchema).toBeDefined();
|
|
193
|
+
|
|
194
|
+
const enumSchema = findSchemaByValue(
|
|
195
|
+
schemas,
|
|
196
|
+
(v) => Array.isArray(v.enum) && v.enum.includes('active'),
|
|
197
|
+
);
|
|
198
|
+
expect(enumSchema).toBeDefined();
|
|
199
|
+
const [enumName, enumValue] = enumSchema!;
|
|
200
|
+
expect(enumValue.enum).toEqual(['active', 'inactive', 'pending']);
|
|
201
|
+
|
|
202
|
+
const [, mainValue] = mainSchema!;
|
|
203
|
+
expect(mainValue.properties.status.$ref).toBe(`#/components/schemas/${enumName}`);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('hoists multiple sibling schemas through an extended wrapper', async () => {
|
|
207
|
+
const refParser = new $RefParser();
|
|
208
|
+
const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-multi-root.json');
|
|
209
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any;
|
|
213
|
+
|
|
214
|
+
expect(schema.components).toBeDefined();
|
|
215
|
+
expect(schema.components.schemas).toBeDefined();
|
|
216
|
+
|
|
217
|
+
const schemas = schema.components.schemas;
|
|
218
|
+
|
|
219
|
+
const mainSchema = findSchemaByValue(
|
|
220
|
+
schemas,
|
|
221
|
+
(v) => v.type === 'object' && v.properties?.health,
|
|
222
|
+
);
|
|
223
|
+
expect(mainSchema).toBeDefined();
|
|
224
|
+
|
|
225
|
+
const statusEnum = findSchemaByValue(
|
|
226
|
+
schemas,
|
|
227
|
+
(v) => Array.isArray(v.enum) && v.enum.includes('enabled'),
|
|
228
|
+
);
|
|
229
|
+
expect(statusEnum).toBeDefined();
|
|
230
|
+
expect(statusEnum![1].enum).toEqual(['enabled', 'disabled', 'standby']);
|
|
231
|
+
|
|
232
|
+
const healthEnum = findSchemaByValue(
|
|
233
|
+
schemas,
|
|
234
|
+
(v) => Array.isArray(v.enum) && v.enum.includes('ok'),
|
|
235
|
+
);
|
|
236
|
+
expect(healthEnum).toBeDefined();
|
|
237
|
+
expect(healthEnum![1].enum).toEqual(['ok', 'warning', 'critical']);
|
|
238
|
+
|
|
239
|
+
const [, mainValue] = mainSchema!;
|
|
240
|
+
expect(mainValue.properties.status.$ref).toBe(`#/components/schemas/${statusEnum![0]}`);
|
|
241
|
+
expect(mainValue.properties.health.$ref).toBe(`#/components/schemas/${healthEnum![0]}`);
|
|
242
|
+
|
|
243
|
+
const unresolvableWarnings = warnSpy.mock.calls.filter(
|
|
244
|
+
(args) => typeof args[0] === 'string' && args[0].includes('Skipping unresolvable $ref'),
|
|
245
|
+
);
|
|
246
|
+
expect(unresolvableWarnings).toHaveLength(0);
|
|
247
|
+
} finally {
|
|
248
|
+
warnSpy.mockRestore();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('handles multiple external files with same-named sibling schemas', async () => {
|
|
253
|
+
const refParser = new $RefParser();
|
|
254
|
+
const pathOrUrlOrSchema = path.join(specsDir, 'sibling-schema-collision-root.json');
|
|
255
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any;
|
|
259
|
+
|
|
260
|
+
expect(schema.components).toBeDefined();
|
|
261
|
+
expect(schema.components.schemas).toBeDefined();
|
|
262
|
+
|
|
263
|
+
const schemas = schema.components.schemas;
|
|
264
|
+
const schemaNames = Object.keys(schemas);
|
|
265
|
+
|
|
266
|
+
const mainSchemaKey = schemaNames.find((name) => name.includes('MainSchema'));
|
|
267
|
+
const otherSchemaKey = schemaNames.find((name) => name.includes('OtherSchema'));
|
|
268
|
+
|
|
269
|
+
expect(mainSchemaKey).toBeDefined();
|
|
270
|
+
expect(otherSchemaKey).toBeDefined();
|
|
271
|
+
|
|
272
|
+
const statusSchemas = schemaNames.filter((name) => name.includes('Status'));
|
|
273
|
+
expect(statusSchemas.length).toBeGreaterThanOrEqual(2);
|
|
274
|
+
|
|
275
|
+
const statusValues = statusSchemas.map((name) => schemas[name]);
|
|
276
|
+
const stringStatus = statusValues.find((v: any) => v.type === 'string');
|
|
277
|
+
const integerStatus = statusValues.find((v: any) => v.type === 'integer');
|
|
278
|
+
|
|
279
|
+
expect(stringStatus).toBeDefined();
|
|
280
|
+
expect(integerStatus).toBeDefined();
|
|
281
|
+
expect(stringStatus!.enum).toEqual(['active', 'inactive']);
|
|
282
|
+
expect(integerStatus!.enum).toEqual([0, 1, 2]);
|
|
283
|
+
|
|
284
|
+
const mainSchemaValue = schemas[mainSchemaKey!];
|
|
285
|
+
const mainStatusRef = mainSchemaValue.properties.status.$ref;
|
|
286
|
+
expect(mainStatusRef).toMatch(/^#\/components\/schemas\/.*Status/);
|
|
287
|
+
|
|
288
|
+
const referencedStatus = schemas[mainStatusRef.replace('#/components/schemas/', '')];
|
|
289
|
+
expect(referencedStatus).toBeDefined();
|
|
290
|
+
expect(referencedStatus.type).toBe('string');
|
|
291
|
+
expect(referencedStatus.enum).toEqual(['active', 'inactive']);
|
|
292
|
+
|
|
293
|
+
const otherSchemaValue = schemas[otherSchemaKey!];
|
|
294
|
+
const otherStatusRef = otherSchemaValue.properties.code.$ref;
|
|
295
|
+
expect(otherStatusRef).toMatch(/^#\/components\/schemas\/.*Status/);
|
|
296
|
+
|
|
297
|
+
const referencedOtherStatus = schemas[otherStatusRef.replace('#/components/schemas/', '')];
|
|
298
|
+
expect(referencedOtherStatus).toBeDefined();
|
|
299
|
+
expect(referencedOtherStatus.type).toBe('integer');
|
|
300
|
+
expect(referencedOtherStatus.enum).toEqual([0, 1, 2]);
|
|
301
|
+
|
|
302
|
+
const unresolvableWarnings = warnSpy.mock.calls.filter(
|
|
303
|
+
(args) => typeof args[0] === 'string' && args[0].includes('Skipping unresolvable $ref'),
|
|
304
|
+
);
|
|
305
|
+
expect(unresolvableWarnings).toHaveLength(0);
|
|
306
|
+
} finally {
|
|
307
|
+
warnSpy.mockRestore();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('mergeMany', () => {
|
|
313
|
+
it('merges paths with non-conflicting methods under the same path', async () => {
|
|
314
|
+
const refParser = new $RefParser();
|
|
315
|
+
const spec1 = {
|
|
316
|
+
info: { title: 'Spec 1', version: '1.0.0' },
|
|
317
|
+
paths: {
|
|
318
|
+
'/pet/{petId}': {
|
|
319
|
+
post: {
|
|
320
|
+
operationId: 'updatePetWithForm',
|
|
321
|
+
responses: { '405': { description: 'Invalid input' } },
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
swagger: '2.0',
|
|
326
|
+
};
|
|
327
|
+
const spec2 = {
|
|
328
|
+
info: { title: 'Spec 2', version: '1.0.0' },
|
|
329
|
+
paths: {
|
|
330
|
+
'/pet/{petId}': {
|
|
331
|
+
delete: {
|
|
332
|
+
operationId: 'deletePet',
|
|
333
|
+
responses: {
|
|
334
|
+
'400': { description: 'Invalid ID supplied' },
|
|
335
|
+
'404': { description: 'Pet not found' },
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
swagger: '2.0',
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const merged = (await refParser.bundleMany({ pathOrUrlOrSchemas: [spec1, spec2] })) as any;
|
|
344
|
+
|
|
345
|
+
// Both methods should be under the same path (no prefix added)
|
|
346
|
+
expect(merged.paths['/pet/{petId}']).toBeDefined();
|
|
347
|
+
expect(merged.paths['/pet/{petId}'].post).toBeDefined();
|
|
348
|
+
expect(merged.paths['/pet/{petId}'].delete).toBeDefined();
|
|
349
|
+
|
|
350
|
+
// No prefixed path should be created
|
|
351
|
+
const pathKeys = Object.keys(merged.paths);
|
|
352
|
+
expect(pathKeys).toHaveLength(1);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('adds prefix to path when HTTP methods conflict', async () => {
|
|
356
|
+
const refParser = new $RefParser();
|
|
357
|
+
const spec1 = {
|
|
358
|
+
info: { title: 'Spec 1', version: '1.0.0' },
|
|
359
|
+
paths: {
|
|
360
|
+
'/pet/{petId}': {
|
|
361
|
+
get: {
|
|
362
|
+
operationId: 'getPetById',
|
|
363
|
+
responses: { '200': { description: 'OK' } },
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
swagger: '2.0',
|
|
368
|
+
};
|
|
369
|
+
const spec2 = {
|
|
370
|
+
info: { title: 'Spec 2', version: '1.0.0' },
|
|
371
|
+
paths: {
|
|
372
|
+
'/pet/{petId}': {
|
|
373
|
+
get: {
|
|
374
|
+
operationId: 'getPet',
|
|
375
|
+
responses: { '200': { description: 'Success' } },
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
swagger: '2.0',
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const merged = (await refParser.bundleMany({ pathOrUrlOrSchemas: [spec1, spec2] })) as any;
|
|
383
|
+
|
|
384
|
+
// The conflicting path should be prefixed
|
|
385
|
+
const pathKeys = Object.keys(merged.paths);
|
|
386
|
+
expect(pathKeys).toHaveLength(2);
|
|
387
|
+
expect(merged.paths['/pet/{petId}']).toBeDefined();
|
|
388
|
+
const prefixedKey = pathKeys.find((k) => k !== '/pet/{petId}');
|
|
389
|
+
expect(prefixedKey).toBeDefined();
|
|
390
|
+
expect(merged.paths[prefixedKey!].get).toBeDefined();
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { getResolvedInput } from '../index';
|
|
4
|
+
|
|
5
|
+
describe('getResolvedInput', () => {
|
|
6
|
+
it('handles url', async () => {
|
|
7
|
+
const pathOrUrlOrSchema = 'https://foo.com';
|
|
8
|
+
const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
|
|
9
|
+
expect(resolvedInput.type).toBe('url');
|
|
10
|
+
expect(resolvedInput.schema).toBeUndefined();
|
|
11
|
+
expect(resolvedInput.path).toBe('https://foo.com/');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('handles file', async () => {
|
|
15
|
+
const pathOrUrlOrSchema = './path/to/openapi.json';
|
|
16
|
+
const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
|
|
17
|
+
expect(resolvedInput.type).toBe('file');
|
|
18
|
+
expect(resolvedInput.schema).toBeUndefined();
|
|
19
|
+
expect(path.normalize(resolvedInput.path).toLowerCase()).toBe(
|
|
20
|
+
path.normalize(path.resolve('./path/to/openapi.json')).toLowerCase(),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('handles raw spec', async () => {
|
|
25
|
+
const pathOrUrlOrSchema = {
|
|
26
|
+
info: {
|
|
27
|
+
version: '1.0.0',
|
|
28
|
+
},
|
|
29
|
+
openapi: '3.1.0',
|
|
30
|
+
paths: {},
|
|
31
|
+
};
|
|
32
|
+
const resolvedInput = await getResolvedInput({ pathOrUrlOrSchema });
|
|
33
|
+
expect(resolvedInput.type).toBe('json');
|
|
34
|
+
expect(resolvedInput.schema).toEqual({
|
|
35
|
+
info: {
|
|
36
|
+
version: '1.0.0',
|
|
37
|
+
},
|
|
38
|
+
openapi: '3.1.0',
|
|
39
|
+
paths: {},
|
|
40
|
+
});
|
|
41
|
+
expect(resolvedInput.path).toBe('');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { $RefParser } from '..';
|
|
4
|
+
import { getSpecsPath } from './utils';
|
|
5
|
+
|
|
6
|
+
describe('pointer', () => {
|
|
7
|
+
it('inlines internal JSON Pointer refs under #/paths/ for OpenAPI bundling', async () => {
|
|
8
|
+
const refParser = new $RefParser();
|
|
9
|
+
const pathOrUrlOrSchema = path.join(
|
|
10
|
+
getSpecsPath(),
|
|
11
|
+
'json-schema-ref-parser',
|
|
12
|
+
'openapi-paths-ref.json',
|
|
13
|
+
);
|
|
14
|
+
const schema = (await refParser.bundle({ pathOrUrlOrSchema })) as any;
|
|
15
|
+
|
|
16
|
+
// The GET endpoint should have its schema defined inline
|
|
17
|
+
const getSchema = schema.paths['/foo'].get.responses['200'].content['application/json'].schema;
|
|
18
|
+
expect(getSchema.$ref).toBeUndefined();
|
|
19
|
+
expect(getSchema.type).toBe('object');
|
|
20
|
+
expect(getSchema.properties.bar.type).toBe('string');
|
|
21
|
+
|
|
22
|
+
// The POST endpoint should have its schema inlined (copied) instead of a $ref
|
|
23
|
+
const postSchema =
|
|
24
|
+
schema.paths['/foo'].post.responses['200'].content['application/json'].schema;
|
|
25
|
+
expect(postSchema.$ref).toBe(
|
|
26
|
+
'#/paths/~1foo/get/responses/200/content/application~1json/schema',
|
|
27
|
+
);
|
|
28
|
+
expect(postSchema.type).toBeUndefined();
|
|
29
|
+
expect(postSchema.properties?.bar?.type).toBeUndefined();
|
|
30
|
+
|
|
31
|
+
// Both schemas should be identical objects
|
|
32
|
+
expect(postSchema).not.toBe(getSchema);
|
|
33
|
+
});
|
|
34
|
+
});
|