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
69 changes: 56 additions & 13 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 @@ -15,32 +17,65 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
if (typeof anyOf === "boolean") {
continue;
}

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

for (const alternative of anyOf) {
const typeErrors = alternative[instanceLocation]["https://json-schema.org/keyword/type"];
const match = !typeErrors || Object.values(typeErrors).every((isValid) => isValid);
if (Instance.typeOf(instance) === "object") {
const instanceProps = Pact.collectSet(
Pact.map(
(keyNode) => /** @type {string} */ (Instance.value(keyNode)),
Instance.keys(instance)
)
);
const prefix = `${instanceLocation}/`;

if (match) {
alternatives.push(await getErrors(alternative, instance, localization));
}
}
filtered = [];
for (const alternative of anyOf) {
const typeResults = alternative[instanceLocation]["https://json-schema.org/keyword/type"];
if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) {
continue;
}

if (alternatives.length === 0) {
const declaredProps = Object.keys(alternative)
.filter((loc) => loc.startsWith(prefix))
.map((loc) => /** @type {string} */ (Pact.head(JsonPointer.pointerSegments(loc.slice(prefix.length - 1)))));
Comment on lines +39 to +41
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use Pact here to avoid creating intermediate arrays.


if (declaredProps.length > 0 && !declaredProps.some((prop) => instanceProps.has(prop))) {
continue;
}

if (!Pact.some((prop) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), instanceProps)) {
continue;
}

filtered.push(alternative);
}
} else {
filtered = [];
for (const alternative of anyOf) {
alternatives.push(await getErrors(alternative, instance, localization));
const typeResults = alternative[instanceLocation]["https://json-schema.org/keyword/type"];
if (!typeResults || Object.values(typeResults).every((isValid) => isValid)) {
filtered.push(alternative);
}
}
Comment on lines +54 to 60
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is now duplicated code. You don't need the if/else. Remove the if wrapper and the else block entirely.

Copy link
Author

Choose a reason for hiding this comment

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

So basically remove if else wrapper put the type check at top for each alternative then if its an object we check rule1 and rule2 right

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes

}

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

const alternatives = [];
for (const alternative of filtered) {
alternatives.push(await getErrors(alternative, instance, localization));
}

if (alternatives.length === 1) {
errors.push(...alternatives[0]);
} else {
errors.push({
message: localization.getAnyOfErrorMessage(),
alternatives: alternatives,
instanceLocation: Instance.uri(instance),
instanceLocation,
schemaLocations: [schemaLocation]
});
}
Expand All @@ -49,4 +84,12 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
return errors;
};

/** @type (propOutput: NormalizedOutput[string] | undefined) => boolean */
const propertyPasses = (propOutput) => {
if (!propOutput || Object.keys(propOutput).length === 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't understand the purpose of the check for an empty object and the tests don't cover it. Either remove it or add a test showing it's needed.

return false;
}
return Object.values(propOutput).every((keywordResults) => Object.values(keywordResults).every((v) => v === true));
};

export default anyOfErrorHandler;
102 changes: 85 additions & 17 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,38 +18,96 @@ 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);

if (match) {
const alternativeErrors = await getErrors(alternative, instance, localization);
if (alternativeErrors.length) {
alternatives.push(alternativeErrors);
} else {
matchCount++;
}
const alternativeErrors = await getErrors(alternative, instance, localization);
if (alternativeErrors.length) {
failingAlternatives.push(alternativeErrors);
} else {
matchCount++;
}
}

if (matchCount === 0 && alternatives.length === 0) {
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;

if (Instance.typeOf(instance) === "object") {
const instanceProps = Pact.collectSet(
Pact.map(
(keyNode) => /** @type {string} */ (Instance.value(keyNode)),
Instance.keys(instance)
)
);
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;
}

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

if (declaredProps.length > 0 && !declaredProps.some((prop) => instanceProps.has(prop))) {
continue;
}

if (!Pact.some((prop) => propertyPasses(alternative[JsonPointer.append(prop, instanceLocation)]), instanceProps)) {
continue;
}

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

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

const alternatives = [];
for (const alternative of filtered) {
const alternativeErrors = await getErrors(alternative, instance, localization);
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),
message: localization.getOneOfErrorMessage(0),
instanceLocation,
schemaLocations: [schemaLocation]
};
if (alternatives.length) {
Expand All @@ -60,4 +120,12 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => {
return errors;
};

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

export default oneOfErrorHandler;
146 changes: 146 additions & 0 deletions src/test-suite/tests/anyOf.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,152 @@
"schemaLocations": ["#/$defs/bar/maxLength"]
}
]
},
{
"description": "anyOf object alternatives keep only one branch when only one branch has a passing instance property",
"compatibility": "6",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"type": { "const": "a" },
"a": { "type": "string" }
},
"required": ["type", "a"]
},
{
"type": "object",
"properties": {
"type": { "const": "b" },
"b": { "type": "number" }
},
"required": ["type", "b"]
}
]
},
"instance": { "type": "b", "b": "oops" },
"errors": [
{
"messageId": "type-message",
"messageParams": {
"expectedTypes": "number"
},
"instanceLocation": "#/b",
"schemaLocations": ["#/anyOf/1/properties/b/type"]
}
]
},
{
"description": "anyOf object alternatives keep both branches when each branch has at least one passing instance property",
"compatibility": "6",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"type": { "const": "a" },
"x": { "type": "string" }
},
"required": ["type", "x"]
},
{
"type": "object",
"properties": {
"type": {
"allOf": [
{ "type": "string" },
{ "minLength": 2 }
]
},
"x": { "type": "number" }
},
"required": ["type", "x"]
}
]
},
"instance": { "type": "a", "x": 42 },
"errors": [
{
"messageId": "anyOf-message",
"alternatives": [
[
{
"messageId": "type-message",
"messageParams": {
"expectedTypes": "string"
},
"instanceLocation": "#/x",
"schemaLocations": ["#/anyOf/0/properties/x/type"]
}
],
[
{
"messageId": "minLength-message",
"messageParams": {
"minLength": "2"
},
"instanceLocation": "#/type",
"schemaLocations": ["#/anyOf/1/properties/type/allOf/1/minLength"]
}
]
],
"instanceLocation": "#",
"schemaLocations": ["#/anyOf"]
}
]
},
{
"description": "anyOf object alternatives fallback to all when no instance property passes in any branch",
"compatibility": "6",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"kind": { "const": "a" }
},
"required": ["kind"]
},
{
"type": "object",
"properties": {
"kind": { "const": "b" }
},
"required": ["kind"]
}
]
},
"instance": { "kind": "c" },
"errors": [
{
"messageId": "anyOf-message",
"alternatives": [
[
{
"messageId": "const-message",
"messageParams": {
"expected": "\"a\""
},
"instanceLocation": "#/kind",
"schemaLocations": ["#/anyOf/0/properties/kind/const"]
}
],
[
{
"messageId": "const-message",
"messageParams": {
"expected": "\"b\""
},
"instanceLocation": "#/kind",
"schemaLocations": ["#/anyOf/1/properties/kind/const"]
}
]
],
"instanceLocation": "#",
"schemaLocations": ["#/anyOf"]
}
]
}
]
}
Loading