@@ -5,7 +5,13 @@ import {
5
5
import c from "ansi-colors" ;
6
6
import supportsColor from "supports-color" ;
7
7
import ts from "typescript" ;
8
- import type { DiscriminatorObject , OpenAPI3 , OpenAPITSOptions , SchemaObject } from "../types.js" ;
8
+ import type {
9
+ DiscriminatorObject ,
10
+ OpenAPI3 ,
11
+ OpenAPITSOptions ,
12
+ ReferenceObject ,
13
+ SchemaObject ,
14
+ } from "../types.js" ;
9
15
import { tsLiteral , tsModifiers , tsPropertyIndex } from "./ts.js" ;
10
16
11
17
if ( ! supportsColor . stdout || supportsColor . stdout . hasBasic === false ) {
@@ -174,42 +180,103 @@ function createDiscriminatorEnum(value: string): SchemaObject {
174
180
}
175
181
176
182
/** Return a key–value map of discriminator objects found in a schema */
177
- export function scanDiscriminators ( schema : OpenAPI3 , options : OpenAPITSOptions ) {
183
+ export function scanDiscriminators (
184
+ schema : OpenAPI3 ,
185
+ options : OpenAPITSOptions ,
186
+ ) {
178
187
const discriminators : Record < string , DiscriminatorObject > = { } ;
179
- const enumsAppended : string [ ] = [ ] ;
188
+ // discriminator objects which we have successfully handled to infer the discriminator enum value
189
+ const discriminatorRefsHandled : string [ ] = [ ] ;
180
190
181
- // perform 2 passes: first, collect all discriminator definitions
191
+ // perform 2 passes: first, collect all discriminator definitions and handle oneOf and mappings
182
192
walk ( schema , ( obj , path ) => {
183
193
const discriminator = obj ?. discriminator as DiscriminatorObject | undefined ;
184
- if ( discriminator ?. propertyName ) {
185
- const ref = createRef ( path ) ;
186
-
187
- discriminators [ ref ] = discriminator ;
188
-
189
- // if a mapping is available we will help Typescript by adding the discriminator enum with its mapped value to each schema
190
- if ( discriminator . mapping ) {
191
- for ( const [ mappedValue , mappedRef ] of Object . entries (
192
- discriminator . mapping
193
- ) ) {
194
- if ( enumsAppended . includes ( mappedRef ) ) {
195
- continue ;
196
- }
194
+ if ( ! discriminator ?. propertyName ) {
195
+ return ;
196
+ }
197
197
198
- const resolved = resolveRef < SchemaObject > ( schema , mappedRef , { silent : options . silent ?? false } ) ;
199
- if ( resolved ?. allOf ) {
200
- resolved . allOf . push ( { type : "object" , properties : { [ discriminator . propertyName ] : createDiscriminatorEnum ( mappedValue ) } } ) ;
201
- enumsAppended . push ( mappedRef ) ;
202
- } else if ( typeof resolved === 'object' && 'type' in resolved && resolved . type === 'object' ) {
203
- if ( ! resolved . properties ) {
204
- resolved . properties = { } ;
205
- }
198
+ // add to discriminators object for later usage
199
+ const ref = createRef ( path ) ;
206
200
207
- resolved . properties [ discriminator . propertyName ] = createDiscriminatorEnum ( mappedValue ) ;
208
- enumsAppended . push ( mappedRef ) ;
209
- } else {
210
- continue ;
211
- }
201
+ discriminators [ ref ] = discriminator ;
202
+
203
+ // if a mapping is available we will help Typescript to infer properties by adding the discriminator enum with its single mapped value to each schema
204
+ // we only handle the mapping in advance for discriminator + oneOf compositions right now
205
+ if ( ! obj ?. oneOf || ! Array . isArray ( obj . oneOf ) ) {
206
+ return ;
207
+ }
208
+
209
+ const oneOf : ( SchemaObject | ReferenceObject ) [ ] = obj . oneOf ;
210
+ const mapping : Record < string , string > = { } ;
211
+
212
+ // the mapping can be inferred from the oneOf refs next to the discriminator object
213
+ for ( const item of oneOf ) {
214
+ if ( "$ref" in item ) {
215
+ // the name of the schema is the infered discriminator enum value
216
+ const value = item . $ref . split ( "/" ) . pop ( ) ;
217
+
218
+ if ( value ) {
219
+ mapping [ item . $ref ] = value ;
220
+ }
221
+ }
222
+ }
223
+
224
+ // the mapping can be defined in the discriminator object itself
225
+ if ( discriminator . mapping ) {
226
+ for ( const mappedValue in discriminator . mapping ) {
227
+ const mappedRef = discriminator . mapping [ mappedValue ] ;
228
+
229
+ mapping [ mappedRef ] = mappedValue ;
230
+ }
231
+ }
232
+
233
+ for ( const [ mappedRef , mappedValue ] of Object . entries ( mapping ) ) {
234
+ if ( discriminatorRefsHandled . includes ( mappedRef ) ) {
235
+ continue ;
236
+ }
237
+
238
+ const resolvedSchema = resolveRef < SchemaObject > ( schema , mappedRef , {
239
+ silent : options . silent ?? false ,
240
+ } ) ;
241
+ if ( resolvedSchema ?. allOf ) {
242
+ // if the schema is an allOf, we can append a new schema object to the allOf array
243
+ resolvedSchema . allOf . push ( {
244
+ type : "object" ,
245
+ required : [ discriminator . propertyName ] ,
246
+ properties : {
247
+ [ discriminator . propertyName ] : createDiscriminatorEnum ( mappedValue ) ,
248
+ } ,
249
+ } ) ;
250
+
251
+ discriminatorRefsHandled . push ( mappedRef ) ;
252
+ } else if (
253
+ typeof resolvedSchema === "object" &&
254
+ "type" in resolvedSchema &&
255
+ resolvedSchema . type === "object"
256
+ ) {
257
+ // if the schema is an object, we can add/replace the discriminator enum to it
258
+ if ( ! resolvedSchema . properties ) {
259
+ resolvedSchema . properties = { } ;
260
+ }
261
+
262
+ if ( ! resolvedSchema . required ) {
263
+ resolvedSchema . required = [ discriminator . propertyName ] ;
264
+ } else if (
265
+ ! resolvedSchema . required . includes ( discriminator . propertyName )
266
+ ) {
267
+ resolvedSchema . required . push ( discriminator . propertyName ) ;
212
268
}
269
+
270
+ resolvedSchema . properties [ discriminator . propertyName ] =
271
+ createDiscriminatorEnum ( mappedValue ) ;
272
+
273
+ discriminatorRefsHandled . push ( mappedRef ) ;
274
+ } else {
275
+ warn (
276
+ `Discriminator mapping has an invalid schema (neither an object schema nor an allOf array): ${ mappedRef } => ${ mappedValue } (Discriminator: ${ ref } )` ,
277
+ options . silent ,
278
+ ) ;
279
+ continue ;
213
280
}
214
281
}
215
282
} ) ;
@@ -234,7 +301,8 @@ export function scanDiscriminators(schema: OpenAPI3, options: OpenAPITSOptions)
234
301
}
235
302
}
236
303
} ) ;
237
- return discriminators ;
304
+
305
+ return { discriminators, discriminatorRefsHandled } ;
238
306
}
239
307
240
308
/** Walk through any JSON-serializable (i.e. non-circular) object */
0 commit comments