@@ -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 } 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 ) {
@@ -165,15 +171,154 @@ export function resolveRef<T>(
165
171
return node ;
166
172
}
167
173
174
+ function createDiscriminatorEnum (
175
+ values : string [ ] ,
176
+ prevSchema ?: SchemaObject ,
177
+ ) : SchemaObject {
178
+ return {
179
+ type : "string" ,
180
+ enum : values ,
181
+ description : prevSchema ?. description
182
+ ? `${ prevSchema . description } (enum property replaced by openapi-typescript)`
183
+ : `discriminator enum property added by openapi-typescript` ,
184
+ } ;
185
+ }
186
+
187
+ type InternalDiscriminatorMapping = Record <
188
+ string ,
189
+ { inferred ?: string ; defined ?: string [ ] }
190
+ > ;
191
+
168
192
/** Return a key–value map of discriminator objects found in a schema */
169
- export function scanDiscriminators ( schema : OpenAPI3 ) {
170
- const discriminators : Record < string , DiscriminatorObject > = { } ;
193
+ export function scanDiscriminators (
194
+ schema : OpenAPI3 ,
195
+ options : OpenAPITSOptions ,
196
+ ) {
197
+ // all discriminator objects found in the schema
198
+ const objects : Record < string , DiscriminatorObject > = { } ;
199
+
200
+ // refs of all mapped schema objects we have successfully handled to infer the discriminator enum value
201
+ const refsHandled : string [ ] = [ ] ;
171
202
172
- // perform 2 passes: first, collect all discriminator definitions
203
+ // perform 2 passes: first, collect all discriminator definitions and handle oneOf and mappings
173
204
walk ( schema , ( obj , path ) => {
174
- if ( ( obj ?. discriminator as DiscriminatorObject ) ?. propertyName ) {
175
- discriminators [ createRef ( path ) ] =
176
- obj . discriminator as DiscriminatorObject ;
205
+ const discriminator = obj ?. discriminator as DiscriminatorObject | undefined ;
206
+ if ( ! discriminator ?. propertyName ) {
207
+ return ;
208
+ }
209
+
210
+ // collect discriminator object for later usage
211
+ const ref = createRef ( path ) ;
212
+
213
+ objects [ ref ] = discriminator ;
214
+
215
+ // 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
216
+ // we only handle the mapping in advance for discriminator + oneOf compositions right now
217
+ if ( ! obj ?. oneOf || ! Array . isArray ( obj . oneOf ) ) {
218
+ return ;
219
+ }
220
+
221
+ const oneOf : ( SchemaObject | ReferenceObject ) [ ] = obj . oneOf ;
222
+ const mapping : InternalDiscriminatorMapping = { } ;
223
+
224
+ // the mapping can be inferred from the oneOf refs next to the discriminator object
225
+ for ( const item of oneOf ) {
226
+ if ( "$ref" in item ) {
227
+ // the name of the schema is the inferred discriminator enum value
228
+ const value = item . $ref . split ( "/" ) . pop ( ) ;
229
+
230
+ if ( value ) {
231
+ if ( ! mapping [ item . $ref ] ) {
232
+ mapping [ item . $ref ] = { inferred : value } ;
233
+ } else {
234
+ mapping [ item . $ref ] . inferred = value ;
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ // the mapping can be defined in the discriminator object itself
241
+ if ( discriminator . mapping ) {
242
+ for ( const mappedValue in discriminator . mapping ) {
243
+ const mappedRef = discriminator . mapping [ mappedValue ] ;
244
+ if ( ! mappedRef ) {
245
+ continue ;
246
+ }
247
+
248
+ if ( ! mapping [ mappedRef ] ?. defined ) {
249
+ // this overrides inferred values, but we don't need them anymore as soon as we have a defined value
250
+ mapping [ mappedRef ] = { defined : [ ] } ;
251
+ }
252
+
253
+ mapping [ mappedRef ] . defined ?. push ( mappedValue ) ;
254
+ }
255
+ }
256
+
257
+ for ( const [ mappedRef , { inferred, defined } ] of Object . entries ( mapping ) ) {
258
+ if ( refsHandled . includes ( mappedRef ) ) {
259
+ continue ;
260
+ }
261
+
262
+ if ( ! inferred && ! defined ) {
263
+ continue ;
264
+ }
265
+
266
+ // prefer defined values over automatically inferred ones
267
+ // the inferred enum values from the schema might not represent the actual enum values of the discriminator,
268
+ // so if we have defined values, use them instead
269
+ const mappedValues = defined ?? [ inferred ! ] ;
270
+ const resolvedSchema = resolveRef < SchemaObject > ( schema , mappedRef , {
271
+ silent : options . silent ?? false ,
272
+ } ) ;
273
+
274
+ if ( resolvedSchema ?. allOf ) {
275
+ // if the schema is an allOf, we can append a new schema object to the allOf array
276
+ resolvedSchema . allOf . push ( {
277
+ type : "object" ,
278
+ // discriminator enum properties always need to be required
279
+ required : [ discriminator . propertyName ] ,
280
+ properties : {
281
+ [ discriminator . propertyName ] : createDiscriminatorEnum ( mappedValues ) ,
282
+ } ,
283
+ } ) ;
284
+
285
+ refsHandled . push ( mappedRef ) ;
286
+ } else if (
287
+ typeof resolvedSchema === "object" &&
288
+ "type" in resolvedSchema &&
289
+ resolvedSchema . type === "object"
290
+ ) {
291
+ // if the schema is an object, we can apply the discriminator enums to its properties
292
+ if ( ! resolvedSchema . properties ) {
293
+ resolvedSchema . properties = { } ;
294
+ }
295
+
296
+ // discriminator enum properties always need to be required
297
+ if ( ! resolvedSchema . required ) {
298
+ resolvedSchema . required = [ discriminator . propertyName ] ;
299
+ } else if (
300
+ ! resolvedSchema . required . includes ( discriminator . propertyName )
301
+ ) {
302
+ resolvedSchema . required . push ( discriminator . propertyName ) ;
303
+ }
304
+
305
+ // add/replace the discriminator enum property
306
+ resolvedSchema . properties [ discriminator . propertyName ] =
307
+ createDiscriminatorEnum (
308
+ mappedValues ,
309
+ resolvedSchema . properties [ discriminator . propertyName ] ,
310
+ ) ;
311
+
312
+ refsHandled . push ( mappedRef ) ;
313
+ } else {
314
+ warn (
315
+ `Discriminator mapping has an invalid schema (neither an object schema nor an allOf array): ${ mappedRef } => ${ mappedValues . join (
316
+ ", " ,
317
+ ) } (Discriminator: ${ ref } )`,
318
+ options . silent ,
319
+ ) ;
320
+ continue ;
321
+ }
177
322
}
178
323
} ) ;
179
324
@@ -185,19 +330,20 @@ export function scanDiscriminators(schema: OpenAPI3) {
185
330
if ( obj && Array . isArray ( obj [ key ] ) ) {
186
331
for ( const item of ( obj as any ) [ key ] ) {
187
332
if ( "$ref" in item ) {
188
- if ( discriminators [ item . $ref ] ) {
189
- discriminators [ createRef ( path ) ] = {
190
- ...discriminators [ item . $ref ] ,
333
+ if ( objects [ item . $ref ] ) {
334
+ objects [ createRef ( path ) ] = {
335
+ ...objects [ item . $ref ] ,
191
336
} ;
192
337
}
193
338
} else if ( item . discriminator ?. propertyName ) {
194
- discriminators [ createRef ( path ) ] = { ...item . discriminator } ;
339
+ objects [ createRef ( path ) ] = { ...item . discriminator } ;
195
340
}
196
341
}
197
342
}
198
343
}
199
344
} ) ;
200
- return discriminators ;
345
+
346
+ return { objects, refsHandled } ;
201
347
}
202
348
203
349
/** Walk through any JSON-serializable (i.e. non-circular) object */
0 commit comments