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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ BetterJSONSchemaErrors Output:-
```
Instead of 2 error message it manages to give a single concise error message. For details, see the dedicated [Range documenetation](./documentation/range-handler.md)

### 6. Custom Keywords and Error Handlers
### 7. Custom Keywords and Error Handlers
In order to create the custom keywords and error handlers we need to create and
register two types of handlers: **Normalization Handler** and **Error Handlers**.

Expand Down
132 changes: 79 additions & 53 deletions src/error-handlers/anyOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental";
import * as Schema from "@hyperjump/browser";
import * as JsonPointer from "@hyperjump/json-pointer";
import { getErrors } from "../error-handling.js";
import { getSchemaDescription } from "../schema-descriptions.js";

/**
* @import { ErrorHandler, ErrorObject, Json, NormalizedOutput } from "../index.d.ts"
* @import { ErrorHandler, ErrorObject, Json, NormalizedOutput, InstanceOutput } from "../index.d.ts"
*/

/** @type ErrorHandler */
Expand All @@ -19,7 +20,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
if (typeof allAlternatives === "boolean") {
continue;
}

/** @type NormalizedOutput[] */
const alternatives = [];
for (const alternative of allAlternatives) {
Expand All @@ -33,7 +33,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
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);
}
Expand All @@ -46,19 +45,31 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
// No alternative matched the type/enum/const of the instance.
if (alternatives.length === 0) {
/** @type Set<string> */
const expectedTypes = new Set();
let 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)) {
let alternativeTypes = new Set(["null", "boolean", "number", "string", "array", "object"]);
for (const schemaLocation in alternative[instanceLocation]["https://json-schema.org/keyword/type"]) {
const keyword = await getSchema(schemaLocation);
const expectedType = /** @type string */ (Schema.value(keyword));
expectedTypes.add(expectedType);
if (Schema.typeOf(keyword) === "array") {
const expectedTypes = /** @type string[] */ (Schema.value(keyword));
alternativeTypes = alternativeTypes.intersection(new Set(expectedTypes));
} else {
const expectedType = /** @type string */ (Schema.value(keyword));
alternativeTypes = alternativeTypes.intersection(new Set([expectedType]));
}
}

// The are 6 types. If all types are allowed, don't use expectedTypes
if (alternativeTypes.size !== 6) {
expectedTypes = expectedTypes.union(alternativeTypes);
}

for (const schemaLocation in alternative[instanceLocation]["https://json-schema.org/keyword/enum"]) {
const keyword = await getSchema(schemaLocation);
const enums = /** @type Json[] */ (Schema.value(keyword));
Expand All @@ -74,7 +85,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
}
}
}

errors.push({
message: localization.getEnumErrorMessage({
allowedValues: [...expectedEnums],
Expand All @@ -96,7 +106,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
const definedProperties = allAlternatives.map((alternative) => {
/** @type Set<string> */
const alternativeProperties = new Set();

for (const instanceLocation in alternative) {
const pointer = instanceLocation.slice(Instance.uri(instance).length + 1);
if (pointer.length > 0) {
Expand All @@ -106,69 +115,86 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
alternativeProperties.add(location);
}
}

return alternativeProperties;
});

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)) {
continue;
}
const anyPropertiesDefined = definedProperties.some((propSet) => propSet.size > 0);

let valid = true;
for (const keyword in alternative[instanceLocation]) {
for (const schemaLocation in alternative[instanceLocation][keyword]) {
if (alternative[instanceLocation][keyword][schemaLocation] !== true) {
valid = false;
break;
if (anyPropertiesDefined) {
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)) {
continue;
}
let valid = true;
for (const keyword in alternative[instanceLocation]) {
for (const schemaLocation in alternative[instanceLocation][keyword]) {
if (alternative[instanceLocation][keyword][schemaLocation] !== true) {
valid = false;
break;
}
}
}
if (valid) {
return true;
}
}
if (valid) {
return true;
}
return false;
});
// Discriminator match
if (discriminatedAlternatives.length === 1) {
errors.push(...await getErrors(discriminatedAlternatives[0], instance, localization));
continue;
}
// Discriminator identified, but none of the alternatives match
if (discriminatedAlternatives.length === 0) {
// TODO: For now, it will use the schema description strategy
}
return false;
});

// Discriminator match
if (discriminatedAlternatives.length === 1) {
errors.push(...await getErrors(discriminatedAlternatives[0], instance, localization));
// Last resort, select the alternative with the most properties matching the instance
const instanceProperties = new Set(Instance.values(instance).map((node) => Instance.uri(node)));
let maxMatches = -1;
let selectedIndex = 0;
let index = -1;
for (const alternativeProperties of definedProperties) {
index++;
const matches = alternativeProperties.intersection(instanceProperties).size;
if (matches > maxMatches) {
selectedIndex = index;
}
}
errors.push(...await getErrors(alternatives[selectedIndex], instance, localization));
continue;
}
}

// Discriminator identified, but none of the alternatives match
if (discriminatedAlternatives.length === 0) {
// TODO: How do we handle this case?
}
// TODO: Handle alternatives without a type

// Last resort, select the alternative with the most properties matching the instance
// TODO: We shouldn't use this strategy if alternatives have the same number of matching instances
const instanceProperties = new Set(Instance.values(instance)
.map((node) => Instance.uri(node)));
let maxMatches = -1;
let selectedIndex = 0;
let index = -1;
for (const alternativeProperties of definedProperties) {
index++;
const matches = alternativeProperties.intersection(instanceProperties).size;
if (matches > maxMatches) {
selectedIndex = index;
}
/** @type string[] */
const descriptions = [];
let allAlternativesHaveDescriptions = true;
for (const alternative of alternatives) {
const description = await getSchemaDescription(normalizedErrors, alternative[Instance.uri(instance)], localization);
if (description !== undefined) {
descriptions.push(description);
} else {
allAlternativesHaveDescriptions = false;
break;
}
}

errors.push(...await getErrors(alternatives[selectedIndex], instance, localization));
if (allAlternativesHaveDescriptions) {
errors.push({
message: localization.getAnyOfBulletsErrorMessage(descriptions),
instanceLocation: Instance.uri(instance),
schemaLocation: schemaLocation
});
continue;
}

// TODO: Handle string alternatives
// TODO: Handle array alternatives
// TODO: Handle alternatives without a type

// TODO: If we get here, we don't know what else to do and give a very generic message
// Ideally this should be replace by something that can handle whatever case is missing.
errors.push({
Expand Down
Loading