Skip to content

Commit 162d50e

Browse files
committed
introduce: dynamicId
1 parent 17d253a commit 162d50e

File tree

10 files changed

+251
-21
lines changed

10 files changed

+251
-21
lines changed

src/SchemaNode.ts

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,39 +39,47 @@ function getDraft(drafts: Draft[], $schema: string) {
3939
}
4040

4141
export type Context = {
42-
/** root node of this json-schema */
42+
/** root node of this JSON Schema */
4343
rootNode: SchemaNode;
4444
/** available draft configurations */
4545
drafts: Draft[];
46-
/** [SHARED ACROSS REMOTES] root nodes of registered remote json-schema, stored by id/url */
46+
/** [SHARED ACROSS REMOTES] root nodes of registered remote JSON Schema, stored by id/url */
4747
remotes: Record<string, SchemaNode>;
4848
/** references stored by fully resolved schema-$id + local-pointer */
4949
refs: Record<string, SchemaNode>;
5050
/** anchors stored by fully resolved schema-$id + $anchor */
5151
anchors: Record<string, SchemaNode>;
5252
/** [SHARED ACROSS REMOTES] dynamicAnchors stored by fully resolved schema-$id + $anchor */
5353
dynamicAnchors: Record<string, SchemaNode>;
54-
/** json-schema parser, validator, reducer and resolver for this json-schema (root-schema and its child nodes) */
54+
/** JSON Schema parser, validator, reducer and resolver for this JSON Schema (root schema and its child nodes) */
5555
keywords: Draft["keywords"];
56-
/** json-schema draft-dependend methods */
56+
/** JSON Schema draft dependend methods */
5757
methods: Draft["methods"];
58-
/** draft-version */
58+
/** draft version */
5959
version: Draft["version"];
60+
/** draft errors & template-strings */
6061
errors: Draft["errors"];
62+
/** draft formats & validators */
6163
formats: Draft["formats"];
6264
/** [SHARED USING ADD REMOTE] getData default options */
6365
templateDefaultOptions?: TemplateOptions;
6466
};
6567

6668
export interface SchemaNode extends SchemaNodeMethodsType {
6769
context: Context;
70+
/** JSON Schema of node */
6871
schema: JsonSchema;
72+
/** absolute path into JSON Schema, includes $ref for resolved schema */
6973
spointer: string;
70-
/** local path within json-schema (not extended by resolving ref) */
74+
/** local path within JSON Schema (not extended by resolving ref) */
7175
schemaId: string;
76+
/** id created when combining subschemas */
77+
dynamicId: string;
78+
/** reference to parent node (node used to compile this node) */
7279
parent?: SchemaNode | undefined;
73-
/** json-pointer from last $id ~~to this location~~ to resolve $refs to $id#/idLocalPointer */
80+
/** JSON Pointer from last $id ~~to this location~~ to resolve $refs to $id#/idLocalPointer */
7481
lastIdPointer: string;
82+
/** when reduced schema containing `oneOf` schema, `oneOfIndex` stores `oneOf`-item used for merge */
7583
oneOfIndex?: number;
7684

7785
reducers: JsonSchemaReducer[];
@@ -97,7 +105,7 @@ export interface SchemaNode extends SchemaNodeMethodsType {
97105
items?: SchemaNode;
98106
not?: SchemaNode;
99107
oneOf?: SchemaNode[];
100-
patternProperties?: { pattern: RegExp; node: SchemaNode }[];
108+
patternProperties?: { name: string; pattern: RegExp; node: SchemaNode }[];
101109
properties?: Record<string, SchemaNode>;
102110
propertyNames?: SchemaNode;
103111
then?: SchemaNode;
@@ -124,19 +132,41 @@ export type GetSchemaOptions = {
124132
pointer?: string;
125133
};
126134

135+
export function joinDynamicId(a?: string, b?: string) {
136+
if (a == b) {
137+
return a ?? "";
138+
}
139+
if (a == null || b == null) {
140+
return a || b;
141+
}
142+
if (a.startsWith(b)) {
143+
return a;
144+
}
145+
if (b.startsWith(a)) {
146+
return b;
147+
}
148+
return `${a}+${b}`;
149+
}
150+
127151
export const SchemaNodeMethods = {
128152
/**
129153
* Compiles a child-schema of this node to its context
130154
* @returns SchemaNode representing the passed JSON Schema
131155
*/
132-
compileSchema(schema: JsonSchema, spointer: string = this.spointer, schemaId?: string): SchemaNode {
156+
compileSchema(
157+
schema: JsonSchema,
158+
spointer: string = this.spointer,
159+
schemaId?: string,
160+
dynamicId?: string
161+
): SchemaNode {
133162
const nextFragment = spointer.split("/$ref")[0];
134163
const parentNode = this as SchemaNode;
135164
const node: SchemaNode = {
136165
lastIdPointer: parentNode.lastIdPointer, // ref helper
137166
context: parentNode.context,
138167
parent: parentNode,
139168
spointer,
169+
dynamicId: joinDynamicId(parentNode.dynamicId, dynamicId),
140170
schemaId: schemaId ?? join(parentNode.schemaId, nextFragment),
141171
reducers: [],
142172
resolvers: [],
@@ -170,9 +200,9 @@ export const SchemaNodeMethods = {
170200
},
171201

172202
/**
173-
* Returns a node containing json-schema of a data-json-pointer.
203+
* Returns a node containing JSON Schema of a data-JSON Pointer.
174204
*
175-
* To resolve dynamic schema where the type of json-schema is evaluated by
205+
* To resolve dynamic schema where the type of JSON Schema is evaluated by
176206
* its value, a data object has to be passed in options.
177207
*
178208
* Per default this function will return `undefined` schema for valid properties
@@ -390,7 +420,7 @@ export const SchemaNodeMethods = {
390420
},
391421

392422
/**
393-
* Register a json-schema as a remote-schema to be resolved by $ref, $anchor, etc
423+
* Register a JSON Schema as a remote-schema to be resolved by $ref, $anchor, etc
394424
* @returns the current node (not the remote schema-node)
395425
*/
396426
addRemote(url: string, schema: JsonSchema): SchemaNode {
@@ -403,6 +433,7 @@ export const SchemaNodeMethods = {
403433
spointer: "#",
404434
lastIdPointer: "#",
405435
schemaId: "#",
436+
dynamicId: "",
406437
reducers: [],
407438
resolvers: [],
408439
validators: [],

src/compileSchema.reduceSchema.test.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,4 +676,145 @@ describe("compileSchema : reduceSchema", () => {
676676
});
677677
});
678678
});
679+
680+
describe("dynamicId", () => {
681+
it("should add dynamicId based on merge anyOf schema", () => {
682+
const { node } = compileSchema({
683+
anyOf: [{ type: "string" }, { minimum: 1 }]
684+
}).reduceSchema(3);
685+
686+
assert.deepEqual(node.dynamicId, "#(anyOf/1)");
687+
});
688+
689+
it("should add dynamicId based on all merged anyOf schema", () => {
690+
const { node } = compileSchema({
691+
anyOf: [{ type: "number" }, { minimum: 1 }]
692+
}).reduceSchema(3);
693+
694+
assert.deepEqual(node.dynamicId, "#(anyOf/0,anyOf/1)");
695+
});
696+
697+
it("should add dynamicId based on merge anyOf schema", () => {
698+
const { node } = compileSchema({
699+
allOf: [{ type: "number" }, { title: "dynamic id" }]
700+
}).reduceSchema(3);
701+
702+
assert.deepEqual(node.dynamicId, "#(allOf/0,allOf/1)");
703+
});
704+
705+
it("should combine dynamicId from `anyOf` and `allOf` schema", () => {
706+
const { node } = compileSchema({
707+
allOf: [{ type: "number" }, { title: "dynamic id" }],
708+
anyOf: [{ minimum: 1 }]
709+
}).reduceSchema(3);
710+
711+
assert.deepEqual(node.dynamicId, "#(allOf/0,allOf/1)+#(anyOf/0)");
712+
});
713+
714+
it("should add dynamicId based on `then` schema", () => {
715+
const { node } = compileSchema({
716+
if: { const: 3 },
717+
then: { type: "string" }
718+
}).reduceSchema(3);
719+
720+
assert.deepEqual(node.dynamicId, "#(then)");
721+
});
722+
723+
it("should add dynamicId based on `else` schema", () => {
724+
const { node } = compileSchema({
725+
if: { const: 3 },
726+
then: { type: "string" },
727+
else: { type: "number" }
728+
}).reduceSchema(2);
729+
730+
assert.deepEqual(node.dynamicId, "#(else)");
731+
});
732+
733+
it("should add dynamicId based on selected `patternProperties`", () => {
734+
const { node } = compileSchema({
735+
patternProperties: {
736+
muh: { type: "string" },
737+
rooar: { type: "bolean" }
738+
}
739+
}).reduceSchema({ muh: "" });
740+
741+
assert.deepEqual(node.dynamicId, "#(patternProperties/muh)");
742+
});
743+
744+
it("should add dynamicId based on selected `dependentSchemas`", () => {
745+
const { node } = compileSchema({
746+
dependentSchemas: {
747+
muh: { properties: { title: { type: "string" } } },
748+
rooar: { properties: { header: { type: "boolean" } } }
749+
}
750+
}).reduceSchema({ muh: "", rooar: true });
751+
752+
assert.deepEqual(node.dynamicId, "#(dependentSchemas/muh,dependentSchemas/rooar)");
753+
});
754+
755+
it("should prefix with schemaId", () => {
756+
const { node } =
757+
compileSchema({
758+
properties: {
759+
counter: { anyOf: [{ type: "number" }, { minimum: 1 }] }
760+
}
761+
})
762+
.getChild("counter", { counter: 3 })
763+
.node?.reduceSchema(3) ?? {};
764+
765+
assert.deepEqual(node.dynamicId, "#/properties/counter(anyOf/0,anyOf/1)");
766+
});
767+
768+
it("should maintain dynamicId through nested reduce-calls", () => {
769+
const { node } =
770+
compileSchema({
771+
allOf: [
772+
{
773+
properties: {
774+
counter: { anyOf: [{ type: "number" }, { minimum: 1 }] }
775+
}
776+
}
777+
]
778+
})
779+
.getChild("counter", { counter: 3 })
780+
.node?.reduceSchema(3) ?? {};
781+
782+
assert.deepEqual(node.dynamicId, "#(allOf/0)+#/properties/counter(anyOf/0,anyOf/1)");
783+
});
784+
785+
it("should add dynamicId from nested reducers in allOf", () => {
786+
const { node } = compileSchema({
787+
allOf: [
788+
{
789+
anyOf: [{ type: "string" }, { minimum: 1 }]
790+
}
791+
]
792+
}).reduceSchema(2);
793+
794+
assert.deepEqual(node.dynamicId, "#(#/allOf/0(anyOf/1))");
795+
});
796+
797+
it("should add dynamicId from nested reducers in anyOf", () => {
798+
const { node } = compileSchema({
799+
anyOf: [
800+
{
801+
allOf: [{ type: "number" }, { minimum: 1 }]
802+
}
803+
]
804+
}).reduceSchema(2);
805+
806+
assert.deepEqual(node.dynamicId, "#(#/anyOf/0(allOf/0,allOf/1))");
807+
});
808+
809+
it("should add dynamicId from nested reducers in then", () => {
810+
const { node } = compileSchema({
811+
if: { minimum: 1 },
812+
then: {
813+
allOf: [{ type: "number" }, { title: "order" }]
814+
}
815+
}).reduceSchema(2);
816+
817+
assert.deepEqual(node.dynamicId, "#/then(allOf/0,allOf/1)");
818+
});
819+
});
679820
});

src/compileSchema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export function compileSchema(schema: JsonSchema, options: Partial<CompileOption
3838
spointer: "#",
3939
lastIdPointer: "#",
4040
schemaId: "#",
41+
dynamicId: "",
4142
reducers: [],
4243
resolvers: [],
4344
validators: [],

src/keywords/allOf.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,24 @@ function reduceAllOf({ node, data }: JsonSchemaReducerParams) {
2626
// note: parts of schemas could be merged, e.g. if they do not include
2727
// dynamic schema parts
2828
let mergedSchema = {};
29+
let dynamicId = "";
2930
for (let i = 0; i < node.allOf.length; i += 1) {
3031
const { node: schemaNode } = node.allOf[i].reduceSchema(data);
3132
if (schemaNode) {
33+
const nestedDynamicId = schemaNode.dynamicId?.replace(node.dynamicId, "") ?? "";
34+
const localDynamicId = nestedDynamicId === "" ? `allOf/${i}` : nestedDynamicId;
35+
dynamicId += `${dynamicId === "" ? "" : ","}${localDynamicId}`;
36+
3237
const schema = mergeSchema(node.allOf[i].schema, schemaNode.schema);
3338
mergedSchema = mergeSchema(mergedSchema, schema, "allOf", "contains");
3439
}
3540
}
36-
return node.compileSchema(mergedSchema, `${node.spointer}/allOf`, node.schemaId);
41+
return node.compileSchema(
42+
mergedSchema,
43+
`${node.spointer}/${dynamicId}`,
44+
node.schemaId,
45+
`${node.schemaId}(${dynamicId})`
46+
);
3747
}
3848

3949
function validateAllOf({ node, data, pointer, path }: JsonSchemaValidatorParams) {

src/keywords/anyOf.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,27 @@ export function parseAnyOf(node: SchemaNode) {
2424

2525
function reduceAnyOf({ node, data, pointer, path }: JsonSchemaReducerParams) {
2626
let mergedSchema = {};
27+
let dynamicId = "";
2728
for (let i = 0; i < node.anyOf.length; i += 1) {
2829
if (validateNode(node.anyOf[i], data, pointer, path).length === 0) {
2930
const { node: schemaNode } = node.anyOf[i].reduceSchema(data);
31+
3032
if (schemaNode) {
33+
const nestedDynamicId = schemaNode.dynamicId?.replace(node.dynamicId, "") ?? "";
34+
const localDynamicId = nestedDynamicId === "" ? `anyOf/${i}` : nestedDynamicId;
35+
dynamicId += `${dynamicId === "" ? "" : ","}${localDynamicId}`;
36+
3137
const schema = mergeSchema(node.anyOf[i].schema, schemaNode.schema);
3238
mergedSchema = mergeSchema(mergedSchema, schema, "anyOf");
3339
}
3440
}
3541
}
36-
return node.compileSchema(mergedSchema, `${node.spointer}/anyOf`, node.schemaId);
42+
return node.compileSchema(
43+
mergedSchema,
44+
`${node.spointer}${dynamicId}`,
45+
node.schemaId,
46+
`${node.schemaId}(${dynamicId})`
47+
);
3748
}
3849

3950
function validateAnyOf({ node, data, pointer, path }: JsonSchemaValidatorParams) {

src/keywords/dependentSchemas.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export function reduceDependentSchemas({ node, data }: JsonSchemaReducerParams)
4848

4949
let mergedSchema: JsonSchema;
5050
const { dependentSchemas } = node;
51+
let added = 0;
52+
let dynamicId = `${node.schemaId}(`;
5153
Object.keys(data).forEach((propertyName) => {
5254
if (dependentSchemas[propertyName] == null) {
5355
return;
@@ -58,14 +60,16 @@ export function reduceDependentSchemas({ node, data }: JsonSchemaReducerParams)
5860
} else {
5961
mergedSchema.properties[propertyName] = dependentSchemas[propertyName];
6062
}
63+
dynamicId += `${added ? "," : ""}dependentSchemas/${propertyName}`;
64+
added++;
6165
});
6266

6367
if (mergedSchema == null) {
6468
return node;
6569
}
6670

6771
mergedSchema = mergeSchema(node.schema, mergedSchema, "dependentSchemas");
68-
return node.compileSchema(mergedSchema, node.spointer, node.schemaId);
72+
return node.compileSchema(mergedSchema, node.spointer, node.schemaId, `${dynamicId})`);
6973
}
7074

7175
export function validateDependentSchemas({ node, data, pointer, path }: JsonSchemaValidatorParams) {

src/keywords/ifthenelse.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,21 @@ function reduceIf({ node, data, pointer, path }: JsonSchemaReducerParams) {
3636
// reduce creates a new node
3737
const { node: schemaNode } = node.then.reduceSchema(data);
3838
if (schemaNode) {
39+
const nestedDynamicId = schemaNode.dynamicId.replace(node.dynamicId, "").replace(/^#/, "");
40+
const dynamicId = nestedDynamicId === "" ? `(then)` : nestedDynamicId;
41+
3942
const schema = mergeSchema(node.then.schema, schemaNode.schema, "if", "then", "else");
40-
return node.compileSchema(schema, node.then.spointer);
43+
return node.compileSchema(schema, node.then.spointer, node.schemaId, `${node.schemaId}${dynamicId}`);
4144
}
4245
}
4346
} else if (node.else) {
4447
const { node: schemaNode } = node.else.reduceSchema(data);
4548
if (schemaNode) {
49+
const nestedDynamicId = schemaNode.dynamicId.replace(node.dynamicId, "");
50+
const dynamicId = nestedDynamicId === "" ? `(else)` : nestedDynamicId;
51+
4652
const schema = mergeSchema(node.else.schema, schemaNode.schema, "if", "then", "else");
47-
return node.compileSchema(schema, node.else.spointer);
53+
return node.compileSchema(schema, node.else.spointer, node.schemaId, `${node.schemaId}${dynamicId}`);
4854
}
4955
}
5056
return undefined;

0 commit comments

Comments
 (0)