1
1
import { normalizeOutputFormat } from "./normalizeOutputFormat/normalizeOutput.js" ;
2
2
import * as Schema from "@hyperjump/browser" ;
3
- import { getSchema } from "@hyperjump/json-schema/experimental" ;
3
+ import { getKeywordByName , getSchema } from "@hyperjump/json-schema/experimental" ;
4
4
import * as Instance from "@hyperjump/json-pointer" ;
5
5
import leven from "leven" ;
6
6
@@ -19,7 +19,7 @@ export async function betterJsonSchemaErrors(instance, errorOutput, schemaUri) {
19
19
const output = { errors : [ ] } ;
20
20
21
21
for ( const errorHandler of errorHandlers ) {
22
- const errorObject = await errorHandler ( normalizedErrors , instance ) ;
22
+ const errorObject = await errorHandler ( normalizedErrors , instance , schema ) ;
23
23
if ( errorObject ) {
24
24
output . errors . push ( ...errorObject ) ;
25
25
}
@@ -29,31 +29,64 @@ export async function betterJsonSchemaErrors(instance, errorOutput, schemaUri) {
29
29
}
30
30
31
31
/**
32
- * @typedef {(normalizedErrors: NormalizedError[], instance: Json) => Promise<ErrorObject[]> } ErrorHandler
32
+ * @typedef {(normalizedErrors: NormalizedError[], instance: Json, schema: Browser<SchemaDocument> ) => Promise<ErrorObject[]> } ErrorHandler
33
33
*/
34
34
35
35
/** @type ErrorHandler[] */
36
36
const errorHandlers = [
37
- // async (normalizedErrors) => {
38
- // /** @type ErrorObject[] */
39
- // const errors = [];
40
- // for (const error of normalizedErrors) {
41
- // if (error.keyword === "https://json-schema.org/keyword/anyOf") {
42
- // // const outputArray = applicatorChildErrors(outputUnit.absoluteKeywordLocation, normalizedErrors);
43
- // // const failingTypeErrors = outputArray
44
- // // .filter((err) => err.keyword === "https://json-schema.org/keyword/type")
45
- // // .map((err) => err.instanceLocation);
46
- // // const numberOfAlternatives = /** @type any[] */ (Schema.value(schema)).length;
47
- // errors.push({
48
- // message: `The instance must be a 'string' or 'number'. Found 'boolean'`,
49
- // instanceLocation: error.instanceLocation,
50
- // schemaLocation: error.absoluteKeywordLocation
51
- // });
52
- // }
53
- // }
54
-
55
- // return errors;
56
- // },
37
+
38
+ // `anyOf` handler
39
+ async ( normalizedErrors , instance , schema ) => {
40
+ /** @type ErrorObject[] */
41
+ const errors = [ ] ;
42
+
43
+ for ( const error of normalizedErrors ) {
44
+ if ( error . keyword === "https://json-schema.org/keyword/anyOf" ) {
45
+ const anyOfSchema = await getSchema ( error . absoluteKeywordLocation ) ;
46
+ const numberOfAlternatives = Schema . length ( anyOfSchema ) ;
47
+ // const discriminatorKeys = await findDiscriminatorKeywords(anyOfSchema);
48
+ const outputArray = applicatorChildErrors ( error . absoluteKeywordLocation , normalizedErrors ) ;
49
+
50
+ const keyword = getKeywordByName ( "type" , schema . document . dialectId ) ;
51
+ const matchingKeywordErrors = outputArray . filter ( ( e ) => e . keyword === keyword . id ) ;
52
+
53
+ if ( isOnlyOneTypeValid ( matchingKeywordErrors , numberOfAlternatives ) ) {
54
+ // all the matchingKeywordErrors are filter out from the outputArray and push in the normalizedErrors array to produce the output.
55
+ const remainingErrors = outputArray . filter ( ( err ) => {
56
+ return ! matchingKeywordErrors . some ( ( matchingErr ) => {
57
+ return matchingErr . absoluteKeywordLocation === err . absoluteKeywordLocation ;
58
+ } ) ;
59
+ } ) ;
60
+ normalizedErrors . push ( ...remainingErrors ) ;
61
+ } else if ( matchingKeywordErrors . length === numberOfAlternatives ) {
62
+ const noMatchFound = await noDiscriminatorKeyMatchError ( matchingKeywordErrors , error , instance ) ;
63
+ errors . push ( noMatchFound ) ;
64
+ } else if ( false ) {
65
+ // Discriminator cases
66
+ } else if ( jsonTypeOf ( instance ) === "object" ) {
67
+ // Number of matching properties
68
+ const selectedAlternative = outputArray . find ( ( error ) => {
69
+ return error . keyword = "https://json-schema.org/keyword/properties" ;
70
+ } ) ?. absoluteKeywordLocation ;
71
+ const remainingErrors = outputArray . filter ( ( err ) => {
72
+ return err . absoluteKeywordLocation . startsWith ( /** @type string */ ( selectedAlternative ) ) ;
73
+ } ) ;
74
+ normalizedErrors . push ( ...remainingErrors ) ;
75
+ } else {
76
+ // I don't know yet what to do
77
+
78
+ // {
79
+ // "$schema": "https://json-schema.org/draft/2020-12/schema",
80
+ // "anyOf": [
81
+ // { "required": [ "foo" ] },
82
+ // { "required": [ "bar" ] }
83
+ // ]
84
+ // }
85
+ }
86
+ }
87
+ }
88
+ return errors ;
89
+ } ,
57
90
58
91
async ( normalizedErrors ) => {
59
92
/** @type ErrorObject[] */
@@ -393,14 +426,88 @@ const errorHandlers = [
393
426
}
394
427
] ;
395
428
396
- // /**
397
- // * Groups errors whose absoluteKeywordLocation starts with a given prefix.
398
- // * @param {string } parentKeywordLocation
399
- // * @param {NormalizedError[] } allErrors
400
- // * @returns {NormalizedError[] }
401
- // */
402
- // function applicatorChildErrors(parentKeywordLocation, allErrors) {
403
- // return allErrors.filter((err) =>
404
- // /** @type string */ (err.absoluteKeywordLocation).startsWith(parentKeywordLocation + "/")
405
- // );
406
- // }
429
+ /**
430
+ * Groups errors whose absoluteKeywordLocation starts with a given prefix.
431
+ * @param {string } parentKeywordLocation
432
+ * @param {NormalizedError[] } allErrors
433
+ * @returns {NormalizedError[] }
434
+ */
435
+ function applicatorChildErrors ( parentKeywordLocation , allErrors ) {
436
+ const matching = [ ] ;
437
+
438
+ for ( let i = allErrors . length - 1 ; i >= 0 ; i -- ) {
439
+ const err = allErrors [ i ] ;
440
+ if ( err . absoluteKeywordLocation . startsWith ( parentKeywordLocation + "/" ) ) {
441
+ matching . push ( err ) ;
442
+ allErrors . splice ( i , 1 ) ;
443
+ }
444
+ }
445
+
446
+ return matching ;
447
+ }
448
+
449
+ /**
450
+ * @param {NormalizedError[] } matchingErrors
451
+ * @param {number } numOfAlternatives
452
+ * @returns {boolean }
453
+ */
454
+ function isOnlyOneTypeValid ( matchingErrors , numOfAlternatives ) {
455
+ const typeErrors = matchingErrors . filter (
456
+ ( e ) => e . keyword === "https://json-schema.org/keyword/type"
457
+ ) ;
458
+ return numOfAlternatives - typeErrors . length === 1 ;
459
+ }
460
+
461
+ /**
462
+ * @param {NormalizedError[] } matchingErrors
463
+ * @param {NormalizedError } parentError
464
+ * @param {Json } instance
465
+ * @returns {Promise<ErrorObject> }
466
+ */
467
+ async function noDiscriminatorKeyMatchError ( matchingErrors , parentError , instance ) {
468
+ const expectedTypes = [ ] ;
469
+
470
+ for ( const err of matchingErrors ) {
471
+ const typeSchema = await getSchema ( err . absoluteKeywordLocation ) ;
472
+ const typeValue = /** @type any[] */ ( Schema . value ( typeSchema ) ) ;
473
+ expectedTypes . push ( typeValue ) ;
474
+ }
475
+
476
+ const pointer = parentError . instanceLocation . replace ( / ^ # / , "" ) ;
477
+ const actualValue = /** @type Json */ ( Instance . get ( pointer , instance ) ) ;
478
+ const actualType = jsonTypeOf ( actualValue ) ;
479
+
480
+ const expectedString = expectedTypes . join ( " or " ) ;
481
+
482
+ return {
483
+ message : `The instance must be a ${ expectedString } . Found '${ actualType } '.` ,
484
+ instanceLocation : parentError . instanceLocation ,
485
+ schemaLocation : parentError . absoluteKeywordLocation
486
+ } ;
487
+ }
488
+
489
+ /** @type (value: Json) => "null" | "boolean" | "number" | "string" | "array" | "object" | "undefined" */
490
+ const jsonTypeOf = ( value ) => {
491
+ const jsType = typeof value ;
492
+
493
+ switch ( jsType ) {
494
+ case "number" :
495
+ case "string" :
496
+ case "boolean" :
497
+ case "undefined" :
498
+ return jsType ;
499
+ case "object" :
500
+ if ( Array . isArray ( value ) ) {
501
+ return "array" ;
502
+ } else if ( value === null ) {
503
+ return "null" ;
504
+ } else if ( Object . getPrototypeOf ( value ) === Object . prototype ) {
505
+ return "object" ;
506
+ }
507
+ default : {
508
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
509
+ const type = jsType === "object" ? Object . getPrototypeOf ( value ) . constructor . name ?? "anonymous" : jsType ;
510
+ throw Error ( `Not a JSON compatible type: ${ type } ` ) ;
511
+ }
512
+ }
513
+ } ;
0 commit comments