json-schema-library
Version:
Customizable and hackable json-validator and json-schema utilities for traversal, data generation and validation
877 lines (789 loc) • 33.9 kB
text/typescript
import { compileSchema } from "./compileSchema";
import { strict as assert } from "assert";
import { isSchemaNode } from "./types";
describe("compileSchema : reduceNode", () => {
describe("behaviour", () => {});
it("should return schema for boolean schema true", () => {
// @ts-expect-error boolean schema still untyped
const node = compileSchema(true);
const schema = node.reduceNode(123)?.node?.schema;
assert.deepEqual(schema, { type: "number" });
});
it("should compile schema with current data", () => {
const node = compileSchema({
type: "object",
if: { required: ["withHeader"], properties: { withHeader: { const: true } } },
then: {
required: ["header"],
properties: { header: { type: "string", minLength: 1 } }
}
});
const dataNode = node.reduceNode({ withHeader: true });
assert.deepEqual(dataNode?.node?.schema, {
type: "object",
required: ["header"],
properties: { header: { type: "string", minLength: 1 } }
});
});
it("should resolve both if-then-else and allOf schema", () => {
const node = compileSchema({
type: "object",
if: { required: ["withHeader"], properties: { withHeader: { const: true } } },
then: {
required: ["header"],
properties: { header: { type: "string", minLength: 1 } }
},
allOf: [{ required: ["date"], properties: { date: { type: "string", format: "date" } } }]
});
const schema = node.reduceNode({ withHeader: true, header: "huhu" })?.node?.schema;
assert.deepEqual(schema, {
type: "object",
required: ["date", "header"],
properties: {
header: { type: "string", minLength: 1 },
date: { type: "string", format: "date" }
}
});
});
describe("object - merge all reduced dynamic schema", () => {
it("should reduce patternProperties and allOf", () => {
const node = compileSchema({
allOf: [{ properties: { "107": { type: "string", maxLength: 99 } } }],
patternProperties: { "[0-1][0-1]7": { type: "string", minLength: 1 } }
});
const schema = node.reduceNode({ "107": undefined })?.node?.schema;
assert.deepEqual(schema, { properties: { "107": { type: "string", minLength: 1, maxLength: 99 } } });
});
});
describe("dependencies", () => {
it("should correctly merge dependencies", () => {
const node: any = compileSchema({
$ref: "#/$defs/schema",
$defs: {
schema: {
type: "object",
required: ["one"],
properties: { one: { type: "string" }, two: { type: "string" } },
dependencies: { one: ["two"], two: { $ref: "#/$defs/two" } }
},
two: { required: ["three"], properties: { three: { type: "number" } } }
}
});
const { node: reduced } = node.reduceNode({ one: "" });
assert.deepEqual(reduced.schema, {
type: "object",
required: ["one", "two", "three"],
properties: { one: { type: "string" }, two: { type: "string" }, three: { type: "number" } }
});
assert.deepEqual(reduced.dynamicId, "#/$defs/schema(dependencies/one,dependencies/two)");
});
it("should add required-property from dependency", () => {
const node: any = compileSchema({
type: "object",
properties: { one: { title: "Property One", type: "string" } },
dependencies: { one: { required: ["two"], properties: { two: { type: "string" } } } }
});
const { node: reduced } = node.reduceNode({ one: "" });
assert.deepEqual(reduced.schema.required, ["two"]);
});
it("should NOT merge dependency when it is not defined", () => {
const node: any = compileSchema({
type: "object",
properties: { one: { type: "string" } },
dependencies: { one: ["two"], two: { type: "number" } }
});
const { node: reduced } = node.reduceNode({});
assert.deepEqual(reduced.schema, {
type: "object",
properties: { one: { type: "string" } }
});
assert.deepEqual(node.dynamicId, "");
});
it("should NOT add dynamic schema if no data matches dependency", () => {
const { node } = compileSchema({
$ref: "#/$defs/schema",
$defs: {
schema: {
type: "object",
required: [],
properties: { one: { type: "string" }, two: { type: "string" } },
dependencies: { one: ["two"] }
}
}
}).reduceNode({});
assert.deepEqual(node.schema, {
type: "object",
required: [],
properties: { one: { type: "string" }, two: { type: "string" } }
});
assert.deepEqual(node.dynamicId, "");
});
it("should resolve nested dependencies schema", () => {
const { node } = compileSchema({
$ref: "#/$defs/schema",
$defs: {
schema: {
type: "object",
required: ["one"],
properties: { one: { type: "string" }, two: { type: "string" } },
dependencies: { one: ["two"], two: { $ref: "/$defs/two" } }
},
two: {
required: ["three"],
properties: { three: { type: "number" } },
dependencies: { two: { properties: { four: { type: "boolean" } } } }
}
}
}).reduceNode({ one: "" });
assert.deepEqual(node.schema, {
type: "object",
required: ["one", "two", "three"],
properties: {
one: { type: "string" },
two: { type: "string" },
three: { type: "number" },
four: { type: "boolean" }
}
});
assert.deepEqual(
node.dynamicId,
"#/$defs/two(dependencies/two)+#/$defs/schema(dependencies/one,#/$defs/two(dependencies/two))"
);
});
});
describe("if-then-else", () => {
it("should select if-then-else schema", () => {
const { node } = compileSchema({
$ref: "#/$defs/schema",
$defs: {
schema: {
type: "object",
required: ["one"],
properties: { one: { type: "string" } },
if: { minProperties: 1 },
then: { $ref: "/$defs/then" }
},
then: { required: ["two"], properties: { two: { type: "string" } } }
}
}).reduceNode({ one: "" });
assert.deepEqual(node.schema, {
type: "object",
required: ["one", "two"],
properties: { one: { type: "string" }, two: { type: "string" } }
});
});
it("should resolve nested if-then-else schema", () => {
const { node } = compileSchema({
$ref: "#/$defs/schema",
$defs: {
schema: {
type: "object",
required: ["one"],
properties: { one: { type: "string" } },
if: { minProperties: 1 },
then: { $ref: "/$defs/then" }
},
then: {
if: { minProperties: 1 },
then: { required: ["two"], properties: { two: { type: "string" } } }
}
}
}).reduceNode({ one: "" });
assert.deepEqual(node.schema, {
type: "object",
required: ["one", "two"],
properties: {
one: { type: "string" },
two: { type: "string" }
}
});
});
});
describe("allOf", () => {
it("should return merged allOf schema", () => {
const { node } = compileSchema({
$ref: "/$defs/schema",
$defs: {
schema: {
type: "object",
required: ["one"],
properties: { one: { type: "string" } },
allOf: [{ $ref: "/$defs/one" }, { $ref: "/$defs/two" }]
},
one: { required: ["one"] },
two: { required: ["two"], properties: { two: { type: "number" } } }
}
}).reduceNode({});
assert.deepEqual(node.schema, {
type: "object",
required: ["one", "two"],
properties: {
one: { type: "string" },
two: { type: "number" }
}
});
});
it("should return undefined if allOf is empty", () => {
const { node } = compileSchema({
type: "object",
required: ["one"],
properties: { one: { type: "string" } },
allOf: []
}).reduceNode({});
assert.deepEqual(node.schema, {
type: "object",
required: ["one"],
properties: { one: { type: "string" } }
});
});
it("should resolve nested allOf schema", () => {
const { node } = compileSchema({
$ref: "/$defs/schema",
$defs: {
schema: {
type: "object",
required: ["one"],
properties: { one: { type: "string" } },
allOf: [{ $ref: "/$defs/one" }, { $ref: "/$defs/two" }]
},
one: { required: ["one"] },
two: { required: ["two"], allOf: [{ properties: { two: { type: "number" } } }] }
}
}).reduceNode({});
assert.deepEqual(node.schema, {
type: "object",
required: ["one", "two"],
properties: { one: { type: "string" }, two: { type: "number" } }
});
});
});
describe("oneOf", () => {
it("should select oneOf schema", () => {
const { node } = compileSchema({
type: "object",
oneOf: [{ properties: { one: { type: "number" } } }, { properties: { two: { type: "string" } } }]
}).reduceNode({ one: "string" });
assert.deepEqual(node.schema, { type: "object", properties: { two: { type: "string" } } });
});
it("should select correct oneOf schema from oneOfProperty", () => {
const { node } = compileSchema({
type: "object",
oneOfProperty: "id",
oneOf: [
{ properties: { id: { const: "first" }, one: { type: "number" } } },
{ properties: { id: { const: "second" }, one: { type: "number" } } }
]
}).reduceNode({ id: "second" });
assert.deepEqual(node.schema, {
type: "object",
oneOfProperty: "id",
properties: { id: { const: "second" }, one: { type: "number" } }
});
});
});
describe("anyOf", () => {
it("should NOT add anyOf schema if no sub.schema matches input data", () => {
const { node } = compileSchema({
type: "object",
anyOf: [{ properties: { id: { const: "first" } } }]
}).reduceNode({ id: "second" });
assert.deepEqual(node.schema, { type: "object" });
});
it("should return matching oneOf schema", () => {
const { node } = compileSchema({
type: "object",
anyOf: [{ properties: { id: { const: "second" } } }]
}).reduceNode({ data: { id: "second" } });
assert.deepEqual(node.schema, { type: "object", properties: { id: { const: "second" } } });
});
it("should return all matching oneOf schema as merged schema", () => {
const { node } = compileSchema({
type: "object",
anyOf: [
{ properties: { id: { const: "second" } } },
{ properties: { id: { minLength: 4 } } },
{ properties: { id: { maxLength: 4 } } }
]
}).reduceNode({ id: "second" });
assert.deepEqual(node.schema, { type: "object", properties: { id: { const: "second", minLength: 4 } } });
});
it("should correctly reduce multiple prefixItems", () => {
const { node } = compileSchema({
prefixItems: [{ const: "foo" }],
anyOf: [{ prefixItems: [true, { const: "bar" }] }, { prefixItems: [true, true, { const: "baz" }] }]
}).reduceNode(["foo", "bar"]);
assert.deepEqual(node.schema, { prefixItems: [true, true, { const: "baz" }] });
});
});
describe("allOf", () => {
it("should return merged schema of type string", () => {
const { node } = compileSchema({
type: "string",
allOf: [{ minLength: 10 }, { pattern: /a-.*/ }]
}).reduceNode("a-value");
assert.deepEqual(node.schema, {
type: "string",
minLength: 10,
pattern: /a-.*/
});
});
it("should return merged schema while resolving $ref", () => {
const { node } = compileSchema({
type: "string",
allOf: [{ $ref: "/$defs/min" }, { $ref: "/$defs/pattern" }],
$defs: {
min: { minLength: 10 },
pattern: { format: "html" }
}
}).reduceNode("a-value");
assert.deepEqual(node.schema, {
type: "string",
minLength: 10,
format: "html"
});
});
it("should return merged properties and attributes", () => {
const { node } = compileSchema({
type: "object",
properties: {
trigger: { type: "boolean" }
},
allOf: [
{
properties: {
title: { type: "string" }
}
},
{
minProperties: 2,
properties: {
time: { type: "number" }
}
}
]
}).reduceNode("a-value");
assert.deepEqual(node.schema, {
type: "object",
minProperties: 2,
properties: {
trigger: { type: "boolean" },
title: { type: "string" },
time: { type: "number" }
}
});
});
it("should return merged required-list of type object", () => {
const { node } = compileSchema({
type: "object",
required: ["trigger"],
properties: {
trigger: { type: "boolean" }
},
allOf: [
{
required: ["title"],
properties: {
title: { type: "string" }
}
},
{
required: [],
properties: {
time: { type: "number" }
}
}
]
}).reduceNode("a-value");
assert.deepEqual(node.schema, {
type: "object",
required: ["trigger", "title"],
properties: {
trigger: { type: "boolean" },
title: { type: "string" },
time: { type: "number" }
}
});
});
it("should return unique merged required-list of type object", () => {
const { node } = compileSchema({
type: "object",
required: ["trigger"],
properties: {
trigger: { type: "boolean" }
},
allOf: [
{
required: ["trigger", "title"],
properties: {
title: { type: "string" }
}
}
]
}).reduceNode("a-value");
assert.deepEqual(node.schema, {
type: "object",
required: ["trigger", "title"],
properties: {
trigger: { type: "boolean" },
title: { type: "string" }
}
});
});
describe("contains", () => {
it("should resolve allOf-contains schema to array-item schema", () => {
const { node } = compileSchema({
allOf: [{ contains: { multipleOf: 2 } }, { contains: { multipleOf: 3 } }]
}).reduceNode([2, 5]);
assert.deepEqual(node.schema, { items: { anyOf: [{ multipleOf: 2 }, { multipleOf: 3 }] } });
});
});
describe("if-then-else", () => {
it("should not return 'then'-schema when 'if' does not match", () => {
const { node } = compileSchema({
type: "object",
required: ["trigger"],
properties: { trigger: { type: "boolean" } },
allOf: [
{
if: { properties: { trigger: { const: true } } },
then: { properties: { additionalSchema: { type: "string", default: "additional" } } }
}
]
}).reduceNode({ trigger: false });
assert.deepEqual(node.schema, {
type: "object",
required: ["trigger"],
properties: {
trigger: { type: "boolean" }
}
});
});
it("should return 'then'-schema when 'if' does match", () => {
const { node } = compileSchema({
type: "object",
required: ["trigger"],
properties: { trigger: { type: "boolean" } },
allOf: [
{
if: { properties: { trigger: { const: true } } },
then: { properties: { additionalSchema: { type: "string", default: "additional" } } }
}
]
}).reduceNode({ trigger: true });
assert.deepEqual(node.schema, {
type: "object",
required: ["trigger"],
properties: {
trigger: { type: "boolean" },
additionalSchema: { type: "string", default: "additional" }
}
});
});
it("should merge multiple 'then'-schema", () => {
const { node } = compileSchema({
type: "object",
required: ["trigger"],
properties: { trigger: { type: "boolean" } },
allOf: [
{
if: { properties: { trigger: { const: true } } },
then: { properties: { additionalSchema: { type: "string", default: "additional" } } }
},
{
if: { properties: { trigger: { const: true } } },
then: { properties: { anotherSchema: { type: "string", default: "another" } } }
}
]
}).reduceNode({ trigger: true });
assert.deepEqual(node.schema, {
type: "object",
required: ["trigger"],
properties: {
trigger: { type: "boolean" },
additionalSchema: { type: "string", default: "additional" },
anotherSchema: { type: "string", default: "another" }
}
});
});
it("should merge only matching 'if'-schema", () => {
const { node } = compileSchema({
type: "object",
required: ["trigger"],
properties: { trigger: { type: "boolean" } },
allOf: [
{
if: { properties: { trigger: { const: true } } },
then: { properties: { additionalSchema: { type: "string", default: "additional" } } }
},
{
if: {
required: ["additionalSchema"],
properties: { additionalSchema: { type: "string", minLength: 50 } }
},
then: { properties: { anotherSchema: { type: "string", default: "another" } } }
}
]
}).reduceNode({ trigger: true });
assert.deepEqual(node.schema, {
type: "object",
required: ["trigger"],
properties: {
trigger: { type: "boolean" },
additionalSchema: { type: "string", default: "additional" }
}
});
});
it("should incrementally resolve multiple 'then'-schema", () => {
const { node } = compileSchema({
type: "object",
required: ["trigger"],
properties: { trigger: { type: "boolean" } },
allOf: [
{
if: { properties: { trigger: { const: true } } },
then: {
required: ["additionalSchema"],
properties: { additionalSchema: { type: "string", default: "additional" } }
}
},
{
if: { required: ["additionalSchema"], properties: { additionalSchema: { minLength: 5 } } },
then: {
required: ["anotherSchema"],
properties: { anotherSchema: { type: "string", default: "another" } }
}
}
]
}).reduceNode({ trigger: true, additionalSchema: "12345" });
assert.deepEqual(node.schema, {
type: "object",
required: ["trigger", "additionalSchema", "anotherSchema"],
properties: {
trigger: { type: "boolean" },
additionalSchema: { type: "string", default: "additional" },
anotherSchema: { type: "string", default: "another" }
}
});
});
});
});
describe("object - recursively resolve dynamic properties", () => {
it("should reduce allOf and oneOf", () => {
const node = compileSchema({
allOf: [
{
oneOf: [
{ type: "string", minLength: 1 },
{ type: "number", minimum: 1 }
]
}
]
});
const schema = node.reduceNode(123)?.node?.schema;
assert.deepEqual(schema, { type: "number", minimum: 1 });
});
it("should reduce oneOf and allOf", () => {
const node = compileSchema({
oneOf: [{ allOf: [{ type: "string", minLength: 1 }] }, { allOf: [{ type: "number", minimum: 1 }] }]
});
const schema = node.reduceNode(123)?.node?.schema;
assert.deepEqual(schema, { type: "number", minimum: 1 });
});
it("should iteratively resolve allOf before merging (issue#44)", () => {
const { node } = compileSchema({
type: "object",
properties: { trigger: { type: "boolean" } },
allOf: [
{
if: {
not: {
properties: { trigger: { type: "boolean", const: true } }
}
},
then: {
properties: { trigger: { type: "boolean", const: false } }
}
},
{
if: {
not: {
properties: { trigger: { type: "boolean", const: false } }
}
},
then: {
properties: { trigger: { const: true } }
}
}
]
}).reduceNode({ trigger: true });
assert(isSchemaNode(node));
assert.deepEqual(node.schema, {
type: "object",
properties: {
trigger: { type: "boolean", const: true }
}
});
});
});
describe("$ref", () => {
it("should resolve to $ref referenced schema", () => {
const { node } = compileSchema({
$ref: "/$defs/one",
$defs: {
one: { type: "boolean", title: "one" }
}
}).reduceNode(3);
assert.deepEqual(node.schema, { type: "boolean", title: "one" });
});
it("should resolve to multiple $ref referenced schema", () => {
const { node } = compileSchema({
$ref: "/$defs/one",
$defs: {
one: { $ref: "/$defs/two" },
two: { type: "boolean", title: "two" }
}
}).reduceNode(3);
assert.deepEqual(node.schema, { type: "boolean", title: "two" });
});
it("should merge nested sub-schema", () => {
const { node } = compileSchema({
$ref: "/$defs/one",
description: "from root",
$defs: {
one: { default: "from one", $ref: "/$defs/two" },
two: { type: "boolean", title: "from two" }
}
}).reduceNode(3);
assert.deepEqual(node.schema, {
description: "from root",
default: "from one",
title: "from two",
type: "boolean"
});
});
});
describe("dynamicId", () => {
it("should add dynamicId based on merge anyOf schema", () => {
const { node } = compileSchema({
anyOf: [{ type: "string" }, { minimum: 1 }]
}).reduceNode(3);
assert.deepEqual(node.dynamicId, "#(anyOf/1)");
});
it("should add dynamicId based on all merged anyOf schema", () => {
const { node } = compileSchema({
anyOf: [{ type: "number" }, { minimum: 1 }]
}).reduceNode(3);
assert.deepEqual(node.dynamicId, "#(anyOf/0,anyOf/1)");
});
it("should add dynamicId based on merge anyOf schema", () => {
const { node } = compileSchema({
allOf: [{ type: "number" }, { title: "dynamic id" }]
}).reduceNode(3);
assert.deepEqual(node.dynamicId, "#(allOf/0,allOf/1)");
});
it("should combine dynamicId from `anyOf` and `allOf` schema", () => {
const { node } = compileSchema({
allOf: [{ type: "number" }, { title: "dynamic id" }],
anyOf: [{ minimum: 1 }]
}).reduceNode(3);
assert.deepEqual(node.dynamicId, "#(allOf/0,allOf/1)+#(anyOf/0)");
});
it("should add dynamicId based on `then` schema", () => {
const { node } = compileSchema({
if: { const: 3 },
then: { type: "string" }
}).reduceNode(3);
assert.deepEqual(node.dynamicId, "#(then)");
});
it("should add dynamicId based on `else` schema", () => {
const { node } = compileSchema({
if: { const: 3 },
then: { type: "string" },
else: { type: "number" }
}).reduceNode(2);
assert.deepEqual(node.dynamicId, "#(else)");
});
it("should add dynamicId based on selected `patternProperties`", () => {
const { node } = compileSchema({
patternProperties: {
muh: { type: "string" },
rooar: { type: "bolean" }
}
}).reduceNode({ muh: "" });
assert.deepEqual(node.dynamicId, "#(patternProperties/muh)");
});
it("should add dynamicId based on selected `dependentSchemas`", () => {
const { node } = compileSchema({
dependentSchemas: {
muh: { properties: { title: { type: "string" } } },
rooar: { properties: { header: { type: "boolean" } } }
}
}).reduceNode({ muh: "", rooar: true });
assert.deepEqual(node.dynamicId, "#(dependentSchemas/muh,dependentSchemas/rooar)");
});
it("should add dynamicId based on selected `dependencies`", () => {
const { node } = compileSchema({
dependencies: {
one: ["two"],
two: { properties: { header: { type: "boolean" } } }
}
}).reduceNode({ one: true });
assert.deepEqual(node.dynamicId, "#(dependencies/one,dependencies/two)");
});
it("should prefix with schemaLocation", () => {
const { node } =
compileSchema({
properties: {
counter: { anyOf: [{ type: "number" }, { minimum: 1 }] }
}
})
.getNodeChild("counter", { counter: 3 })
.node?.reduceNode(3) ?? {};
assert.deepEqual(node.dynamicId, "#/properties/counter(anyOf/0,anyOf/1)");
});
it("should maintain dynamicId through nested reduce-calls", () => {
const { node } =
compileSchema({
allOf: [
{
properties: {
counter: { anyOf: [{ type: "number" }, { minimum: 1 }] }
}
}
]
})
.getNodeChild("counter", { counter: 3 })
.node?.reduceNode(3) ?? {};
assert.deepEqual(node.dynamicId, "#(allOf/0)+#/properties/counter(anyOf/0,anyOf/1)");
});
it("should add dynamicId from nested reducers in allOf", () => {
const { node } = compileSchema({
allOf: [
{
anyOf: [{ type: "string" }, { minimum: 1 }]
}
]
}).reduceNode(2);
assert.deepEqual(node.dynamicId, "#(#/allOf/0(anyOf/1))");
});
it("should add dynamicId from nested reducers in anyOf", () => {
const { node } = compileSchema({
anyOf: [
{
allOf: [{ type: "number" }, { minimum: 1 }]
}
]
}).reduceNode(2);
assert.deepEqual(node.dynamicId, "#(#/anyOf/0(allOf/0,allOf/1))");
});
it("should add dynamicId from nested reducers in then", () => {
const { node } = compileSchema({
if: { minimum: 1 },
then: {
allOf: [{ type: "number" }, { title: "order" }]
}
}).reduceNode(2);
assert.deepEqual(node.dynamicId, "#/then(allOf/0,allOf/1)");
});
});
});