Skip to content

Commit d563ab6

Browse files
authored
Merge pull request #60 from arpitkuriyal/anyOf
Handle more anyOF cases
2 parents 5cd9ede + 3e8a342 commit d563ab6

File tree

6 files changed

+789
-66
lines changed

6 files changed

+789
-66
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ BetterJSONSchemaErrors Output:-
200200
```
201201
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)
202202

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

src/error-handlers/anyOf.js

Lines changed: 79 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental";
33
import * as Schema from "@hyperjump/browser";
44
import * as JsonPointer from "@hyperjump/json-pointer";
55
import { getErrors } from "../error-handling.js";
6+
import { getSchemaDescription } from "../schema-descriptions.js";
67

78
/**
8-
* @import { ErrorHandler, ErrorObject, Json, NormalizedOutput } from "../index.d.ts"
9+
* @import { ErrorHandler, ErrorObject, Json, NormalizedOutput, InstanceOutput } from "../index.d.ts"
910
*/
1011

1112
/** @type ErrorHandler */
@@ -19,7 +20,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
1920
if (typeof allAlternatives === "boolean") {
2021
continue;
2122
}
22-
2323
/** @type NormalizedOutput[] */
2424
const alternatives = [];
2525
for (const alternative of allAlternatives) {
@@ -33,7 +33,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
3333
const isConstValid = schemaErrors["https://json-schema.org/keyword/const"]
3434
? Object.values(schemaErrors["https://json-schema.org/keyword/const"] ?? {}).every((valid) => valid)
3535
: undefined;
36-
3736
if (isTypeValid === true || isEnumValid === true || isConstValid === true) {
3837
alternatives.push(alternative);
3938
}
@@ -46,19 +45,31 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
4645
// No alternative matched the type/enum/const of the instance.
4746
if (alternatives.length === 0) {
4847
/** @type Set<string> */
49-
const expectedTypes = new Set();
48+
let expectedTypes = new Set();
5049

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

5453
for (const alternative of allAlternatives) {
5554
for (const instanceLocation in alternative) {
5655
if (instanceLocation === Instance.uri(instance)) {
56+
let alternativeTypes = new Set(["null", "boolean", "number", "string", "array", "object"]);
5757
for (const schemaLocation in alternative[instanceLocation]["https://json-schema.org/keyword/type"]) {
5858
const keyword = await getSchema(schemaLocation);
59-
const expectedType = /** @type string */ (Schema.value(keyword));
60-
expectedTypes.add(expectedType);
59+
if (Schema.typeOf(keyword) === "array") {
60+
const expectedTypes = /** @type string[] */ (Schema.value(keyword));
61+
alternativeTypes = alternativeTypes.intersection(new Set(expectedTypes));
62+
} else {
63+
const expectedType = /** @type string */ (Schema.value(keyword));
64+
alternativeTypes = alternativeTypes.intersection(new Set([expectedType]));
65+
}
66+
}
67+
68+
// The are 6 types. If all types are allowed, don't use expectedTypes
69+
if (alternativeTypes.size !== 6) {
70+
expectedTypes = expectedTypes.union(alternativeTypes);
6171
}
72+
6273
for (const schemaLocation in alternative[instanceLocation]["https://json-schema.org/keyword/enum"]) {
6374
const keyword = await getSchema(schemaLocation);
6475
const enums = /** @type Json[] */ (Schema.value(keyword));
@@ -74,7 +85,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
7485
}
7586
}
7687
}
77-
7888
errors.push({
7989
message: localization.getEnumErrorMessage({
8090
allowedValues: [...expectedEnums],
@@ -96,7 +106,6 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
96106
const definedProperties = allAlternatives.map((alternative) => {
97107
/** @type Set<string> */
98108
const alternativeProperties = new Set();
99-
100109
for (const instanceLocation in alternative) {
101110
const pointer = instanceLocation.slice(Instance.uri(instance).length + 1);
102111
if (pointer.length > 0) {
@@ -106,69 +115,86 @@ const anyOfErrorHandler = async (normalizedErrors, instance, localization) => {
106115
alternativeProperties.add(location);
107116
}
108117
}
109-
110118
return alternativeProperties;
111119
});
112120

113-
const discriminator = definedProperties.reduce((acc, properties) => {
114-
return acc.intersection(properties);
115-
}, definedProperties[0]);
116-
const discriminatedAlternatives = alternatives.filter((alternative) => {
117-
for (const instanceLocation in alternative) {
118-
if (!discriminator.has(instanceLocation)) {
119-
continue;
120-
}
121+
const anyPropertiesDefined = definedProperties.some((propSet) => propSet.size > 0);
121122

122-
let valid = true;
123-
for (const keyword in alternative[instanceLocation]) {
124-
for (const schemaLocation in alternative[instanceLocation][keyword]) {
125-
if (alternative[instanceLocation][keyword][schemaLocation] !== true) {
126-
valid = false;
127-
break;
123+
if (anyPropertiesDefined) {
124+
const discriminator = definedProperties.reduce((acc, properties) => {
125+
return acc.intersection(properties);
126+
}, definedProperties[0]);
127+
const discriminatedAlternatives = alternatives.filter((alternative) => {
128+
for (const instanceLocation in alternative) {
129+
if (!discriminator.has(instanceLocation)) {
130+
continue;
131+
}
132+
let valid = true;
133+
for (const keyword in alternative[instanceLocation]) {
134+
for (const schemaLocation in alternative[instanceLocation][keyword]) {
135+
if (alternative[instanceLocation][keyword][schemaLocation] !== true) {
136+
valid = false;
137+
break;
138+
}
128139
}
129140
}
141+
if (valid) {
142+
return true;
143+
}
130144
}
131-
if (valid) {
132-
return true;
133-
}
145+
return false;
146+
});
147+
// Discriminator match
148+
if (discriminatedAlternatives.length === 1) {
149+
errors.push(...await getErrors(discriminatedAlternatives[0], instance, localization));
150+
continue;
151+
}
152+
// Discriminator identified, but none of the alternatives match
153+
if (discriminatedAlternatives.length === 0) {
154+
// TODO: For now, it will use the schema description strategy
134155
}
135-
return false;
136-
});
137156

138-
// Discriminator match
139-
if (discriminatedAlternatives.length === 1) {
140-
errors.push(...await getErrors(discriminatedAlternatives[0], instance, localization));
157+
// Last resort, select the alternative with the most properties matching the instance
158+
const instanceProperties = new Set(Instance.values(instance).map((node) => Instance.uri(node)));
159+
let maxMatches = -1;
160+
let selectedIndex = 0;
161+
let index = -1;
162+
for (const alternativeProperties of definedProperties) {
163+
index++;
164+
const matches = alternativeProperties.intersection(instanceProperties).size;
165+
if (matches > maxMatches) {
166+
selectedIndex = index;
167+
}
168+
}
169+
errors.push(...await getErrors(alternatives[selectedIndex], instance, localization));
141170
continue;
142171
}
172+
}
143173

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

149-
// Last resort, select the alternative with the most properties matching the instance
150-
// TODO: We shouldn't use this strategy if alternatives have the same number of matching instances
151-
const instanceProperties = new Set(Instance.values(instance)
152-
.map((node) => Instance.uri(node)));
153-
let maxMatches = -1;
154-
let selectedIndex = 0;
155-
let index = -1;
156-
for (const alternativeProperties of definedProperties) {
157-
index++;
158-
const matches = alternativeProperties.intersection(instanceProperties).size;
159-
if (matches > maxMatches) {
160-
selectedIndex = index;
161-
}
176+
/** @type string[] */
177+
const descriptions = [];
178+
let allAlternativesHaveDescriptions = true;
179+
for (const alternative of alternatives) {
180+
const description = await getSchemaDescription(normalizedErrors, alternative[Instance.uri(instance)], localization);
181+
if (description !== undefined) {
182+
descriptions.push(description);
183+
} else {
184+
allAlternativesHaveDescriptions = false;
185+
break;
162186
}
187+
}
163188

164-
errors.push(...await getErrors(alternatives[selectedIndex], instance, localization));
189+
if (allAlternativesHaveDescriptions) {
190+
errors.push({
191+
message: localization.getAnyOfBulletsErrorMessage(descriptions),
192+
instanceLocation: Instance.uri(instance),
193+
schemaLocation: schemaLocation
194+
});
165195
continue;
166196
}
167197

168-
// TODO: Handle string alternatives
169-
// TODO: Handle array alternatives
170-
// TODO: Handle alternatives without a type
171-
172198
// TODO: If we get here, we don't know what else to do and give a very generic message
173199
// Ideally this should be replace by something that can handle whatever case is missing.
174200
errors.push({

0 commit comments

Comments
 (0)