Skip to content

Commit 4205dcf

Browse files
committed
add: merge meta-properties resolving $ref
1 parent 7de578c commit 4205dcf

File tree

5 files changed

+82
-14
lines changed

5 files changed

+82
-14
lines changed

src/compileSchema.getNode.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,5 +506,31 @@ describe("compileSchema : getNode", () => {
506506
assert.deepEqual(error.data.schema, schema.items, "should have exposed json-schema of error location");
507507
assert.deepEqual(error.data?.oneOf, schema.items.oneOf, "should have exposed oneOf array on data");
508508
});
509+
510+
describe("$ref", () => {
511+
it("should resolve $ref", () => {
512+
const { node } = compileSchema({
513+
type: "array",
514+
prefixItems: [{ $ref: "/$defs/target" }],
515+
$defs: {
516+
target: { type: "array", minItems: 2 }
517+
}
518+
}).getNode("#/0");
519+
520+
assert.deepEqual(node.schema, { type: "array", minItems: 2 });
521+
});
522+
523+
it("should mrege title from local schema", () => {
524+
const { node } = compileSchema({
525+
type: "array",
526+
prefixItems: [{ title: "from ref", $ref: "/$defs/target" }],
527+
$defs: {
528+
target: { title: "from $defs", type: "array", minItems: 2 }
529+
}
530+
}).getNode("#/0");
531+
532+
assert.deepEqual(node.schema, { title: "from ref", type: "array", minItems: 2 });
533+
});
534+
});
509535
});
510536
});

src/keywords/$ref.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { isObject } from "../utils/isObject";
77
import { validateNode } from "../validateNode";
88
import { get, split } from "@sagold/json-pointer";
99
import { mergeNode } from "../mergeNode";
10+
import { pick } from "../utils/pick";
1011

1112
export const $refKeyword: Keyword = {
1213
id: "$ref",
@@ -108,6 +109,7 @@ export function resolveRef({ pointer, path }: { pointer?: string; path?: Validat
108109
if (resolvedNode != null) {
109110
path?.push({ pointer, node: resolvedNode });
110111
}
112+
111113
return resolvedNode;
112114
}
113115

@@ -128,22 +130,22 @@ function resolveRecursiveRef(node: SchemaNode, path: ValidationPath): SchemaNode
128130
const nonMatchingDynamicAnchor = node.context.dynamicAnchors[refInCurrentScope] == null;
129131
if (nonMatchingDynamicAnchor) {
130132
if (node.context.anchors[refInCurrentScope]) {
131-
return compileNext(node.context.anchors[refInCurrentScope], node.evaluationPath);
133+
return compileNext(node.context.anchors[refInCurrentScope], node);
132134
}
133135
}
134136

135137
for (let i = 0; i < history.length; i += 1) {
136138
// A $dynamicRef that initially resolves to a schema with a matching $dynamicAnchor resolves to the first $dynamicAnchor in the dynamic scope
137139
if (history[i].node.schema.$dynamicAnchor) {
138-
return compileNext(history[i].node, node.evaluationPath);
140+
return compileNext(history[i].node, node);
139141
}
140142

141143
// A $dynamicRef only stops at a $dynamicAnchor if it is in the same dynamic scope.
142144
const refWithoutScope = node.schema.$dynamicRef.split("#").pop();
143145
const ref = joinId(history[i].node.$id, `#${refWithoutScope}`);
144146
const anchorNode = node.context.dynamicAnchors[ref];
145147
if (anchorNode) {
146-
return compileNext(node.context.dynamicAnchors[ref], node.evaluationPath);
148+
return compileNext(node.context.dynamicAnchors[ref], node);
147149
}
148150
}
149151

@@ -153,12 +155,21 @@ function resolveRecursiveRef(node: SchemaNode, path: ValidationPath): SchemaNode
153155
return nextNode;
154156
}
155157

156-
function compileNext(referencedNode: SchemaNode, evaluationPath = referencedNode.evaluationPath) {
157-
const referencedSchema = isObject(referencedNode.schema)
158-
? omit(referencedNode.schema, "$id")
159-
: referencedNode.schema;
158+
const PROPERTIES_TO_MERGE = ["title", "description", "options", "readOnly", "writeOnly"];
160159

161-
return referencedNode.compileSchema(referencedSchema, `${evaluationPath}/$ref`, referencedNode.schemaLocation);
160+
function compileNext(referencedNode: SchemaNode, sourceNode: SchemaNode) {
161+
let referencedSchema = referencedNode.schema;
162+
if (isObject(referencedNode.schema)) {
163+
referencedSchema = {
164+
...omit(referencedNode.schema, "$id"),
165+
...pick(sourceNode.schema, ...PROPERTIES_TO_MERGE)
166+
};
167+
}
168+
return referencedNode.compileSchema(
169+
referencedSchema,
170+
`${sourceNode.evaluationPath}/$ref`,
171+
referencedNode.schemaLocation
172+
);
162173
}
163174

164175
export function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode | undefined {
@@ -168,16 +179,16 @@ export function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode | undefi
168179

169180
// resolve $ref by json-evaluationPath
170181
if (node.context.refs[$ref]) {
171-
return compileNext(node.context.refs[$ref], node.evaluationPath);
182+
return compileNext(node.context.refs[$ref], node);
172183
}
173184
// resolve $ref from $anchor
174185
if (node.context.anchors[$ref]) {
175-
return compileNext(node.context.anchors[$ref], node.evaluationPath);
186+
return compileNext(node.context.anchors[$ref], node);
176187
}
177188
// resolve $ref from $dynamicAnchor
178189
if (node.context.dynamicAnchors[$ref]) {
179190
// A $ref to a $dynamicAnchor in the same schema resource behaves like a normal $ref to an $anchor
180-
return compileNext(node.context.dynamicAnchors[$ref], node.evaluationPath);
191+
return compileNext(node.context.dynamicAnchors[$ref], node);
181192
}
182193

183194
// check for remote-host + pointer pair to switch rootSchema
@@ -191,7 +202,7 @@ export function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode | undefi
191202
const $ref = fragments[0];
192203
// this is a reference to remote-host root node
193204
if (node.context.remotes[$ref]) {
194-
return compileNext(node.context.remotes[$ref], node.evaluationPath);
205+
return compileNext(node.context.remotes[$ref], node);
195206
}
196207
if ($ref[0] === "#") {
197208
// support refOfUnknownKeyword

src/methods/getData.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,19 @@ describe("getData", () => {
681681
{ first: "first", second: "second" }
682682
]);
683683
});
684+
685+
it("should resolve $ref in items-object", () => {
686+
const data = compileSchema({
687+
minItems: 1,
688+
items: { $ref: "/$defs/bool" },
689+
$defs: {
690+
bool: { type: "boolean", default: true }
691+
}
692+
}).getData();
693+
assert(Array.isArray(data));
694+
assert.deepEqual(data.length, 1);
695+
assert.deepEqual(data, [true]);
696+
});
684697
});
685698

686699
describe("prefixItems: []", () => {
@@ -820,6 +833,21 @@ describe("getData", () => {
820833
).getData();
821834
assert.deepEqual(data, [true]);
822835
});
836+
837+
it("should resolve $ref in prefixItems", () => {
838+
const node = compileSchema({
839+
type: "array",
840+
minItems: 2,
841+
prefixItems: [{ $ref: "/$defs/bool" }, { $ref: "/$defs/string" }],
842+
$defs: {
843+
bool: { type: "boolean" },
844+
string: { type: "string" }
845+
}
846+
});
847+
const res = node.getData([]);
848+
849+
assert.deepEqual(res, [false, ""]);
850+
});
823851
});
824852

825853
describe("oneOf", () => {

src/tests/docs/remoteSchema.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ describe("docs - remote schema", () => {
6363
}
6464
}
6565
}).addRemoteSchema("https://remote.com/schema.json", {
66-
title: "$defs remote schema",
6766
$defs: {
6867
character: {
6968
type: "string",

src/utils/pick.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export function pick<T extends { [P in keyof T]: unknown }, K extends keyof T>(v
55
return value;
66
}
77
const result = {} as Pick<T, K>;
8-
properties.forEach((property) => (result[property] = value[property]));
8+
properties.forEach((property) => {
9+
if (value[property] !== undefined) {
10+
result[property] = value[property];
11+
}
12+
});
913
return result;
1014
}

0 commit comments

Comments
 (0)