11import { getSchema } from "@hyperjump/json-schema/experimental" ;
2- import * as Schema from "@hyperjump/browser" ;
32import * as Instance from "@hyperjump/json-schema/instance/experimental" ;
3+ import * as Schema from "@hyperjump/browser" ;
4+ import * as JsonPointer from "@hyperjump/json-pointer" ;
45import { getErrors } from "../error-handling.js" ;
56
67/**
78 * @import { ErrorHandler, ErrorObject, NormalizedOutput } from "../index.d.ts"
89 */
910
1011/** @type ErrorHandler */
11- const anyOf = async ( normalizedErrors , instance , localization ) => {
12+ const anyOfErrorHandler = async ( normalizedErrors , instance , localization ) => {
1213 /** @type ErrorObject[] */
1314 const errors = [ ] ;
1415
1516 if ( normalizedErrors [ "https://json-schema.org/keyword/anyOf" ] ) {
1617 for ( const schemaLocation in normalizedErrors [ "https://json-schema.org/keyword/anyOf" ] ) {
18+ const allAlternatives = normalizedErrors [ "https://json-schema.org/keyword/anyOf" ] [ schemaLocation ] ;
19+ if ( typeof allAlternatives === "boolean" ) {
20+ continue ;
21+ }
22+
1723 /** @type NormalizedOutput[] */
1824 const alternatives = [ ] ;
19- const allAlternatives = /** @type NormalizedOutput[] */ ( normalizedErrors [ "https://json-schema.org/keyword/anyOf" ] [ schemaLocation ] ) ;
2025 for ( const alternative of allAlternatives ) {
21- if ( Object . values ( alternative [ Instance . uri ( instance ) ] [ "https://json-schema.org/keyword/type" ] ) . every ( ( valid ) => valid ) ) {
26+ if ( Object . values ( alternative [ Instance . uri ( instance ) ] [ "https://json-schema.org/keyword/type" ] ?? { } ) . every ( ( valid ) => valid ) ) {
2227 alternatives . push ( alternative ) ;
2328 }
2429 }
25- // case 1 where no. alternative matched the type of the instance.
30+
31+ // No alternative matched the type of the instance.
2632 if ( alternatives . length === 0 ) {
2733 /** @type Set<string> */
2834 const expectedTypes = new Set ( ) ;
35+
2936 for ( const alternative of allAlternatives ) {
3037 for ( const instanceLocation in alternative ) {
3138 if ( instanceLocation === Instance . uri ( instance ) ) {
@@ -37,28 +44,114 @@ const anyOf = async (normalizedErrors, instance, localization) => {
3744 }
3845 }
3946 }
47+
4048 errors . push ( {
4149 message : localization . getTypeErrorMessage ( [ ...expectedTypes ] , Instance . typeOf ( instance ) ) ,
4250 instanceLocation : Instance . uri ( instance ) ,
4351 schemaLocation : schemaLocation
4452 } ) ;
45- } else if ( alternatives . length === 1 ) { // case 2 when only one type match
46- return getErrors ( alternatives [ 0 ] , instance , localization ) ;
47- } else if ( instance . type === "object" ) {
48- let targetAlternativeIndex = - 1 ;
49- for ( const alternative of alternatives ) {
50- targetAlternativeIndex ++ ;
53+ continue ;
54+ }
55+
56+ // Only one alternative matches the type of the instance
57+ if ( alternatives . length === 1 ) {
58+ errors . push ( ...await getErrors ( alternatives [ 0 ] , instance , localization ) ) ;
59+ continue ;
60+ }
61+
62+ if ( instance . type === "object" ) {
63+ const definedProperties = allAlternatives . map ( ( alternative ) => {
64+ /** @type Set<string> */
65+ const alternativeProperties = new Set ( ) ;
66+
5167 for ( const instanceLocation in alternative ) {
52- if ( instanceLocation !== "#" ) {
53- return getErrors ( alternatives [ targetAlternativeIndex ] , instance , localization ) ;
68+ const pointer = instanceLocation . slice ( Instance . uri ( instance ) . length + 1 ) ;
69+ if ( pointer . length > 0 ) {
70+ const position = pointer . indexOf ( "/" ) ;
71+ const propertyName = pointer . slice ( 0 , position === - 1 ? undefined : position ) ;
72+ const location = JsonPointer . append ( propertyName , Instance . uri ( instance ) ) ;
73+ alternativeProperties . add ( location ) ;
5474 }
5575 }
76+
77+ return alternativeProperties ;
78+ } ) ;
79+
80+ const discriminator = definedProperties . reduce ( ( acc , properties ) => {
81+ return acc . intersection ( properties ) ;
82+ } , definedProperties [ 0 ] ) ;
83+
84+ const discriminatedAlternatives = alternatives . filter ( ( alternative ) => {
85+ for ( const instanceLocation in alternative ) {
86+ if ( ! discriminator . has ( instanceLocation ) ) {
87+ continue ;
88+ }
89+
90+ let valid = true ;
91+ for ( const keyword in alternative [ instanceLocation ] ) {
92+ for ( const schemaLocation in alternative [ instanceLocation ] [ keyword ] ) {
93+ if ( alternative [ instanceLocation ] [ keyword ] [ schemaLocation ] !== true ) {
94+ valid = false ;
95+ break ;
96+ }
97+ }
98+ }
99+ if ( valid ) {
100+ return true ;
101+ }
102+ }
103+ return false ;
104+ } ) ;
105+
106+ // Discriminator match
107+ if ( discriminatedAlternatives . length === 1 ) {
108+ errors . push ( ...await getErrors ( discriminatedAlternatives [ 0 ] , instance , localization ) ) ;
109+ continue ;
56110 }
111+
112+ // Discriminator identified, but none of the alternatives match
113+ if ( discriminatedAlternatives . length === 0 ) {
114+ // TODO: How do we handle this case?
115+ }
116+
117+ // Last resort, select the alternative with the most properties matching the instance
118+ // TODO: We shouldn't use this strategy if alternatives have the same number of matching instances
119+ const instanceProperties = new Set ( Instance . values ( instance )
120+ . map ( ( node ) => Instance . uri ( node ) ) ) ;
121+ let maxMatches = - 1 ;
122+ let selectedIndex = 0 ;
123+ let index = - 1 ;
124+ for ( const alternativeProperties of definedProperties ) {
125+ index ++ ;
126+ const matches = alternativeProperties . intersection ( instanceProperties ) . size ;
127+ if ( matches > maxMatches ) {
128+ selectedIndex = index ;
129+ }
130+ }
131+
132+ errors . push ( ...await getErrors ( alternatives [ selectedIndex ] , instance , localization ) ) ;
133+ continue ;
57134 }
135+
136+ // TODO: Handle alternatives with const
137+ // TODO: Handle alternatives with enum
138+ // TODO: Handle null alternatives
139+ // TODO: Handle boolean alternatives
140+ // TODO: Handle string alternatives
141+ // TODO: Handle array alternatives
142+ // TODO: Handle alternatives without a type
143+
144+ // TODO: If we get here, we don't know what else to do and give a very generic message
145+ // Ideally this should be replace by something that can handle whatever case is missing.
146+ errors . push ( {
147+ message : localization . getAnyOfErrorMessage ( ) ,
148+ instanceLocation : Instance . uri ( instance ) ,
149+ schemaLocation : schemaLocation
150+ } ) ;
58151 }
59152 }
60153
61154 return errors ;
62155} ;
63156
64- export default anyOf ;
157+ export default anyOfErrorHandler ;
0 commit comments