Skip to content

Commit f69848d

Browse files
authored
Merge pull request #56 from arpitkuriyal/discriminator
Discriminator- more anyof cases
2 parents 286ed46 + 5900982 commit f69848d

File tree

5 files changed

+267
-68
lines changed

5 files changed

+267
-68
lines changed

src/error-handlers/anyOf.js

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,34 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
2323
/** @type NormalizedOutput[] */
2424
const alternatives = [];
2525
for (const alternative of allAlternatives) {
26-
if (Object.values(alternative[Instance.uri(instance)]["https://json-schema.org/keyword/type"] ?? {}).every((valid) => valid)) {
26+
const schemaErrors = alternative[Instance.uri(instance)];
27+
const isTypeValid = schemaErrors["https://json-schema.org/keyword/type"]
28+
? Object.values(schemaErrors["https://json-schema.org/keyword/type"]).every((valid) => valid)
29+
: undefined;
30+
const isEnumValid = schemaErrors["https://json-schema.org/keyword/enum"]
31+
? Object.values(schemaErrors["https://json-schema.org/keyword/enum"] ?? {}).every((valid) => valid)
32+
: undefined;
33+
const isConstValid = schemaErrors["https://json-schema.org/keyword/const"]
34+
? Object.values(schemaErrors["https://json-schema.org/keyword/const"] ?? {}).every((valid) => valid)
35+
: undefined;
36+
37+
if (isTypeValid === true || isEnumValid === true || isConstValid === true) {
38+
alternatives.push(alternative);
39+
}
40+
41+
if (isConstValid === undefined && isEnumValid === undefined && isTypeValid === undefined) {
2742
alternatives.push(alternative);
2843
}
2944
}
3045

31-
// No alternative matched the type of the instance.
46+
// No alternative matched the type/enum/const of the instance.
3247
if (alternatives.length === 0) {
3348
/** @type Set<string> */
3449
const expectedTypes = new Set();
3550

51+
/** @type Set<Json> */
52+
const expectedEnums = new Set();
53+
3654
for (const alternative of allAlternatives) {
3755
for (const instanceLocation in alternative) {
3856
if (instanceLocation === Instance.uri(instance)) {
@@ -41,12 +59,27 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
4159
const expectedType = /** @type string */ (Schema.value(keyword));
4260
expectedTypes.add(expectedType);
4361
}
62+
for (const schemaLocation in alternative[instanceLocation]["https://json-schema.org/keyword/enum"]) {
63+
const keyword = await getSchema(schemaLocation);
64+
const enums = /** @type Json[] */ (Schema.value(keyword));
65+
for (const enumValue of enums) {
66+
expectedEnums.add(enumValue);
67+
}
68+
}
69+
for (const schemaLocation in alternative[instanceLocation]["https://json-schema.org/keyword/const"]) {
70+
const keyword = await getSchema(schemaLocation);
71+
const constValue = /** @type Json */ (Schema.value(keyword));
72+
expectedEnums.add(constValue);
73+
}
4474
}
4575
}
4676
}
4777

4878
errors.push({
49-
message: localization.getTypeErrorMessage([...expectedTypes], Instance.typeOf(instance)),
79+
message: localization.getEnumErrorMessage({
80+
allowedValues: [...expectedEnums],
81+
allowedTypes: [...expectedTypes]
82+
}, Instance.value(instance)),
5083
instanceLocation: Instance.uri(instance),
5184
schemaLocation: schemaLocation
5285
});
@@ -80,7 +113,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
80113
const discriminator = definedProperties.reduce((acc, properties) => {
81114
return acc.intersection(properties);
82115
}, definedProperties[0]);
83-
84116
const discriminatedAlternatives = alternatives.filter((alternative) => {
85117
for (const instanceLocation in alternative) {
86118
if (!discriminator.has(instanceLocation)) {
@@ -133,10 +165,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
133165
continue;
134166
}
135167

136-
// TODO: Handle alternatives with const
137-
// TODO: Handle alternatives with enum
138-
// TODO: Handle null alternatives
139-
// TODO: Handle boolean alternatives
140168
// TODO: Handle string alternatives
141169
// TODO: Handle array alternatives
142170
// TODO: Handle alternatives without a type

src/error-handlers/enum.js

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { getSchema } from "@hyperjump/json-schema/experimental";
22
import * as Schema from "@hyperjump/browser";
33
import * as Instance from "@hyperjump/json-schema/instance/experimental";
4-
import leven from "leven";
54

65
/**
76
* @import { ErrorHandler, ErrorObject } from "../index.d.ts"
@@ -18,34 +17,11 @@ const enum_ = async (normalizedErrors, instance, localization) => {
1817
const keyword = await getSchema(schemaLocation);
1918

2019
/** @type {Array<string>} */
21-
const allowedValues = Schema.value(keyword);
20+
let allowedValues = Schema.value(keyword);
2221
const currentValue = /** @type {string} */ (Instance.value(instance));
2322

24-
const bestMatch = allowedValues
25-
.map((value) => ({
26-
value,
27-
weight: leven(value, currentValue)
28-
}))
29-
.sort((a, b) => a.weight - b.weight)[0];
30-
let message;
31-
if (
32-
allowedValues.length === 1
33-
|| (bestMatch && bestMatch.weight < bestMatch.value.length)
34-
) {
35-
message = localization.getEnumErrorMessage({
36-
variant: "suggestion",
37-
instanceValue: currentValue,
38-
suggestion: bestMatch.value
39-
});
40-
} else {
41-
message = localization.getEnumErrorMessage({
42-
variant: "fallback",
43-
instanceValue: currentValue,
44-
allowedValues: allowedValues
45-
});
46-
}
4723
errors.push({
48-
message,
24+
message: localization.getEnumErrorMessage({ allowedValues }, currentValue),
4925
instanceLocation: Instance.uri(instance),
5026
schemaLocation: schemaLocation
5127
});

src/keyword-error-message.test.js

Lines changed: 168 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ describe("Error messages", async () => {
518518
expect(result.errors).to.eql([{
519519
schemaLocation: "https://example.com/main#/enum",
520520
instanceLocation: "#",
521-
message: localization.getEnumErrorMessage({ variant: "suggestion", instanceValue: "rwd", suggestion: "red" })
521+
message: localization.getEnumErrorMessage({ allowedValues: ["red", "green", "blue"] }, "rwd")
522522
}]);
523523
});
524524

@@ -856,7 +856,7 @@ describe("Error messages", async () => {
856856
{
857857
schemaLocation: "https://example.com/main#/anyOf",
858858
instanceLocation: "#",
859-
message: localization.getTypeErrorMessage(["string", "number"], "boolean")
859+
message: localization.getEnumErrorMessage({ allowedTypes: ["string", "number"] }, false)
860860
}
861861
]);
862862
});
@@ -989,7 +989,7 @@ describe("Error messages", async () => {
989989
const instance = {
990990
type: "d",
991991
banana: "yellow",
992-
box: 10
992+
box: ""
993993
};
994994

995995
/** @type OutputFormat */
@@ -1163,6 +1163,171 @@ describe("Error messages", async () => {
11631163
]);
11641164
});
11651165

1166+
test("anyOf with enums provides a 'did you mean' suggestion", async () => {
1167+
registerSchema({
1168+
$schema: "https://json-schema.org/draft/2020-12/schema",
1169+
anyOf: [
1170+
{ enum: ["apple", "orange", "banana"] },
1171+
{ enum: [100, 200, 300] }
1172+
]
1173+
}, schemaUri);
1174+
1175+
// The instance is a typo but is clearly intended to be "apple".
1176+
const instance = "aple";
1177+
1178+
/** @type OutputFormat */
1179+
const output = {
1180+
valid: false,
1181+
errors: [
1182+
{
1183+
absoluteKeywordLocation: "https://example.com/main#/anyOf/0/enum",
1184+
instanceLocation: "#"
1185+
},
1186+
{
1187+
absoluteKeywordLocation: "https://example.com/main#/anyOf/1/enum",
1188+
instanceLocation: "#"
1189+
},
1190+
{
1191+
absoluteKeywordLocation: "https://example.com/main#/anyOf",
1192+
instanceLocation: "#"
1193+
}
1194+
]
1195+
};
1196+
1197+
const result = await betterJsonSchemaErrors(output, schemaUri, instance);
1198+
1199+
expect(result.errors).to.eql([
1200+
{
1201+
schemaLocation: "https://example.com/main#/anyOf",
1202+
instanceLocation: "#",
1203+
message: localization.getEnumErrorMessage({ allowedValues: ["apple"] }, "aple")
1204+
}
1205+
]);
1206+
});
1207+
1208+
test("anyOf with const", async () => {
1209+
registerSchema({
1210+
$schema: "https://json-schema.org/draft/2020-12/schema",
1211+
anyOf: [
1212+
{ const: "a" },
1213+
{ const: 1 }
1214+
]
1215+
}, schemaUri);
1216+
1217+
const instance = 12;
1218+
1219+
/** @type OutputFormat */
1220+
const output = {
1221+
valid: false,
1222+
errors: [
1223+
{
1224+
absoluteKeywordLocation: "https://example.com/main#/anyOf/0/const",
1225+
instanceLocation: "#"
1226+
},
1227+
{
1228+
absoluteKeywordLocation: "https://example.com/main#/anyOf/1/const",
1229+
instanceLocation: "#"
1230+
},
1231+
{
1232+
absoluteKeywordLocation: "https://example.com/main#/anyOf",
1233+
instanceLocation: "#"
1234+
}
1235+
]
1236+
};
1237+
1238+
const result = await betterJsonSchemaErrors(output, schemaUri, instance);
1239+
1240+
expect(result.errors).to.eql([
1241+
{
1242+
schemaLocation: "https://example.com/main#/anyOf",
1243+
instanceLocation: "#",
1244+
message: localization.getEnumErrorMessage({ allowedValues: ["a", 1] }, 12)
1245+
}
1246+
]);
1247+
});
1248+
1249+
test("anyOf with const and enum", async () => {
1250+
registerSchema({
1251+
$schema: "https://json-schema.org/draft/2020-12/schema",
1252+
anyOf: [
1253+
{ enum: ["a", "b", "c"] },
1254+
{ const: 1 }
1255+
]
1256+
}, schemaUri);
1257+
1258+
const instance = 12;
1259+
1260+
/** @type OutputFormat */
1261+
const output = {
1262+
valid: false,
1263+
errors: [
1264+
{
1265+
absoluteKeywordLocation: "https://example.com/main#/anyOf/0/enum",
1266+
instanceLocation: "#"
1267+
},
1268+
{
1269+
absoluteKeywordLocation: "https://example.com/main#/anyOf/1/const",
1270+
instanceLocation: "#"
1271+
},
1272+
{
1273+
absoluteKeywordLocation: "https://example.com/main#/anyOf",
1274+
instanceLocation: "#"
1275+
}
1276+
]
1277+
};
1278+
1279+
const result = await betterJsonSchemaErrors(output, schemaUri, instance);
1280+
1281+
expect(result.errors).to.eql([
1282+
{
1283+
schemaLocation: "https://example.com/main#/anyOf",
1284+
instanceLocation: "#",
1285+
message: localization.getEnumErrorMessage({ allowedValues: ["a", "b", "c", 1] }, 12)
1286+
}
1287+
]);
1288+
});
1289+
1290+
test("anyOf with enum and type", async () => {
1291+
registerSchema({
1292+
$schema: "https://json-schema.org/draft/2020-12/schema",
1293+
anyOf: [
1294+
{ enum: ["a", "b", "c"] },
1295+
{ type: "number" }
1296+
]
1297+
}, schemaUri);
1298+
1299+
const instance = false;
1300+
1301+
/** @type OutputFormat */
1302+
const output = {
1303+
valid: false,
1304+
errors: [
1305+
{
1306+
absoluteKeywordLocation: "https://example.com/main#/anyOf/0/enum",
1307+
instanceLocation: "#"
1308+
},
1309+
{
1310+
absoluteKeywordLocation: "https://example.com/main#/anyOf/1/type",
1311+
instanceLocation: "#"
1312+
},
1313+
{
1314+
absoluteKeywordLocation: "https://example.com/main#/anyOf",
1315+
instanceLocation: "#"
1316+
}
1317+
]
1318+
};
1319+
1320+
const result = await betterJsonSchemaErrors(output, schemaUri, instance);
1321+
1322+
expect(result.errors).to.eql([
1323+
{
1324+
schemaLocation: "https://example.com/main#/anyOf",
1325+
instanceLocation: "#",
1326+
message: localization.getEnumErrorMessage({ allowedValues: ["a", "b", "c"], allowedTypes: ["number"] }, false)
1327+
}
1328+
]);
1329+
});
1330+
11661331
test("normalized output for a failing 'contains' keyword", async () => {
11671332
registerSchema({
11681333
$schema: "https://json-schema.org/draft/2020-12/schema",

0 commit comments

Comments
 (0)