Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions src/error-handlers/anyOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,34 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
/** @type NormalizedOutput[] */
const alternatives = [];
for (const alternative of allAlternatives) {
if (Object.values(alternative[Instance.uri(instance)]["https://json-schema.org/keyword/type"] ?? {}).every((valid) => valid)) {
const schemaErrors = alternative[Instance.uri(instance)];
const isTypeValid = schemaErrors["https://json-schema.org/keyword/type"]
? Object.values(schemaErrors["https://json-schema.org/keyword/type"]).every((valid) => valid)
: undefined;
const isEnumValid = schemaErrors["https://json-schema.org/keyword/enum"]
? Object.values(schemaErrors["https://json-schema.org/keyword/enum"] ?? {}).every((valid) => valid)
: undefined;
const isConstValid = schemaErrors["https://json-schema.org/keyword/const"]
? Object.values(schemaErrors["https://json-schema.org/keyword/const"] ?? {}).every((valid) => valid)
: undefined;

if (isTypeValid === true || isEnumValid === true || isConstValid === true) {
alternatives.push(alternative);
}

if (isConstValid === undefined && isEnumValid === undefined && isTypeValid === undefined) {
alternatives.push(alternative);
}
}

// No alternative matched the type of the instance.
// No alternative matched the type/enum/const of the instance.
if (alternatives.length === 0) {
/** @type Set<string> */
const expectedTypes = new Set();

/** @type Set<Json> */
const expectedEnums = new Set();

for (const alternative of allAlternatives) {
for (const instanceLocation in alternative) {
if (instanceLocation === Instance.uri(instance)) {
Expand All @@ -41,12 +59,27 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
const expectedType = /** @type string */ (Schema.value(keyword));
expectedTypes.add(expectedType);
}
for (const schemaLocation in alternative[instanceLocation]["https://json-schema.org/keyword/enum"]) {
const keyword = await getSchema(schemaLocation);
const enums = /** @type Json[] */ (Schema.value(keyword));
for (const enumValue of enums) {
expectedEnums.add(enumValue);
}
}
for (const schemaLocation in alternative[instanceLocation]["https://json-schema.org/keyword/const"]) {
const keyword = await getSchema(schemaLocation);
const constValue = /** @type Json */ (Schema.value(keyword));
expectedEnums.add(constValue);
}
}
}
}

errors.push({
message: localization.getTypeErrorMessage([...expectedTypes], Instance.typeOf(instance)),
message: localization.getEnumErrorMessage({
allowedValues: [...expectedEnums],
allowedTypes: [...expectedTypes]
}, Instance.value(instance)),
instanceLocation: Instance.uri(instance),
schemaLocation: schemaLocation
});
Expand Down Expand Up @@ -80,7 +113,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
const discriminator = definedProperties.reduce((acc, properties) => {
return acc.intersection(properties);
}, definedProperties[0]);

const discriminatedAlternatives = alternatives.filter((alternative) => {
for (const instanceLocation in alternative) {
if (!discriminator.has(instanceLocation)) {
Expand Down Expand Up @@ -133,10 +165,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
continue;
}

// TODO: Handle alternatives with const
// TODO: Handle alternatives with enum
// TODO: Handle null alternatives
// TODO: Handle boolean alternatives
// TODO: Handle string alternatives
// TODO: Handle array alternatives
// TODO: Handle alternatives without a type
Expand Down
28 changes: 2 additions & 26 deletions src/error-handlers/enum.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { getSchema } from "@hyperjump/json-schema/experimental";
import * as Schema from "@hyperjump/browser";
import * as Instance from "@hyperjump/json-schema/instance/experimental";
import leven from "leven";

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

/** @type {Array<string>} */
const allowedValues = Schema.value(keyword);
let allowedValues = Schema.value(keyword);
const currentValue = /** @type {string} */ (Instance.value(instance));

const bestMatch = allowedValues
.map((value) => ({
value,
weight: leven(value, currentValue)
}))
.sort((a, b) => a.weight - b.weight)[0];
let message;
if (
allowedValues.length === 1
|| (bestMatch && bestMatch.weight < bestMatch.value.length)
) {
message = localization.getEnumErrorMessage({
variant: "suggestion",
instanceValue: currentValue,
suggestion: bestMatch.value
});
} else {
message = localization.getEnumErrorMessage({
variant: "fallback",
instanceValue: currentValue,
allowedValues: allowedValues
});
}
errors.push({
message,
message: localization.getEnumErrorMessage({ allowedValues }, currentValue),
instanceLocation: Instance.uri(instance),
schemaLocation: schemaLocation
});
Expand Down
171 changes: 168 additions & 3 deletions src/keyword-error-message.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ describe("Error messages", async () => {
expect(result.errors).to.eql([{
schemaLocation: "https://example.com/main#/enum",
instanceLocation: "#",
message: localization.getEnumErrorMessage({ variant: "suggestion", instanceValue: "rwd", suggestion: "red" })
message: localization.getEnumErrorMessage({ allowedValues: ["red", "green", "blue"] }, "rwd")
}]);
});

Expand Down Expand Up @@ -856,7 +856,7 @@ describe("Error messages", async () => {
{
schemaLocation: "https://example.com/main#/anyOf",
instanceLocation: "#",
message: localization.getTypeErrorMessage(["string", "number"], "boolean")
message: localization.getEnumErrorMessage({ allowedTypes: ["string", "number"] }, false)
}
]);
});
Expand Down Expand Up @@ -989,7 +989,7 @@ describe("Error messages", async () => {
const instance = {
type: "d",
banana: "yellow",
box: 10
box: ""
};

/** @type OutputFormat */
Expand Down Expand Up @@ -1163,6 +1163,171 @@ describe("Error messages", async () => {
]);
});

test("anyOf with enums provides a 'did you mean' suggestion", async () => {
registerSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
anyOf: [
{ enum: ["apple", "orange", "banana"] },
{ enum: [100, 200, 300] }
]
}, schemaUri);

// The instance is a typo but is clearly intended to be "apple".
const instance = "aple";

/** @type OutputFormat */
const output = {
valid: false,
errors: [
{
absoluteKeywordLocation: "https://example.com/main#/anyOf/0/enum",
instanceLocation: "#"
},
{
absoluteKeywordLocation: "https://example.com/main#/anyOf/1/enum",
instanceLocation: "#"
},
{
absoluteKeywordLocation: "https://example.com/main#/anyOf",
instanceLocation: "#"
}
]
};

const result = await betterJsonSchemaErrors(output, schemaUri, instance);

expect(result.errors).to.eql([
{
schemaLocation: "https://example.com/main#/anyOf",
instanceLocation: "#",
message: localization.getEnumErrorMessage({ allowedValues: ["apple"] }, "aple")
}
]);
});

test("anyOf with const", async () => {
registerSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
anyOf: [
{ const: "a" },
{ const: 1 }
]
}, schemaUri);

const instance = 12;

/** @type OutputFormat */
const output = {
valid: false,
errors: [
{
absoluteKeywordLocation: "https://example.com/main#/anyOf/0/const",
instanceLocation: "#"
},
{
absoluteKeywordLocation: "https://example.com/main#/anyOf/1/const",
instanceLocation: "#"
},
{
absoluteKeywordLocation: "https://example.com/main#/anyOf",
instanceLocation: "#"
}
]
};

const result = await betterJsonSchemaErrors(output, schemaUri, instance);

expect(result.errors).to.eql([
{
schemaLocation: "https://example.com/main#/anyOf",
instanceLocation: "#",
message: localization.getEnumErrorMessage({ allowedValues: ["a", 1] }, 12)
}
]);
});

test("anyOf with const and enum", async () => {
registerSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
anyOf: [
{ enum: ["a", "b", "c"] },
{ const: 1 }
]
}, schemaUri);

const instance = 12;

/** @type OutputFormat */
const output = {
valid: false,
errors: [
{
absoluteKeywordLocation: "https://example.com/main#/anyOf/0/enum",
instanceLocation: "#"
},
{
absoluteKeywordLocation: "https://example.com/main#/anyOf/1/const",
instanceLocation: "#"
},
{
absoluteKeywordLocation: "https://example.com/main#/anyOf",
instanceLocation: "#"
}
]
};

const result = await betterJsonSchemaErrors(output, schemaUri, instance);

expect(result.errors).to.eql([
{
schemaLocation: "https://example.com/main#/anyOf",
instanceLocation: "#",
message: localization.getEnumErrorMessage({ allowedValues: ["a", "b", "c", 1] }, 12)
}
]);
});

test("anyOf with enum and type", async () => {
registerSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
anyOf: [
{ enum: ["a", "b", "c"] },
{ type: "number" }
]
}, schemaUri);

const instance = false;

/** @type OutputFormat */
const output = {
valid: false,
errors: [
{
absoluteKeywordLocation: "https://example.com/main#/anyOf/0/enum",
instanceLocation: "#"
},
{
absoluteKeywordLocation: "https://example.com/main#/anyOf/1/type",
instanceLocation: "#"
},
{
absoluteKeywordLocation: "https://example.com/main#/anyOf",
instanceLocation: "#"
}
]
};

const result = await betterJsonSchemaErrors(output, schemaUri, instance);

expect(result.errors).to.eql([
{
schemaLocation: "https://example.com/main#/anyOf",
instanceLocation: "#",
message: localization.getEnumErrorMessage({ allowedValues: ["a", "b", "c"], allowedTypes: ["number"] }, false)
}
]);
});

test("normalized output for a failing 'contains' keyword", async () => {
registerSchema({
$schema: "https://json-schema.org/draft/2020-12/schema",
Expand Down
Loading