Skip to content
Open
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
73 changes: 64 additions & 9 deletions src/error-handlers/anyOf.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as Instance from "@hyperjump/json-schema/instance/experimental";
import * as JsonPointer from "@hyperjump/json-pointer";
import * as Pact from "@hyperjump/pact";
import { getErrors } from "../json-schema-errors.js";

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

/** @type ErrorHandler */
Expand All @@ -16,20 +18,65 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
continue;
}

const alternatives = [];
const instanceLocation = Instance.uri(instance);
let filtered = anyOf;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is initialized to anyOf, but then reset to [] before it's used. That can be cleaned up.


const instanceProps = Pact.pipe(
Instance.keys(instance),
Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode))),
Pact.collectSet
);
Comment on lines +24 to +28
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I occurs to me that we're collecting property names here, but then we always convert to a property location wherever we use this variable. I bet if we just use the property location to begin with, we can simplify things considerably.


const discriminators = Pact.pipe(
instanceProps,
Pact.filter((prop) => {
const propLocation = JsonPointer.append(prop, instanceLocation);
return Pact.some((alternative) => propertyPasses(alternative[propLocation]), anyOf);
}),
Pact.collectSet
);

const prefix = `${instanceLocation}/`;

filtered = [];
for (const alternative of anyOf) {
const typeErrors = alternative[instanceLocation]["https://json-schema.org/keyword/type"];
const match = !typeErrors || Object.values(typeErrors).every((isValid) => isValid);
// Filter alternatives whose declared type doesn't match the instance type
const typeResults = alternative[instanceLocation]["https://json-schema.org/keyword/type"];
if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) {
continue;
}

if (match) {
alternatives.push(await getErrors(alternative, instance, localization));
if (Instance.typeOf(instance) === "object") {
const declaredProps = Pact.pipe(
Object.keys(alternative),
Pact.filter((loc) => loc.startsWith(prefix)),
Pact.map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1))))),
Pact.collectSet
);

// Filter alternative if it has no declared properties in common with the instance
if (!Pact.some((prop) => declaredProps.has(prop), instanceProps)) {
continue;
}

// Filter alternative if it has failing properties that are decalred and passing in another alternative
if (Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators)) {
continue;
}
}

filtered.push(alternative);
}

if (filtered.length === 0) {
filtered = anyOf;
}

/** @type ErrorObject[][] */
const alternatives = [];

if (alternatives.length === 0) {
for (const alternative of anyOf) {
for (const alternative of filtered) {
alternatives.push(await getErrors(alternative, instance, localization));
}
}
Expand All @@ -39,8 +86,8 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
} else {
errors.push({
message: localization.getAnyOfErrorMessage(),
alternatives: alternatives,
instanceLocation: Instance.uri(instance),
alternatives,
instanceLocation,
schemaLocations: [schemaLocation]
});
}
Expand All @@ -49,4 +96,12 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
return errors;
};

/** @type (propOutput: NormalizedOutput[string] | undefined) => boolean */
const propertyPasses = (propOutput) => {
if (!propOutput) {
return false;
}
return Object.values(propOutput).every((keywordResults) => Object.values(keywordResults).every((v) => v === true));
};

export default anyOfErrorHandler;
120 changes: 101 additions & 19 deletions src/error-handlers/oneOf.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as Instance from "@hyperjump/json-schema/instance/experimental";
import * as JsonPointer from "@hyperjump/json-pointer";
import * as Pact from "@hyperjump/pact";
import { getErrors } from "../json-schema-errors.js";

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

/** @type ErrorHandler */
Expand All @@ -16,48 +18,128 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => {
continue;
}

const alternatives = [];
const instanceLocation = Instance.uri(instance);

let matchCount = 0;
/** @type ErrorObject[][] */
const failingAlternatives = [];

for (const alternative of oneOf) {
const typeErrors = alternative[instanceLocation]["https://json-schema.org/keyword/type"];
const match = !typeErrors || Object.values(typeErrors).every((isValid) => isValid);
const alternativeErrors = await getErrors(alternative, instance, localization);
if (alternativeErrors.length) {
failingAlternatives.push(alternativeErrors);
} else {
matchCount++;
}
}

if (match) {
const alternativeErrors = await getErrors(alternative, instance, localization);
if (alternativeErrors.length) {
alternatives.push(alternativeErrors);
} else {
matchCount++;
if (matchCount > 1) {
/** @type ErrorObject */
const error = {
message: localization.getOneOfErrorMessage(matchCount),
instanceLocation,
schemaLocations: [schemaLocation]
};
if (failingAlternatives.length) {
error.alternatives = failingAlternatives;
}
errors.push(error);
continue;
}

let filtered = oneOf;

const isObject = Instance.typeOf(instance) === "object";
const instanceProps = isObject
? Pact.collectSet(Pact.pipe(Instance.keys(instance), Pact.map((keyNode) => /** @type {string} */ (Instance.value(keyNode)))))
: undefined;
const prefix = `${instanceLocation}/`;

filtered = [];
for (const alternative of oneOf) {
const typeResults = alternative[instanceLocation]["https://json-schema.org/keyword/type"];
if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) {
continue;
}

if (isObject) {
const declaredProps = Pact.collectSet(Pact.pipe(
Object.keys(alternative),
Pact.filter((loc) => loc.startsWith(prefix)),
Pact.map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1)))))
));

if (declaredProps.size > 0 && !Pact.some((prop) => declaredProps.has(prop), /** @type {Set<string>} */ (instanceProps))) {
continue;
}
}

filtered.push(alternative);
}

if (filtered.length === 0) {
filtered = oneOf;
}

if (matchCount === 0 && alternatives.length === 0) {
for (const alternative of oneOf) {
/** @type ErrorObject[][] */
const alternatives = [];

if (isObject) {
const discriminators = Pact.collectSet(
/** @type {Iterable<string>} */ (Pact.pipe(
filtered,
Pact.map((alternative) => Pact.pipe(
/** @type {Set<string>} */ (instanceProps),
Pact.filter((prop) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]))
)),
Pact.flatten
))
);

for (const alternative of filtered) {
if (!Pact.some((prop) => !propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), discriminators)) {
const alternativeErrors = await getErrors(alternative, instance, localization);
if (alternativeErrors.length) {
alternatives.push(alternativeErrors);
}
}
}
}

if (alternatives.length === 0) {
for (const alternative of filtered) {
const alternativeErrors = await getErrors(alternative, instance, localization);
alternatives.push(alternativeErrors);
if (alternativeErrors.length) {
alternatives.push(alternativeErrors);
}
}
}

if (alternatives.length === 1 && matchCount === 0) {
if (alternatives.length === 1) {
errors.push(...alternatives[0]);
} else {
/** @type ErrorObject */
const alternativeErrors = {
message: localization.getOneOfErrorMessage(matchCount),
instanceLocation: Instance.uri(instance),
const error = {
message: localization.getOneOfErrorMessage(0),
instanceLocation,
schemaLocations: [schemaLocation]
};
if (alternatives.length) {
alternativeErrors.alternatives = alternatives;
error.alternatives = alternatives;
}
errors.push(alternativeErrors);
errors.push(error);
}
}

return errors;
};

/** @type (propOutput: NormalizedOutput[string] | undefined) => boolean */
const propertyPasses = (propOutput) => {
if (!propOutput) {
return false;
}
return Object.values(propOutput).every((keywordResults) => Object.values(keywordResults).every((v) => v === true));
};

export default oneOfErrorHandler;
Loading