11import { normalizeOutputFormat } from "./normalizeOutputFormat/normalizeOutput.js" ;
22import * as Schema from "@hyperjump/browser" ;
3- import { getSchema } from "@hyperjump/json-schema/experimental" ;
3+ import { getKeywordByName , getSchema } from "@hyperjump/json-schema/experimental" ;
44import * as Instance from "@hyperjump/json-pointer" ;
55import leven from "leven" ;
66
@@ -19,7 +19,7 @@ export async function betterJsonSchemaErrors(instance, errorOutput, schemaUri) {
1919 const output = { errors : [ ] } ;
2020
2121 for ( const errorHandler of errorHandlers ) {
22- const errorObject = await errorHandler ( normalizedErrors , instance ) ;
22+ const errorObject = await errorHandler ( normalizedErrors , instance , schema ) ;
2323 if ( errorObject ) {
2424 output . errors . push ( ...errorObject ) ;
2525 }
@@ -29,31 +29,64 @@ export async function betterJsonSchemaErrors(instance, errorOutput, schemaUri) {
2929}
3030
3131/**
32- * @typedef {(normalizedErrors: NormalizedError[], instance: Json) => Promise<ErrorObject[]> } ErrorHandler
32+ * @typedef {(normalizedErrors: NormalizedError[], instance: Json, schema: Browser<SchemaDocument> ) => Promise<ErrorObject[]> } ErrorHandler
3333 */
3434
3535/** @type ErrorHandler[] */
3636const 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+ } ,
5790
5891 async ( normalizedErrors ) => {
5992 /** @type ErrorObject[] */
@@ -393,14 +426,88 @@ const errorHandlers = [
393426 }
394427] ;
395428
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