@hey-api/json-schema-ref-parser 1.3.0 → 1.4.0
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 +130 -1
- package/dist/index.d.mts +33 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +131 -98
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -8
- package/src/__tests__/__snapshots__/circular-ref-with-description.json +11 -0
- package/src/__tests__/__snapshots__/main-with-external-siblings.json +78 -0
- package/src/__tests__/__snapshots__/multiple-refs.json +48 -0
- package/src/__tests__/__snapshots__/redfish-like.json +87 -0
- package/src/__tests__/bundle.test.ts +367 -33
- package/src/bundle.ts +60 -14
- package/src/dereference.ts +1 -1
- package/src/index.ts +56 -18
- package/src/parsers/yaml.ts +2 -4
- package/src/pointer.ts +5 -5
- package/src/ref.ts +2 -2
- package/src/refs.ts +1 -1
package/src/bundle.ts
CHANGED
|
@@ -92,7 +92,7 @@ const getContainerTypeFromPath = (
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* Inventories the given JSON Reference (i.e
|
|
95
|
+
* Inventories the given JSON Reference (i.e., records detailed information about it so we can
|
|
96
96
|
* optimize all $refs in the schema), and then crawls the resolved value.
|
|
97
97
|
*/
|
|
98
98
|
const inventory$Ref = <S extends object = JSONSchema>({
|
|
@@ -157,11 +157,31 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
157
157
|
pointer = $refs._resolve($refPath, pathFromRoot, options);
|
|
158
158
|
} catch (error) {
|
|
159
159
|
if (error instanceof MissingPointerError) {
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
// The ref couldn't be resolved in the target file. This commonly
|
|
161
|
+
// happens when a wrapper file redirects via $ref to a versioned
|
|
162
|
+
// file, and the bundler's crawl path retains the wrapper URL.
|
|
163
|
+
// Try resolving the hash fragment against other files in $refs
|
|
164
|
+
// that might contain the target schema.
|
|
165
|
+
const hash = url.getHash($refPath);
|
|
166
|
+
if (hash) {
|
|
167
|
+
const baseFile = url.stripHash($refPath);
|
|
168
|
+
for (const filePath of Object.keys($refs._$refs)) {
|
|
169
|
+
if (filePath === baseFile) continue;
|
|
170
|
+
try {
|
|
171
|
+
pointer = $refs._resolve(filePath + hash, pathFromRoot, options);
|
|
172
|
+
if (pointer) break;
|
|
173
|
+
} catch {
|
|
174
|
+
// try next file
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!pointer) {
|
|
179
|
+
console.warn(`Skipping unresolvable $ref: ${$refPath}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
throw error;
|
|
163
184
|
}
|
|
164
|
-
throw error; // Re-throw unexpected errors
|
|
165
185
|
}
|
|
166
186
|
|
|
167
187
|
if (pointer) {
|
|
@@ -193,10 +213,10 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
193
213
|
}
|
|
194
214
|
|
|
195
215
|
const newEntry: InventoryEntry = {
|
|
196
|
-
$ref, // The JSON Reference (e.g
|
|
197
|
-
circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e
|
|
216
|
+
$ref, // The JSON Reference (e.g., {$ref: string})
|
|
217
|
+
circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e., it references itself)
|
|
198
218
|
depth, // How far from the JSON Schema root is this $ref pointer?
|
|
199
|
-
extended, // Does this $ref extend its resolved value? (i.e
|
|
219
|
+
extended, // Does this $ref extend its resolved value? (i.e., it has extra properties, in addition to "$ref")
|
|
200
220
|
external, // Does this $ref pointer point to a file other than the main JSON Schema file?
|
|
201
221
|
file, // The file that the $ref pointer resolves to
|
|
202
222
|
hash, // The hash within `file` that the $ref pointer resolves to
|
|
@@ -217,8 +237,19 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
217
237
|
inventory.push(newEntry);
|
|
218
238
|
inventoryLookup.add(newEntry);
|
|
219
239
|
|
|
220
|
-
// Recursively crawl the resolved value
|
|
240
|
+
// Recursively crawl the resolved value.
|
|
241
|
+
// When the resolution followed a $ref chain to a different file,
|
|
242
|
+
// use the resolved file as the base path so that local $ref values
|
|
243
|
+
// (e.g., #/components/schemas/SiblingSchema) inside the resolved
|
|
244
|
+
// value resolve against the correct file.
|
|
221
245
|
if (!existingEntry || external) {
|
|
246
|
+
let crawlPath = pointer.path;
|
|
247
|
+
|
|
248
|
+
const originalFile = url.stripHash($refPath);
|
|
249
|
+
if (file !== originalFile) {
|
|
250
|
+
crawlPath = file + url.getHash(pointer.path);
|
|
251
|
+
}
|
|
252
|
+
|
|
222
253
|
crawl({
|
|
223
254
|
$refs,
|
|
224
255
|
indirections: indirections + 1,
|
|
@@ -227,7 +258,7 @@ const inventory$Ref = <S extends object = JSONSchema>({
|
|
|
227
258
|
key: null,
|
|
228
259
|
options,
|
|
229
260
|
parent: pointer.value,
|
|
230
|
-
path:
|
|
261
|
+
path: crawlPath,
|
|
231
262
|
pathFromRoot,
|
|
232
263
|
resolvedRefs,
|
|
233
264
|
visitedObjects,
|
|
@@ -414,8 +445,8 @@ function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
|
|
|
414
445
|
const ensureContainer = (
|
|
415
446
|
type: 'schemas' | 'parameters' | 'requestBodies' | 'responses' | 'headers',
|
|
416
447
|
) => {
|
|
417
|
-
const isOas3 =
|
|
418
|
-
const isOas2 =
|
|
448
|
+
const isOas3 = Boolean(root && typeof root === 'object' && typeof root.openapi === 'string');
|
|
449
|
+
const isOas2 = Boolean(root && typeof root === 'object' && typeof root.swagger === 'string');
|
|
419
450
|
|
|
420
451
|
if (isOas3) {
|
|
421
452
|
if (!root.components || typeof root.components !== 'object') {
|
|
@@ -539,7 +570,7 @@ function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
|
|
|
539
570
|
}
|
|
540
571
|
|
|
541
572
|
// Keep internal refs internal. However, if the $ref extends the resolved value
|
|
542
|
-
// (i.e
|
|
573
|
+
// (i.e., it has additional properties in addition to "$ref"), then we must
|
|
543
574
|
// preserve the original $ref rather than rewriting it to the resolved hash.
|
|
544
575
|
if (!entry.external) {
|
|
545
576
|
if (!entry.extended && entry.$ref && typeof entry.$ref === 'object') {
|
|
@@ -586,7 +617,22 @@ function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
|
|
|
586
617
|
} catch {
|
|
587
618
|
// Ignore errors
|
|
588
619
|
}
|
|
589
|
-
|
|
620
|
+
|
|
621
|
+
// Try without prefix first (cleaner names)
|
|
622
|
+
const schemaName = lastToken(entry.hash);
|
|
623
|
+
let proposed = schemaName;
|
|
624
|
+
|
|
625
|
+
// Check if this name would conflict with existing schemas from other files
|
|
626
|
+
if (!usedNamesByObj.has(container)) {
|
|
627
|
+
usedNamesByObj.set(container, new Set<string>(Object.keys(container || {})));
|
|
628
|
+
}
|
|
629
|
+
const used = usedNamesByObj.get(container)!;
|
|
630
|
+
|
|
631
|
+
// If the name is already used, add the file prefix
|
|
632
|
+
if (used.has(proposed)) {
|
|
633
|
+
proposed = `${proposedBase}_${schemaName}`;
|
|
634
|
+
}
|
|
635
|
+
|
|
590
636
|
defName = uniqueName(container, proposed);
|
|
591
637
|
namesForPrefix.set(targetKey, defName);
|
|
592
638
|
// Store the resolved value under the container
|
package/src/dereference.ts
CHANGED
|
@@ -226,7 +226,7 @@ function dereference$Ref<S extends object = JSONSchema>(
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
if (directCircular) {
|
|
229
|
-
// The pointer is a DIRECT circular reference (i.e
|
|
229
|
+
// The pointer is a DIRECT circular reference (i.e., it references itself).
|
|
230
230
|
// So replace the $ref path with the absolute path from the JSON Schema root
|
|
231
231
|
dereferencedValue.$ref = pathFromRoot;
|
|
232
232
|
}
|
package/src/index.ts
CHANGED
|
@@ -117,12 +117,12 @@ export class $RefParser {
|
|
|
117
117
|
|
|
118
118
|
await resolveExternal(this, this.options);
|
|
119
119
|
const errors = JSONParserErrorGroup.getParserErrors(this);
|
|
120
|
-
if (errors.length
|
|
120
|
+
if (errors.length) {
|
|
121
121
|
throw new JSONParserErrorGroup(this);
|
|
122
122
|
}
|
|
123
123
|
_bundle(this, this.options);
|
|
124
124
|
const errors2 = JSONParserErrorGroup.getParserErrors(this);
|
|
125
|
-
if (errors2.length
|
|
125
|
+
if (errors2.length) {
|
|
126
126
|
throw new JSONParserErrorGroup(this);
|
|
127
127
|
}
|
|
128
128
|
return this.schema!;
|
|
@@ -148,14 +148,14 @@ export class $RefParser {
|
|
|
148
148
|
|
|
149
149
|
await resolveExternal(this, this.options);
|
|
150
150
|
const errors = JSONParserErrorGroup.getParserErrors(this);
|
|
151
|
-
if (errors.length
|
|
151
|
+
if (errors.length) {
|
|
152
152
|
throw new JSONParserErrorGroup(this);
|
|
153
153
|
}
|
|
154
154
|
_bundle(this, this.options);
|
|
155
155
|
// Merged root is ready for bundling
|
|
156
156
|
|
|
157
157
|
const errors2 = JSONParserErrorGroup.getParserErrors(this);
|
|
158
|
-
if (errors2.length
|
|
158
|
+
if (errors2.length) {
|
|
159
159
|
throw new JSONParserErrorGroup(this);
|
|
160
160
|
}
|
|
161
161
|
return this.schema!;
|
|
@@ -297,7 +297,7 @@ export class $RefParser {
|
|
|
297
297
|
|
|
298
298
|
public mergeMany(): JSONSchema {
|
|
299
299
|
const schemas = this.schemaMany || [];
|
|
300
|
-
if (schemas.length
|
|
300
|
+
if (!schemas.length) {
|
|
301
301
|
throw ono('mergeMany called with no schemas. Did you run parseMany?');
|
|
302
302
|
}
|
|
303
303
|
|
|
@@ -335,7 +335,7 @@ export class $RefParser {
|
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
|
-
if (Object.keys(infoAccumulator).length
|
|
338
|
+
if (Object.keys(infoAccumulator).length) {
|
|
339
339
|
merged.info = infoAccumulator;
|
|
340
340
|
}
|
|
341
341
|
|
|
@@ -356,7 +356,7 @@ export class $RefParser {
|
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
|
-
if (servers.length
|
|
359
|
+
if (servers.length) {
|
|
360
360
|
merged.servers = servers;
|
|
361
361
|
}
|
|
362
362
|
|
|
@@ -527,24 +527,48 @@ export class $RefParser {
|
|
|
527
527
|
}
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
const HTTP_METHODS = new Set([
|
|
531
|
+
'delete',
|
|
532
|
+
'get',
|
|
533
|
+
'head',
|
|
534
|
+
'options',
|
|
535
|
+
'patch',
|
|
536
|
+
'post',
|
|
537
|
+
'put',
|
|
538
|
+
'trace',
|
|
539
|
+
]);
|
|
540
|
+
|
|
530
541
|
const srcPaths = (schema.paths || {}) as Record<string, any>;
|
|
531
542
|
for (const [p, item] of Object.entries(srcPaths)) {
|
|
532
|
-
let targetPath = p;
|
|
533
543
|
if (merged.paths[p]) {
|
|
534
|
-
const
|
|
535
|
-
|
|
544
|
+
const newMethods = Object.keys(item as object).filter((k) => HTTP_METHODS.has(k));
|
|
545
|
+
const hasMethodConflict = newMethods.some((m) => merged.paths[p][m] !== undefined);
|
|
546
|
+
const rewritten = cloneAndRewrite(
|
|
547
|
+
item,
|
|
548
|
+
refMap,
|
|
549
|
+
tagMap,
|
|
550
|
+
prefix,
|
|
551
|
+
url.stripHash(sourcePath),
|
|
552
|
+
);
|
|
553
|
+
if (hasMethodConflict) {
|
|
554
|
+
const trimmed = p.startsWith('/') ? p.substring(1) : p;
|
|
555
|
+
merged.paths[`/${prefix}/${trimmed}`] = rewritten;
|
|
556
|
+
} else {
|
|
557
|
+
Object.assign(merged.paths[p], rewritten);
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
merged.paths[p] = cloneAndRewrite(
|
|
561
|
+
item,
|
|
562
|
+
refMap,
|
|
563
|
+
tagMap,
|
|
564
|
+
prefix,
|
|
565
|
+
url.stripHash(sourcePath),
|
|
566
|
+
);
|
|
536
567
|
}
|
|
537
|
-
merged.paths[targetPath] = cloneAndRewrite(
|
|
538
|
-
item,
|
|
539
|
-
refMap,
|
|
540
|
-
tagMap,
|
|
541
|
-
prefix,
|
|
542
|
-
url.stripHash(sourcePath),
|
|
543
|
-
);
|
|
544
568
|
}
|
|
545
569
|
}
|
|
546
570
|
|
|
547
|
-
if (tags.length
|
|
571
|
+
if (tags.length) {
|
|
548
572
|
merged.tags = tags;
|
|
549
573
|
}
|
|
550
574
|
|
|
@@ -561,3 +585,17 @@ export class $RefParser {
|
|
|
561
585
|
|
|
562
586
|
export { sendRequest } from './resolvers/url';
|
|
563
587
|
export type { JSONSchema } from './types';
|
|
588
|
+
export type { JSONParserErrorType } from './util/errors';
|
|
589
|
+
export {
|
|
590
|
+
InvalidPointerError,
|
|
591
|
+
isHandledError,
|
|
592
|
+
JSONParserError,
|
|
593
|
+
JSONParserErrorGroup,
|
|
594
|
+
MissingPointerError,
|
|
595
|
+
normalizeError,
|
|
596
|
+
ParserError,
|
|
597
|
+
ResolverError,
|
|
598
|
+
TimeoutError,
|
|
599
|
+
UnmatchedParserError,
|
|
600
|
+
UnmatchedResolverError,
|
|
601
|
+
} from './util/errors';
|
package/src/parsers/yaml.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { JSON_SCHEMA } from 'js-yaml';
|
|
1
|
+
import { parse } from 'yaml';
|
|
3
2
|
|
|
4
3
|
import type { FileInfo, JSONSchema, Plugin } from '../types';
|
|
5
4
|
import { ParserError } from '../util/errors';
|
|
@@ -16,8 +15,7 @@ export const yamlParser: Plugin = {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
try {
|
|
19
|
-
|
|
20
|
-
return yamlSchema;
|
|
18
|
+
return parse(data) as JSONSchema;
|
|
21
19
|
} catch (error: any) {
|
|
22
20
|
throw new ParserError(error?.message || 'Parser Error', file.url);
|
|
23
21
|
}
|
package/src/pointer.ts
CHANGED
|
@@ -140,7 +140,7 @@ class Pointer<S extends object = JSONSchema> {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
if (errors.length
|
|
143
|
+
if (errors.length) {
|
|
144
144
|
throw errors.length === 1
|
|
145
145
|
? errors[0]
|
|
146
146
|
: new AggregateError(errors, 'Multiple missing pointer errors');
|
|
@@ -171,7 +171,7 @@ class Pointer<S extends object = JSONSchema> {
|
|
|
171
171
|
const tokens = Pointer.parse(this.path);
|
|
172
172
|
let token;
|
|
173
173
|
|
|
174
|
-
if (tokens.length
|
|
174
|
+
if (!tokens.length) {
|
|
175
175
|
// There are no tokens, replace the entire object with the new value
|
|
176
176
|
this.value = value;
|
|
177
177
|
return value;
|
|
@@ -205,7 +205,7 @@ class Pointer<S extends object = JSONSchema> {
|
|
|
205
205
|
/**
|
|
206
206
|
* Parses a JSON pointer (or a path containing a JSON pointer in the hash)
|
|
207
207
|
* and returns an array of the pointer's tokens.
|
|
208
|
-
* (e.g
|
|
208
|
+
* (e.g., "schema.json#/definitions/person/name" => ["definitions", "person", "name"])
|
|
209
209
|
*
|
|
210
210
|
* The pointer is parsed according to RFC 6901
|
|
211
211
|
* {@link https://tools.ietf.org/html/rfc6901#section-3}
|
|
@@ -244,8 +244,8 @@ class Pointer<S extends object = JSONSchema> {
|
|
|
244
244
|
/**
|
|
245
245
|
* Creates a JSON pointer path, by joining one or more tokens to a base path.
|
|
246
246
|
*
|
|
247
|
-
* @param base - The base path (e.g
|
|
248
|
-
* @param tokens - The token(s) to append (e.g
|
|
247
|
+
* @param base - The base path (e.g., "schema.json#/definitions/person")
|
|
248
|
+
* @param tokens - The token(s) to append (e.g., ["name", "first"])
|
|
249
249
|
* @returns
|
|
250
250
|
*/
|
|
251
251
|
static join(base: string, tokens: string | string[]) {
|
package/src/ref.ts
CHANGED
|
@@ -46,7 +46,7 @@ class $Ref<S extends object = JSONSchema> {
|
|
|
46
46
|
$refs: $Refs<S>;
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Indicates the type of {@link $Ref#path} (e.g
|
|
49
|
+
* Indicates the type of {@link $Ref#path} (e.g., "file", "http", etc.)
|
|
50
50
|
*/
|
|
51
51
|
pathType: string | unknown;
|
|
52
52
|
|
|
@@ -152,7 +152,7 @@ class $Ref<S extends object = JSONSchema> {
|
|
|
152
152
|
value !== null &&
|
|
153
153
|
'$ref' in value &&
|
|
154
154
|
typeof value.$ref === 'string' &&
|
|
155
|
-
value.$ref.length
|
|
155
|
+
Boolean(value.$ref.length)
|
|
156
156
|
);
|
|
157
157
|
}
|
|
158
158
|
|
package/src/refs.ts
CHANGED
|
@@ -224,7 +224,7 @@ function getPaths<S extends object = JSONSchema>($refs: $RefsMap<S>, types: stri
|
|
|
224
224
|
|
|
225
225
|
// Filter the paths by type
|
|
226
226
|
types = Array.isArray(types[0]) ? types[0] : Array.prototype.slice.call(types);
|
|
227
|
-
if (types.length
|
|
227
|
+
if (types.length && types[0]) {
|
|
228
228
|
paths = paths.filter((key) => types.includes($refs[key]!.pathType as string));
|
|
229
229
|
}
|
|
230
230
|
|