@apidevtools/json-schema-ref-parser 15.3.1 → 15.3.3
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/dist/lib/bundle.js +14 -11
- package/dist/lib/dereference.js +17 -21
- package/dist/lib/index.js +3 -0
- package/dist/lib/parse.d.ts +6 -1
- package/dist/lib/parse.js +15 -1
- package/dist/lib/pointer.d.ts +4 -0
- package/dist/lib/pointer.js +30 -4
- package/dist/lib/ref.d.ts +6 -2
- package/dist/lib/ref.js +31 -2
- package/dist/lib/refs.d.ts +5 -0
- package/dist/lib/refs.js +35 -9
- package/dist/lib/resolve-external.js +22 -9
- package/dist/lib/types/index.d.ts +11 -0
- package/dist/lib/util/schema-resources.d.ts +6 -0
- package/dist/lib/util/schema-resources.js +72 -0
- package/lib/bundle.ts +49 -9
- package/lib/dereference.ts +24 -14
- package/lib/index.ts +3 -0
- package/lib/parse.ts +22 -1
- package/lib/pointer.ts +33 -4
- package/lib/ref.ts +39 -1
- package/lib/refs.ts +44 -9
- package/lib/resolve-external.ts +45 -7
- package/lib/types/index.ts +13 -0
- package/lib/util/schema-resources.ts +109 -0
- package/package.json +12 -12
|
@@ -3,6 +3,7 @@ import Pointer from "./pointer.js";
|
|
|
3
3
|
import parse from "./parse.js";
|
|
4
4
|
import * as url from "./util/url.js";
|
|
5
5
|
import { isHandledError } from "./util/errors.js";
|
|
6
|
+
import { getSchemaBasePath } from "./util/schema-resources.js";
|
|
6
7
|
/**
|
|
7
8
|
* Crawls the JSON schema, finds all external JSON references, and resolves their values.
|
|
8
9
|
* This method does not mutate the JSON schema. The resolved values are added to {@link $RefParser#$refs}.
|
|
@@ -20,7 +21,7 @@ function resolveExternal(parser, options) {
|
|
|
20
21
|
}
|
|
21
22
|
try {
|
|
22
23
|
// console.log('Resolving $ref pointers in %s', parser.$refs._root$Ref.path);
|
|
23
|
-
const promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", parser.$refs, options);
|
|
24
|
+
const promises = crawl(parser.schema, parser.$refs._root$Ref.path + "#", parser.$refs._root$Ref.path, parser.$refs._root$Ref.dynamicIdScope, parser.$refs, options);
|
|
24
25
|
return Promise.all(promises);
|
|
25
26
|
}
|
|
26
27
|
catch (e) {
|
|
@@ -43,19 +44,21 @@ function resolveExternal(parser, options) {
|
|
|
43
44
|
* If any of the JSON references point to files that contain additional JSON references,
|
|
44
45
|
* then the corresponding promise will internally reference an array of promises.
|
|
45
46
|
*/
|
|
46
|
-
function crawl(obj, path, $refs, options, seen, external) {
|
|
47
|
+
function crawl(obj, path, scopeBase, dynamicIdScope, $refs, options, seen, external) {
|
|
47
48
|
seen ||= new Set();
|
|
48
49
|
let promises = [];
|
|
49
50
|
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !seen.has(obj)) {
|
|
50
51
|
seen.add(obj); // Track previously seen objects to avoid infinite recursion
|
|
52
|
+
const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
|
|
51
53
|
if ($Ref.isExternal$Ref(obj)) {
|
|
52
|
-
promises.push(resolve$Ref(obj, path, $refs, options));
|
|
54
|
+
promises.push(resolve$Ref(obj, path, currentScopeBase, dynamicIdScope, $refs, options));
|
|
53
55
|
}
|
|
54
56
|
const keys = Object.keys(obj);
|
|
55
57
|
for (const key of keys) {
|
|
56
58
|
const keyPath = Pointer.join(path, key);
|
|
57
59
|
const value = obj[key];
|
|
58
|
-
|
|
60
|
+
const childScopeBase = dynamicIdScope && $Ref.isExternal$Ref(value) ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
|
|
61
|
+
promises = promises.concat(crawl(value, keyPath, childScopeBase, dynamicIdScope, $refs, options, seen, external));
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
return promises;
|
|
@@ -72,23 +75,33 @@ function crawl(obj, path, $refs, options, seen, external) {
|
|
|
72
75
|
* The promise resolves once all JSON references in the object have been resolved,
|
|
73
76
|
* including nested references that are contained in externally-referenced files.
|
|
74
77
|
*/
|
|
75
|
-
async function resolve$Ref($ref, path, $refs, options) {
|
|
78
|
+
async function resolve$Ref($ref, path, scopeBase, dynamicIdScope, $refs, options) {
|
|
76
79
|
const shouldResolveOnCwd = options.dereference?.externalReferenceResolution === "root";
|
|
77
|
-
const
|
|
80
|
+
const resolutionBase = shouldResolveOnCwd ? url.cwd() : dynamicIdScope ? scopeBase : path;
|
|
81
|
+
const resolvedPath = url.resolve(resolutionBase, $ref.$ref);
|
|
78
82
|
const withoutHash = url.stripHash(resolvedPath);
|
|
79
83
|
// $ref.$ref = url.relative($refs._root$Ref.path, resolvedPath);
|
|
80
84
|
// Do we already have this $ref?
|
|
81
|
-
const ref = $refs.
|
|
85
|
+
const ref = $refs._get$Ref(withoutHash);
|
|
82
86
|
if (ref) {
|
|
83
87
|
// We've already parsed this $ref, so use the existing value
|
|
84
88
|
return Promise.resolve(ref.value);
|
|
85
89
|
}
|
|
86
90
|
// Parse the $referenced file/url
|
|
87
91
|
try {
|
|
88
|
-
const
|
|
92
|
+
const reference = $ref.$ref;
|
|
93
|
+
const parseTarget = {
|
|
94
|
+
url: resolvedPath,
|
|
95
|
+
baseUrl: resolutionBase,
|
|
96
|
+
};
|
|
97
|
+
if (typeof reference === "string") {
|
|
98
|
+
parseTarget.reference = reference;
|
|
99
|
+
}
|
|
100
|
+
const result = await parse(parseTarget, $refs, options);
|
|
89
101
|
// Crawl the parsed value
|
|
90
102
|
// console.log('Resolving $ref pointers in %s', withoutHash);
|
|
91
|
-
const
|
|
103
|
+
const parsedRef = $refs._get$Ref(withoutHash);
|
|
104
|
+
const promises = crawl(result, withoutHash + "#", withoutHash, parsedRef?.dynamicIdScope ?? false, $refs, options, new Set(), true);
|
|
92
105
|
return Promise.all(promises);
|
|
93
106
|
}
|
|
94
107
|
catch (err) {
|
|
@@ -103,6 +103,17 @@ export interface FileInfo {
|
|
|
103
103
|
* The full URL of the file. This could be any type of URL, including "http://", "https://", "file://", "ftp://", "mongodb://", or even a local filesystem path (when running in Node.js).
|
|
104
104
|
*/
|
|
105
105
|
url: string;
|
|
106
|
+
/**
|
|
107
|
+
* The unresolved `$ref` value exactly as it appeared in the parent schema, without the hash.
|
|
108
|
+
* This is useful for custom resolvers that need to preserve relative reference semantics instead
|
|
109
|
+
* of relying on the already-resolved `url`.
|
|
110
|
+
*/
|
|
111
|
+
reference?: string;
|
|
112
|
+
/**
|
|
113
|
+
* The URL that `reference` was resolved against to produce `url`.
|
|
114
|
+
* This may include a JSON Pointer fragment when the reference originated from a nested location.
|
|
115
|
+
*/
|
|
116
|
+
baseUrl?: string;
|
|
106
117
|
/**
|
|
107
118
|
* The hash (URL fragment) of the file URL, including the # symbol. If the URL doesn't have a hash, then this will be an empty string.
|
|
108
119
|
*/
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ParserOptions } from "../options.js";
|
|
2
|
+
import type { JSONSchema } from "../index.js";
|
|
3
|
+
import type $Refs from "../refs.js";
|
|
4
|
+
export declare function getSchemaBasePath(basePath: string, value: unknown): string;
|
|
5
|
+
export declare function usesDynamicIdScope(value: unknown): boolean;
|
|
6
|
+
export declare function registerSchemaResources<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>($refs: $Refs<S, O>, basePath: string, value: unknown, pathType?: string | unknown, dynamicIdScope?: boolean): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as url from "./url.js";
|
|
2
|
+
export function getSchemaBasePath(basePath, value) {
|
|
3
|
+
const schemaId = getSchemaId(value);
|
|
4
|
+
return schemaId ? url.resolve(basePath, schemaId) : basePath;
|
|
5
|
+
}
|
|
6
|
+
export function usesDynamicIdScope(value) {
|
|
7
|
+
if (!value || typeof value !== "object" || ArrayBuffer.isView(value)) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const schema = value.$schema;
|
|
11
|
+
if (typeof schema === "string" &&
|
|
12
|
+
(schema.includes("draft/2019-09/") || schema.includes("draft/2020-12/") || schema.includes("oas/3.1/"))) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
const openapi = value.openapi;
|
|
16
|
+
return typeof openapi === "string" && /^3\.1(?:\.|$)/.test(openapi);
|
|
17
|
+
}
|
|
18
|
+
export function registerSchemaResources($refs, basePath, value, pathType, dynamicIdScope = false) {
|
|
19
|
+
if (!dynamicIdScope) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const visit = (node, scopeBase, pointerTokens) => {
|
|
24
|
+
if (!node || typeof node !== "object" || ArrayBuffer.isView(node) || seen.has(node)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
seen.add(node);
|
|
28
|
+
const nextScopeBase = getSchemaBasePath(scopeBase, node);
|
|
29
|
+
const resourcePointerTokens = nextScopeBase === scopeBase ? pointerTokens : [];
|
|
30
|
+
if (nextScopeBase !== scopeBase) {
|
|
31
|
+
$refs._addAlias(nextScopeBase, node, pathType, dynamicIdScope);
|
|
32
|
+
}
|
|
33
|
+
registerAnchorAliases($refs, nextScopeBase, resourcePointerTokens, node);
|
|
34
|
+
for (const key of Object.keys(node)) {
|
|
35
|
+
visit(node[key], nextScopeBase, [...resourcePointerTokens, key]);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
visit(value, basePath, []);
|
|
39
|
+
}
|
|
40
|
+
function getSchemaId(value) {
|
|
41
|
+
if (value &&
|
|
42
|
+
typeof value === "object" &&
|
|
43
|
+
"$id" in value &&
|
|
44
|
+
typeof value.$id === "string" &&
|
|
45
|
+
value.$id.length > 0) {
|
|
46
|
+
return value.$id;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function registerAnchorAliases($refs, scopeBase, pointerTokens, value) {
|
|
51
|
+
if (!value || typeof value !== "object" || ArrayBuffer.isView(value)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const resourceBase = url.stripHash(scopeBase);
|
|
55
|
+
const targetPath = pointerTokens.length > 0 ? joinPointerPath(resourceBase, pointerTokens) : `${resourceBase}#`;
|
|
56
|
+
const anchors = [
|
|
57
|
+
value.$anchor,
|
|
58
|
+
value.$dynamicAnchor,
|
|
59
|
+
];
|
|
60
|
+
for (const anchor of anchors) {
|
|
61
|
+
if (typeof anchor === "string" && anchor.length > 0) {
|
|
62
|
+
$refs._addExactAlias(`${resourceBase}#${anchor}`, targetPath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function joinPointerPath(basePath, tokens) {
|
|
67
|
+
let path = `${basePath}#`;
|
|
68
|
+
for (const token of tokens) {
|
|
69
|
+
path += `/${token.replace(/~/g, "~0").replace(/\//g, "~1")}`;
|
|
70
|
+
}
|
|
71
|
+
return path;
|
|
72
|
+
}
|
package/lib/bundle.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import $Ref from "./ref.js";
|
|
2
2
|
import Pointer from "./pointer.js";
|
|
3
3
|
import * as url from "./util/url.js";
|
|
4
|
+
import { getSchemaBasePath } from "./util/schema-resources.js";
|
|
4
5
|
import type $Refs from "./refs.js";
|
|
5
6
|
import type $RefParser from "./index.js";
|
|
6
7
|
import type { ParserOptions } from "./index.js";
|
|
@@ -37,7 +38,18 @@ function bundle<S extends object = JSONSchema, O extends ParserOptions<S> = Pars
|
|
|
37
38
|
|
|
38
39
|
// Build an inventory of all $ref pointers in the JSON Schema
|
|
39
40
|
const inventory: InventoryEntry[] = [];
|
|
40
|
-
crawl<S, O>(
|
|
41
|
+
crawl<S, O>(
|
|
42
|
+
parser,
|
|
43
|
+
"schema",
|
|
44
|
+
parser.$refs._root$Ref.path + "#",
|
|
45
|
+
parser.$refs._root$Ref.path!,
|
|
46
|
+
parser.$refs._root$Ref.dynamicIdScope,
|
|
47
|
+
"#",
|
|
48
|
+
0,
|
|
49
|
+
inventory,
|
|
50
|
+
parser.$refs,
|
|
51
|
+
options,
|
|
52
|
+
);
|
|
41
53
|
|
|
42
54
|
// Get the root schema's $id (if any) for qualifying refs inside sub-schemas with their own $id
|
|
43
55
|
const rootId =
|
|
@@ -71,6 +83,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
71
83
|
parent: object | $RefParser<S, O>,
|
|
72
84
|
key: string | null,
|
|
73
85
|
path: string,
|
|
86
|
+
scopeBase: string,
|
|
87
|
+
dynamicIdScope: boolean,
|
|
74
88
|
pathFromRoot: string,
|
|
75
89
|
indirections: number,
|
|
76
90
|
inventory: InventoryEntry[],
|
|
@@ -82,8 +96,9 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
82
96
|
const isExcludedPath = bundleOptions.excludedPathMatcher || (() => false);
|
|
83
97
|
|
|
84
98
|
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
|
|
100
|
+
if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
|
|
101
|
+
inventory$Ref(parent, key, path, currentScopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options);
|
|
87
102
|
} else {
|
|
88
103
|
// Crawl the object in a specific order that's optimized for bundling.
|
|
89
104
|
// This is important because it determines how `pathFromRoot` gets built,
|
|
@@ -107,10 +122,22 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
107
122
|
const keyPathFromRoot = Pointer.join(pathFromRoot, key);
|
|
108
123
|
const value = obj[key];
|
|
109
124
|
|
|
110
|
-
if ($Ref.isAllowed$Ref(value)) {
|
|
111
|
-
|
|
125
|
+
if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
|
|
126
|
+
const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
|
|
127
|
+
inventory$Ref(
|
|
128
|
+
obj,
|
|
129
|
+
key,
|
|
130
|
+
keyPath,
|
|
131
|
+
valueScopeBase,
|
|
132
|
+
dynamicIdScope,
|
|
133
|
+
keyPathFromRoot,
|
|
134
|
+
indirections,
|
|
135
|
+
inventory,
|
|
136
|
+
$refs,
|
|
137
|
+
options,
|
|
138
|
+
);
|
|
112
139
|
} else {
|
|
113
|
-
crawl(obj, key, keyPath, keyPathFromRoot, indirections, inventory, $refs, options);
|
|
140
|
+
crawl(obj, key, keyPath, currentScopeBase, dynamicIdScope, keyPathFromRoot, indirections, inventory, $refs, options);
|
|
114
141
|
}
|
|
115
142
|
|
|
116
143
|
// We need to ensure that we have an object to work with here because we may be crawling
|
|
@@ -142,6 +169,8 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
142
169
|
$refParent: any,
|
|
143
170
|
$refKey: string | null,
|
|
144
171
|
path: string,
|
|
172
|
+
scopeBase: string,
|
|
173
|
+
dynamicIdScope: boolean,
|
|
145
174
|
pathFromRoot: string,
|
|
146
175
|
indirections: number,
|
|
147
176
|
inventory: InventoryEntry[],
|
|
@@ -149,7 +178,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
149
178
|
options: O,
|
|
150
179
|
) {
|
|
151
180
|
const $ref = $refKey === null ? $refParent : $refParent[$refKey];
|
|
152
|
-
const $refPath = url.resolve(path, $ref.$ref);
|
|
181
|
+
const $refPath = url.resolve(dynamicIdScope ? scopeBase : path, $ref.$ref);
|
|
153
182
|
const pointer = $refs._resolve($refPath, pathFromRoot, options);
|
|
154
183
|
if (pointer === null) {
|
|
155
184
|
return;
|
|
@@ -158,7 +187,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
158
187
|
const depth = parsed.length;
|
|
159
188
|
const file = url.stripHash(pointer.path);
|
|
160
189
|
const hash = url.getHash(pointer.path);
|
|
161
|
-
const external = file !== $refs._root$Ref.path;
|
|
190
|
+
const external = file !== $refs._root$Ref.path && !$refs._aliases[file];
|
|
162
191
|
const extended = $Ref.isExtended$Ref($ref);
|
|
163
192
|
indirections += pointer.indirections;
|
|
164
193
|
|
|
@@ -189,7 +218,18 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
189
218
|
|
|
190
219
|
// Recursively crawl the resolved value
|
|
191
220
|
if (!existingEntry || external) {
|
|
192
|
-
crawl(
|
|
221
|
+
crawl(
|
|
222
|
+
pointer.value,
|
|
223
|
+
null,
|
|
224
|
+
pointer.path,
|
|
225
|
+
pointer.$ref.path!,
|
|
226
|
+
pointer.$ref.dynamicIdScope,
|
|
227
|
+
pathFromRoot,
|
|
228
|
+
indirections + 1,
|
|
229
|
+
inventory,
|
|
230
|
+
$refs,
|
|
231
|
+
options,
|
|
232
|
+
);
|
|
193
233
|
}
|
|
194
234
|
}
|
|
195
235
|
|
package/lib/dereference.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import $Ref from "./ref.js";
|
|
2
2
|
import Pointer from "./pointer.js";
|
|
3
3
|
import * as url from "./util/url.js";
|
|
4
|
+
import { getSchemaBasePath } from "./util/schema-resources.js";
|
|
4
5
|
import type $Refs from "./refs.js";
|
|
5
6
|
import type { DereferenceOptions, ParserOptions } from "./options.js";
|
|
6
7
|
import { type $RefParser, type JSONSchema } from "./index.js";
|
|
@@ -24,6 +25,8 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
|
|
|
24
25
|
const dereferenced = crawl<S, O>(
|
|
25
26
|
parser.schema,
|
|
26
27
|
parser.$refs._root$Ref.path!,
|
|
28
|
+
parser.$refs._root$Ref.path!,
|
|
29
|
+
parser.$refs._root$Ref.dynamicIdScope,
|
|
27
30
|
"#",
|
|
28
31
|
new Set(),
|
|
29
32
|
new Set(),
|
|
@@ -55,6 +58,8 @@ function dereference<S extends object = JSONSchema, O extends ParserOptions<S> =
|
|
|
55
58
|
function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
|
|
56
59
|
obj: any,
|
|
57
60
|
path: string,
|
|
61
|
+
scopeBase: string,
|
|
62
|
+
dynamicIdScope: boolean,
|
|
58
63
|
pathFromRoot: string,
|
|
59
64
|
parents: Set<any>,
|
|
60
65
|
processedObjects: Set<any>,
|
|
@@ -87,11 +92,14 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
87
92
|
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
|
|
88
93
|
parents.add(obj);
|
|
89
94
|
processedObjects.add(obj);
|
|
95
|
+
const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
|
|
90
96
|
|
|
91
|
-
if ($Ref.isAllowed$Ref(obj, options)) {
|
|
97
|
+
if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
|
|
92
98
|
dereferenced = dereference$Ref(
|
|
93
99
|
obj,
|
|
94
100
|
path,
|
|
101
|
+
currentScopeBase,
|
|
102
|
+
dynamicIdScope,
|
|
95
103
|
pathFromRoot,
|
|
96
104
|
parents,
|
|
97
105
|
processedObjects,
|
|
@@ -117,10 +125,13 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
117
125
|
const value = obj[key];
|
|
118
126
|
let circular;
|
|
119
127
|
|
|
120
|
-
if ($Ref.isAllowed$Ref(value, options)) {
|
|
128
|
+
if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
|
|
129
|
+
const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
|
|
121
130
|
dereferenced = dereference$Ref(
|
|
122
131
|
value,
|
|
123
132
|
keyPath,
|
|
133
|
+
valueScopeBase,
|
|
134
|
+
dynamicIdScope,
|
|
124
135
|
keyPathFromRoot,
|
|
125
136
|
parents,
|
|
126
137
|
processedObjects,
|
|
@@ -172,6 +183,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
172
183
|
dereferenced = crawl(
|
|
173
184
|
value,
|
|
174
185
|
keyPath,
|
|
186
|
+
currentScopeBase,
|
|
187
|
+
dynamicIdScope,
|
|
175
188
|
keyPathFromRoot,
|
|
176
189
|
parents,
|
|
177
190
|
processedObjects,
|
|
@@ -219,6 +232,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
|
|
|
219
232
|
function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
|
|
220
233
|
$ref: any,
|
|
221
234
|
path: string,
|
|
235
|
+
scopeBase: string,
|
|
236
|
+
dynamicIdScope: boolean,
|
|
222
237
|
pathFromRoot: string,
|
|
223
238
|
parents: Set<any>,
|
|
224
239
|
processedObjects: any,
|
|
@@ -230,7 +245,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
|
|
|
230
245
|
) {
|
|
231
246
|
const isExternalRef = $Ref.isExternal$Ref($ref);
|
|
232
247
|
const shouldResolveOnCwd = isExternalRef && options?.dereference?.externalReferenceResolution === "root";
|
|
233
|
-
const
|
|
248
|
+
const resolutionBase = shouldResolveOnCwd ? url.cwd() : dynamicIdScope ? scopeBase : path;
|
|
249
|
+
const $refPath = url.resolve(resolutionBase, $ref.$ref);
|
|
234
250
|
|
|
235
251
|
const cache = dereferencedCache.get($refPath);
|
|
236
252
|
|
|
@@ -241,18 +257,10 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
|
|
|
241
257
|
// If the cached object however is _not_ circular and there are additional keys alongside our
|
|
242
258
|
// `$ref` pointer here we should merge them back in and return that.
|
|
243
259
|
if (!cache.circular) {
|
|
244
|
-
|
|
245
|
-
if (refKeys.length > 1) {
|
|
246
|
-
const extraKeys = {};
|
|
247
|
-
for (const key of refKeys) {
|
|
248
|
-
if (key !== "$ref" && !(key in cache.value)) {
|
|
249
|
-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
250
|
-
extraKeys[key] = $ref[key];
|
|
251
|
-
}
|
|
252
|
-
}
|
|
260
|
+
if (Object.keys($ref).length > 1) {
|
|
253
261
|
return {
|
|
254
262
|
circular: cache.circular,
|
|
255
|
-
value:
|
|
263
|
+
value: $Ref.dereference($ref, cache.value, options, dynamicIdScope),
|
|
256
264
|
};
|
|
257
265
|
}
|
|
258
266
|
|
|
@@ -302,7 +310,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
|
|
|
302
310
|
}
|
|
303
311
|
|
|
304
312
|
// Dereference the JSON reference
|
|
305
|
-
let dereferencedValue = $Ref.dereference($ref, pointer.value, options);
|
|
313
|
+
let dereferencedValue = $Ref.dereference($ref, pointer.value, options, dynamicIdScope);
|
|
306
314
|
|
|
307
315
|
// Crawl the dereferenced value (unless it's circular)
|
|
308
316
|
if (!circular) {
|
|
@@ -310,6 +318,8 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
|
|
|
310
318
|
const dereferenced = crawl(
|
|
311
319
|
dereferencedValue,
|
|
312
320
|
pointer.path,
|
|
321
|
+
pointer.$ref.path!,
|
|
322
|
+
pointer.$ref.dynamicIdScope,
|
|
313
323
|
pathFromRoot,
|
|
314
324
|
parents,
|
|
315
325
|
processedObjects,
|
package/lib/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
JSONParserErrorGroup,
|
|
20
20
|
} from "./util/errors.js";
|
|
21
21
|
import maybe from "./util/maybe.js";
|
|
22
|
+
import { registerSchemaResources, usesDynamicIdScope } from "./util/schema-resources.js";
|
|
22
23
|
import type { ParserOptions } from "./options.js";
|
|
23
24
|
import { getJsonSchemaRefParserDefaultOptions } from "./options.js";
|
|
24
25
|
import type {
|
|
@@ -115,6 +116,8 @@ export class $RefParser<S extends object = JSONSchema, O extends ParserOptions<S
|
|
|
115
116
|
const $ref = this.$refs._add(args.path);
|
|
116
117
|
$ref.value = args.schema;
|
|
117
118
|
$ref.pathType = pathType;
|
|
119
|
+
$ref.dynamicIdScope = usesDynamicIdScope($ref.value);
|
|
120
|
+
registerSchemaResources(this.$refs, $ref.path!, $ref.value, $ref.pathType, $ref.dynamicIdScope);
|
|
118
121
|
promise = Promise.resolve(args.schema);
|
|
119
122
|
} else {
|
|
120
123
|
// Parse the schema file/url
|
package/lib/parse.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as url from "./util/url.js";
|
|
2
|
+
import { registerSchemaResources, usesDynamicIdScope } from "./util/schema-resources.js";
|
|
2
3
|
import { filter, all, sort, run } from "./util/plugins.js";
|
|
3
4
|
import {
|
|
4
5
|
ResolverError,
|
|
@@ -11,14 +12,24 @@ import type $Refs from "./refs.js";
|
|
|
11
12
|
import type { ParserOptions } from "./options.js";
|
|
12
13
|
import type { FileInfo, JSONSchema } from "./types/index.js";
|
|
13
14
|
|
|
15
|
+
interface ParseTarget {
|
|
16
|
+
url: string;
|
|
17
|
+
reference?: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
/**
|
|
15
22
|
* Reads and parses the specified file path or URL.
|
|
16
23
|
*/
|
|
17
24
|
async function parse<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
|
|
18
|
-
|
|
25
|
+
target: string | ParseTarget,
|
|
19
26
|
$refs: $Refs<S, O>,
|
|
20
27
|
options: O,
|
|
21
28
|
) {
|
|
29
|
+
let path = typeof target === "string" ? target : target.url;
|
|
30
|
+
const baseUrl = typeof target === "string" ? undefined : target.baseUrl;
|
|
31
|
+
let reference = typeof target === "string" ? undefined : target.reference;
|
|
32
|
+
|
|
22
33
|
// Remove the URL fragment, if any
|
|
23
34
|
const hashIndex = path.indexOf("#");
|
|
24
35
|
let hash = "";
|
|
@@ -27,6 +38,12 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
|
|
|
27
38
|
// Remove the URL fragment, if any
|
|
28
39
|
path = path.substring(0, hashIndex);
|
|
29
40
|
}
|
|
41
|
+
if (reference) {
|
|
42
|
+
const referenceHashIndex = reference.indexOf("#");
|
|
43
|
+
if (referenceHashIndex >= 0) {
|
|
44
|
+
reference = reference.substring(0, referenceHashIndex);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
30
47
|
|
|
31
48
|
// Add a new $Ref for this file, even though we don't have the value yet.
|
|
32
49
|
// This ensures that we don't simultaneously read & parse the same file multiple times
|
|
@@ -37,6 +54,8 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
|
|
|
37
54
|
url: path,
|
|
38
55
|
hash,
|
|
39
56
|
extension: url.getExtension(path),
|
|
57
|
+
...(reference !== undefined ? { reference } : {}),
|
|
58
|
+
...(baseUrl !== undefined ? { baseUrl } : {}),
|
|
40
59
|
} as FileInfo;
|
|
41
60
|
|
|
42
61
|
// Read the file and then parse the data
|
|
@@ -47,6 +66,8 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
|
|
|
47
66
|
|
|
48
67
|
const parser = await parseFile<S, O>(file, options, $refs);
|
|
49
68
|
$ref.value = parser.result;
|
|
69
|
+
$ref.dynamicIdScope = usesDynamicIdScope($ref.value);
|
|
70
|
+
registerSchemaResources($refs, $ref.path!, $ref.value, $ref.pathType, $ref.dynamicIdScope);
|
|
50
71
|
|
|
51
72
|
return parser.result;
|
|
52
73
|
} catch (err) {
|
package/lib/pointer.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ParserOptions } from "./options.js";
|
|
|
3
3
|
import $Ref from "./ref.js";
|
|
4
4
|
import * as url from "./util/url.js";
|
|
5
5
|
import { JSONParserError, InvalidPointerError, MissingPointerError, isHandledError } from "./util/errors.js";
|
|
6
|
+
import { getSchemaBasePath } from "./util/schema-resources.js";
|
|
6
7
|
import type { JSONSchema } from "./index.js";
|
|
7
8
|
import type { JSONSchema4Type, JSONSchema6Type, JSONSchema7Type } from "json-schema";
|
|
8
9
|
|
|
@@ -38,6 +39,11 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
38
39
|
*/
|
|
39
40
|
originalPath: string;
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The current base URI used to resolve nested $ref pointers while walking this pointer.
|
|
44
|
+
*/
|
|
45
|
+
scopeBase: string;
|
|
46
|
+
|
|
41
47
|
/**
|
|
42
48
|
* The value of the JSON pointer.
|
|
43
49
|
* Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
|
|
@@ -61,6 +67,8 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
61
67
|
|
|
62
68
|
this.originalPath = friendlyPath || path;
|
|
63
69
|
|
|
70
|
+
this.scopeBase = $ref.path || url.stripHash(path);
|
|
71
|
+
|
|
64
72
|
this.value = undefined;
|
|
65
73
|
|
|
66
74
|
this.circular = false;
|
|
@@ -87,6 +95,9 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
87
95
|
|
|
88
96
|
// Crawl the object, one token at a time
|
|
89
97
|
this.value = unwrapOrThrow(obj);
|
|
98
|
+
if (this.$ref.dynamicIdScope) {
|
|
99
|
+
this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
|
|
100
|
+
}
|
|
90
101
|
|
|
91
102
|
for (let i = 0; i < tokens.length; i++) {
|
|
92
103
|
// During token walking, if the current value is an extended $ref (has sibling keys
|
|
@@ -149,10 +160,14 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
found.push(token);
|
|
163
|
+
if (this.$ref.dynamicIdScope) {
|
|
164
|
+
this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
|
|
165
|
+
}
|
|
152
166
|
}
|
|
153
167
|
|
|
154
168
|
// Resolve the final value
|
|
155
|
-
|
|
169
|
+
const finalResolutionBase = this.$ref.dynamicIdScope ? this.scopeBase : this.path;
|
|
170
|
+
if (!this.value || (this.value.$ref && url.resolve(finalResolutionBase, this.value.$ref) !== pathFromRoot)) {
|
|
156
171
|
resolveIf$Ref(this, options, pathFromRoot);
|
|
157
172
|
}
|
|
158
173
|
|
|
@@ -181,6 +196,9 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
181
196
|
|
|
182
197
|
// Crawl the object, one token at a time
|
|
183
198
|
this.value = unwrapOrThrow(obj);
|
|
199
|
+
if (this.$ref.dynamicIdScope) {
|
|
200
|
+
this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
|
|
201
|
+
}
|
|
184
202
|
|
|
185
203
|
for (let i = 0; i < tokens.length - 1; i++) {
|
|
186
204
|
resolveIf$Ref(this, options);
|
|
@@ -193,6 +211,10 @@ class Pointer<S extends object = JSONSchema, O extends ParserOptions<S> = Parser
|
|
|
193
211
|
// The token doesn't exist, so create it
|
|
194
212
|
this.value = setValue(this, token, {});
|
|
195
213
|
}
|
|
214
|
+
|
|
215
|
+
if (this.$ref.dynamicIdScope) {
|
|
216
|
+
this.scopeBase = getSchemaBasePath(this.scopeBase, this.value);
|
|
217
|
+
}
|
|
196
218
|
}
|
|
197
219
|
|
|
198
220
|
// Set the value of the final token
|
|
@@ -286,8 +308,9 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
286
308
|
) {
|
|
287
309
|
// Is the value a JSON reference? (and allowed?)
|
|
288
310
|
|
|
289
|
-
if ($Ref.isAllowed$Ref(pointer.value, options)) {
|
|
290
|
-
const
|
|
311
|
+
if ($Ref.isAllowed$Ref(pointer.value, options, pointer.$ref.dynamicIdScope)) {
|
|
312
|
+
const resolutionBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.path;
|
|
313
|
+
const $refPath = url.resolve(resolutionBase, pointer.value.$ref);
|
|
291
314
|
|
|
292
315
|
if ($refPath === pointer.path && !isRootPath(pathFromRoot)) {
|
|
293
316
|
// The value is a reference to itself, so there's nothing to do.
|
|
@@ -303,13 +326,19 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
|
|
|
303
326
|
if ($Ref.isExtended$Ref(pointer.value)) {
|
|
304
327
|
// This JSON reference "extends" the resolved value, rather than simply pointing to it.
|
|
305
328
|
// So the resolved path does NOT change. Just the value does.
|
|
306
|
-
pointer.value = $Ref.dereference(pointer.value, resolved.value, options);
|
|
329
|
+
pointer.value = $Ref.dereference(pointer.value, resolved.value, options, pointer.$ref.dynamicIdScope);
|
|
330
|
+
if (pointer.$ref.dynamicIdScope) {
|
|
331
|
+
pointer.scopeBase = getSchemaBasePath(pointer.scopeBase, pointer.value);
|
|
332
|
+
}
|
|
307
333
|
return false;
|
|
308
334
|
} else {
|
|
309
335
|
// Resolve the reference
|
|
310
336
|
pointer.$ref = resolved.$ref;
|
|
311
337
|
pointer.path = resolved.path;
|
|
312
338
|
pointer.value = resolved.value;
|
|
339
|
+
pointer.scopeBase = pointer.$ref.dynamicIdScope
|
|
340
|
+
? getSchemaBasePath(pointer.$ref.path!, pointer.value)
|
|
341
|
+
: pointer.$ref.path!;
|
|
313
342
|
}
|
|
314
343
|
|
|
315
344
|
return true;
|