@@ -3,9 +3,10 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental";
33import * as Schema from "@hyperjump/browser" ;
44import * as JsonPointer from "@hyperjump/json-pointer" ;
55import { 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