Skip to content

Commit c67137e

Browse files
filter anyOf and oneOf alternative (#180)
* filter anyOf and oneOf alternative * added tests for new behavior * used hyperjump pact for better performance * fixed linting issues with oxlint * changed the logic to match what is discussed * simplified code * Cleanup pipeline usage * Move discriminator filtering into the main loop * apply discussed changes * Using property locations allowed us to simplify even more! * Add a couple more tests --------- Co-authored-by: Jason Desrosiers <jdesrosi@gmail.com>
1 parent 005cd14 commit c67137e

4 files changed

Lines changed: 716 additions & 19 deletions

File tree

src/error-handlers/anyOf.js

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as Instance from "@hyperjump/json-schema/instance/experimental";
2+
import * as Pact from "@hyperjump/pact";
23
import { getErrors } from "../json-schema-errors.js";
34

45
/**
5-
* @import { ErrorHandler, ErrorObject } from "../index.d.ts"
6+
* @import { ErrorHandler, ErrorObject, InstanceOutput } from "../index.d.ts"
67
*/
78

89
/** @type ErrorHandler */
@@ -16,18 +17,44 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
1617
continue;
1718
}
1819

20+
const propertyLocations = Pact.pipe(
21+
Instance.values(instance),
22+
Pact.map(Instance.uri),
23+
Pact.collectArray
24+
);
25+
26+
const discriminators = propertyLocations.filter((propertyLocation) => {
27+
return anyOf.some((alternative) => isPassingProperty(alternative[propertyLocation]));
28+
});
29+
30+
/** @type ErrorObject[][] */
1931
const alternatives = [];
2032
const instanceLocation = Instance.uri(instance);
2133

2234
for (const alternative of anyOf) {
23-
const typeErrors = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"];
24-
const match = !typeErrors || Object.values(typeErrors).every((isValid) => isValid);
35+
// Filter alternatives whose declared type doesn't match the instance type
36+
const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"];
37+
if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) {
38+
continue;
39+
}
2540

26-
if (match) {
27-
alternatives.push(await getErrors(alternative, instance, localization));
41+
if (Instance.typeOf(instance) === "object") {
42+
// Filter alternative if it has no declared properties in common with the instance
43+
if (!propertyLocations.some((propertyLocation) => propertyLocation in alternative)) {
44+
continue;
45+
}
46+
47+
// Filter alternative if it has failing properties that are declared and passing in another alternative
48+
if (discriminators.some((propertyLocation) => !isPassingProperty(alternative[propertyLocation]))) {
49+
continue;
50+
}
2851
}
52+
53+
// The alternative passed all the filters
54+
alternatives.push(await getErrors(alternative, instance, localization));
2955
}
3056

57+
// If all alternatives were filtered out, default to returning all of them
3158
if (alternatives.length === 0) {
3259
for (const alternative of anyOf) {
3360
alternatives.push(await getErrors(alternative, instance, localization));
@@ -39,8 +66,8 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
3966
} else {
4067
errors.push({
4168
message: localization.getAnyOfErrorMessage(),
42-
alternatives: alternatives,
43-
instanceLocation: Instance.uri(instance),
69+
alternatives,
70+
instanceLocation,
4471
schemaLocations: [schemaLocation]
4572
});
4673
}
@@ -49,4 +76,21 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
4976
return errors;
5077
};
5178

79+
/** @type (alternative: InstanceOutput | undefined) => boolean */
80+
const isPassingProperty = (propertyOutput) => {
81+
if (!propertyOutput) {
82+
return false;
83+
}
84+
85+
for (const keywordUri in propertyOutput) {
86+
for (const schemaLocation in propertyOutput[keywordUri]) {
87+
if (propertyOutput[keywordUri][schemaLocation] !== true) {
88+
return false;
89+
}
90+
}
91+
}
92+
93+
return true;
94+
};
95+
5296
export default anyOfErrorHandler;

src/error-handlers/oneOf.js

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as Instance from "@hyperjump/json-schema/instance/experimental";
2+
import * as Pact from "@hyperjump/pact";
23
import { getErrors } from "../json-schema-errors.js";
34

45
/**
5-
* @import { ErrorHandler, ErrorObject } from "../index.d.ts"
6+
* @import { ErrorHandler, ErrorObject, InstanceOutput } from "../index.d.ts"
67
*/
78

89
/** @type ErrorHandler */
@@ -16,21 +17,45 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => {
1617
continue;
1718
}
1819

20+
const propertyLocations = Pact.pipe(
21+
Instance.values(instance),
22+
Pact.map(Instance.uri),
23+
Pact.collectArray
24+
);
25+
26+
const discriminators = propertyLocations.filter((propertyLocation) => {
27+
return oneOf.some((alternative) => isPassingProperty(alternative[propertyLocation]));
28+
});
29+
1930
const alternatives = [];
2031
const instanceLocation = Instance.uri(instance);
2132
let matchCount = 0;
2233

2334
for (const alternative of oneOf) {
24-
const typeErrors = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"];
25-
const match = !typeErrors || Object.values(typeErrors).every((isValid) => isValid);
35+
// Filter alternatives whose declared type doesn't match the instance type
36+
const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"];
37+
if (typeResults && !Object.values(typeResults).every((isValid) => isValid)) {
38+
continue;
39+
}
2640

27-
if (match) {
28-
const alternativeErrors = await getErrors(alternative, instance, localization);
29-
if (alternativeErrors.length) {
30-
alternatives.push(alternativeErrors);
31-
} else {
32-
matchCount++;
41+
if (Instance.typeOf(instance) === "object") {
42+
// Filter alternative if it has no declared properties in common with the instance
43+
if (!propertyLocations.some((propertyLocation) => propertyLocation in alternative)) {
44+
continue;
3345
}
46+
47+
// Filter alternative if it has failing properties that are declared and passing in another alternative
48+
if (discriminators.some((propertyLocation) => !isPassingProperty(alternative[propertyLocation]))) {
49+
continue;
50+
}
51+
}
52+
53+
// The alternative passed all the filters
54+
const alternativeErrors = await getErrors(alternative, instance, localization);
55+
if (alternativeErrors.length) {
56+
alternatives.push(alternativeErrors);
57+
} else {
58+
matchCount++;
3459
}
3560
}
3661

@@ -60,4 +85,21 @@ const oneOfErrorHandler = async (normalizedErrors, instance, localization) => {
6085
return errors;
6186
};
6287

88+
/** @type (alternative: InstanceOutput | undefined) => boolean */
89+
const isPassingProperty = (propertyOutput) => {
90+
if (!propertyOutput) {
91+
return false;
92+
}
93+
94+
for (const keywordUri in propertyOutput) {
95+
for (const schemaLocation in propertyOutput[keywordUri]) {
96+
if (propertyOutput[keywordUri][schemaLocation] !== true) {
97+
return false;
98+
}
99+
}
100+
}
101+
102+
return true;
103+
};
104+
63105
export default oneOfErrorHandler;

0 commit comments

Comments
 (0)