Skip to content

Commit f3f7e35

Browse files
committed
fix: support 2020-12 anchors and ref siblings
Fixes #145 Closes #419
1 parent c355f09 commit f3f7e35

File tree

9 files changed

+235
-59
lines changed

9 files changed

+235
-59
lines changed

lib/bundle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
9797

9898
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
9999
const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
100-
if ($Ref.isAllowed$Ref(obj)) {
100+
if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
101101
inventory$Ref(parent, key, path, currentScopeBase, dynamicIdScope, pathFromRoot, indirections, inventory, $refs, options);
102102
} else {
103103
// Crawl the object in a specific order that's optimized for bundling.
@@ -122,7 +122,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
122122
const keyPathFromRoot = Pointer.join(pathFromRoot, key);
123123
const value = obj[key];
124124

125-
if ($Ref.isAllowed$Ref(value)) {
125+
if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
126126
const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
127127
inventory$Ref(
128128
obj,

lib/dereference.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
9494
processedObjects.add(obj);
9595
const currentScopeBase = dynamicIdScope ? getSchemaBasePath(scopeBase, obj) : scopeBase;
9696

97-
if ($Ref.isAllowed$Ref(obj, options)) {
97+
if ($Ref.isAllowed$Ref(obj, options, dynamicIdScope)) {
9898
dereferenced = dereference$Ref(
9999
obj,
100100
path,
@@ -125,7 +125,7 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
125125
const value = obj[key];
126126
let circular;
127127

128-
if ($Ref.isAllowed$Ref(value, options)) {
128+
if ($Ref.isAllowed$Ref(value, options, dynamicIdScope)) {
129129
const valueScopeBase = dynamicIdScope ? getSchemaBasePath(currentScopeBase, value) : currentScopeBase;
130130
dereferenced = dereference$Ref(
131131
value,
@@ -257,18 +257,10 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
257257
// If the cached object however is _not_ circular and there are additional keys alongside our
258258
// `$ref` pointer here we should merge them back in and return that.
259259
if (!cache.circular) {
260-
const refKeys = Object.keys($ref);
261-
if (refKeys.length > 1) {
262-
const extraKeys = {};
263-
for (const key of refKeys) {
264-
if (key !== "$ref" && !(key in cache.value)) {
265-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
266-
extraKeys[key] = $ref[key];
267-
}
268-
}
260+
if (Object.keys($ref).length > 1) {
269261
return {
270262
circular: cache.circular,
271-
value: Object.assign({}, cache.value, extraKeys),
263+
value: $Ref.dereference($ref, cache.value, options, dynamicIdScope),
272264
};
273265
}
274266

@@ -318,7 +310,7 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
318310
}
319311

320312
// Dereference the JSON reference
321-
let dereferencedValue = $Ref.dereference($ref, pointer.value, options);
313+
let dereferencedValue = $Ref.dereference($ref, pointer.value, options, dynamicIdScope);
322314

323315
// Crawl the dereferenced value (unless it's circular)
324316
if (!circular) {

lib/pointer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
308308
) {
309309
// Is the value a JSON reference? (and allowed?)
310310

311-
if ($Ref.isAllowed$Ref(pointer.value, options)) {
311+
if ($Ref.isAllowed$Ref(pointer.value, options, pointer.$ref.dynamicIdScope)) {
312312
const resolutionBase = pointer.$ref.dynamicIdScope ? pointer.scopeBase : pointer.path;
313313
const $refPath = url.resolve(resolutionBase, pointer.value.$ref);
314314

@@ -326,7 +326,7 @@ function resolveIf$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
326326
if ($Ref.isExtended$Ref(pointer.value)) {
327327
// This JSON reference "extends" the resolved value, rather than simply pointing to it.
328328
// So the resolved path does NOT change. Just the value does.
329-
pointer.value = $Ref.dereference(pointer.value, resolved.value, options);
329+
pointer.value = $Ref.dereference(pointer.value, resolved.value, options, pointer.$ref.dynamicIdScope);
330330
if (pointer.$ref.dynamicIdScope) {
331331
pointer.scopeBase = getSchemaBasePath(pointer.scopeBase, pointer.value);
332332
}

lib/ref.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,18 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
199199
* @param options
200200
* @returns
201201
*/
202-
static isAllowed$Ref<S extends object = JSONSchema>(value: unknown, options?: ParserOptions<S>) {
202+
static isAllowed$Ref<S extends object = JSONSchema>(
203+
value: unknown,
204+
options?: ParserOptions<S>,
205+
allowPlainNameFragments = false,
206+
) {
203207
if (this.is$Ref(value)) {
204208
if (value.$ref.substring(0, 2) === "#/" || value.$ref === "#") {
205209
// It's a JSON Pointer reference, which is always allowed
206210
return true;
211+
} else if (allowPlainNameFragments && value.$ref[0] === "#") {
212+
// JSON Schema 2019-09+ allows plain-name fragments declared via $anchor/$dynamicAnchor
213+
return true;
207214
} else if (value.$ref[0] !== "#" && (!options || options.resolve?.external)) {
208215
// It's an external reference, which is allowed by the options
209216
return true;
@@ -286,7 +293,12 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
286293
$ref: $Ref<S, O>,
287294
resolvedValue: S,
288295
options?: O,
296+
useSpecCompliantRefSiblings = false,
289297
): S {
298+
if ($Ref.isExtended$Ref($ref) && useSpecCompliantRefSiblings) {
299+
return preserveRefSiblings($ref, resolvedValue) as S;
300+
}
301+
290302
if (resolvedValue && typeof resolvedValue === "object" && $Ref.isExtended$Ref($ref)) {
291303
const merged = {} as Partial<S>;
292304
for (const key of Object.keys($ref)) {
@@ -325,6 +337,27 @@ class $Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOpt
325337
}
326338
}
327339

340+
function preserveRefSiblings<T>(refValue: Record<string, any>, resolvedValue: T): T {
341+
const preserved = {} as Record<string, any>;
342+
const existingAllOf = "allOf" in refValue ? refValue.allOf : undefined;
343+
344+
for (const key of Object.keys(refValue)) {
345+
if (key !== "$ref" && key !== "allOf") {
346+
preserved[key] = refValue[key];
347+
}
348+
}
349+
350+
const allOf = [resolvedValue];
351+
if (Array.isArray(existingAllOf)) {
352+
allOf.push(...existingAllOf);
353+
} else if (existingAllOf !== undefined) {
354+
allOf.push(existingAllOf);
355+
}
356+
357+
preserved.allOf = allOf;
358+
return preserved as T;
359+
}
360+
328361
function deepMerge<T>(target: Partial<T>, source: Partial<T>): T {
329362
//return {...target, ...source};
330363

lib/refs.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
112112
*/
113113
_get$Ref(path: string) {
114114
path = url.resolve(this._root$Ref.path!, path);
115-
return this._getRef(path);
115+
return this._getRef(this._exactAliases[path] || path);
116116
}
117117

118118
/**
@@ -149,6 +149,15 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
149149
return $ref;
150150
}
151151

152+
_addExactAlias(path: string, targetPath: string) {
153+
if (!path || this._exactAliases[path]) {
154+
return this._exactAliases[path];
155+
}
156+
157+
this._exactAliases[path] = targetPath;
158+
return targetPath;
159+
}
160+
152161
/**
153162
* Resolves the given JSON reference.
154163
*
@@ -160,13 +169,14 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
160169
*/
161170
_resolve(path: string, pathFromRoot: string, options?: O) {
162171
const absPath = url.resolve(this._root$Ref.path!, path);
163-
const $ref = this._getRef(absPath);
172+
const canonicalPath = this._exactAliases[absPath] || absPath;
173+
const $ref = this._getRef(canonicalPath);
164174

165175
if (!$ref) {
166176
throw new Error(`Error resolving $ref pointer "${path}". \n"${url.stripHash(absPath)}" not found.`);
167177
}
168178

169-
return $ref.resolve(absPath, options, path, pathFromRoot);
179+
return $ref.resolve(canonicalPath, options, path, pathFromRoot);
170180
}
171181

172182
/**
@@ -179,6 +189,8 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
179189

180190
_aliases: $RefsMap<S, O> = {};
181191

192+
_exactAliases: Record<string, string> = {};
193+
182194
/**
183195
* The {@link $Ref} object that is the root of the JSON schema.
184196
*
@@ -197,6 +209,7 @@ export default class $Refs<S extends object = JSONSchema, O extends ParserOption
197209

198210
this._$refs = {};
199211
this._aliases = {};
212+
this._exactAliases = {};
200213

201214
// @ts-ignore
202215
this._root$Ref = null;

lib/util/schema-resources.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,26 @@ export function registerSchemaResources<S extends object = JSONSchema, O extends
3838

3939
const seen = new Set<object>();
4040

41-
const visit = (node: unknown, scopeBase: string) => {
41+
const visit = (node: unknown, scopeBase: string, pointerTokens: string[]) => {
4242
if (!node || typeof node !== "object" || ArrayBuffer.isView(node) || seen.has(node)) {
4343
return;
4444
}
4545

4646
seen.add(node);
4747

4848
const nextScopeBase = getSchemaBasePath(scopeBase, node);
49+
const resourcePointerTokens = nextScopeBase === scopeBase ? pointerTokens : [];
4950
if (nextScopeBase !== scopeBase) {
5051
$refs._addAlias(nextScopeBase, node as S, pathType, dynamicIdScope);
5152
}
53+
registerAnchorAliases($refs, nextScopeBase, resourcePointerTokens, node);
5254

5355
for (const key of Object.keys(node)) {
54-
visit((node as Record<string, unknown>)[key], nextScopeBase);
56+
visit((node as Record<string, unknown>)[key], nextScopeBase, [...resourcePointerTokens, key]);
5557
}
5658
};
5759

58-
visit(value, basePath);
60+
visit(value, basePath, []);
5961
}
6062

6163
function getSchemaId(value: unknown): string | undefined {
@@ -71,3 +73,37 @@ function getSchemaId(value: unknown): string | undefined {
7173

7274
return undefined;
7375
}
76+
77+
function registerAnchorAliases<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
78+
$refs: $Refs<S, O>,
79+
scopeBase: string,
80+
pointerTokens: string[],
81+
value: unknown,
82+
) {
83+
if (!value || typeof value !== "object" || ArrayBuffer.isView(value)) {
84+
return;
85+
}
86+
87+
const resourceBase = url.stripHash(scopeBase);
88+
const targetPath = pointerTokens.length > 0 ? joinPointerPath(resourceBase, pointerTokens) : `${resourceBase}#`;
89+
const anchors = [
90+
(value as { $anchor?: unknown }).$anchor,
91+
(value as { $dynamicAnchor?: unknown }).$dynamicAnchor,
92+
];
93+
94+
for (const anchor of anchors) {
95+
if (typeof anchor === "string" && anchor.length > 0) {
96+
$refs._addExactAlias(`${resourceBase}#${anchor}`, targetPath);
97+
}
98+
}
99+
}
100+
101+
function joinPointerPath(basePath: string, tokens: string[]) {
102+
let path = `${basePath}#`;
103+
104+
for (const token of tokens) {
105+
path += `/${token.replace(/~/g, "~0").replace(/\//g, "~1")}`;
106+
}
107+
108+
return path;
109+
}

test/specs/defs/dereferencedA.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
}
3131
},
3232
{
33-
"type": "number",
33+
"type": "object",
3434
"format": "float"
3535
}
3636
]
@@ -76,7 +76,7 @@
7676
}
7777
},
7878
{
79-
"type": "number",
79+
"type": "object",
8080
"format": "float"
8181
}
8282
]
@@ -110,7 +110,7 @@
110110
}
111111
},
112112
{
113-
"type": "number",
113+
"type": "object",
114114
"format": "float"
115115
}
116116
]

0 commit comments

Comments
 (0)