fork of hey-api/openapi-ts because I need some additional things

Merge pull request #1248 from hey-api/fix/parser-enum-null

fix: handle nullable enums in experimental parser

authored by

Lubos and committed by
GitHub
be276286 de21dfd0

+156 -48
+5
.changeset/cool-shrimps-juggle.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + fix: handle nullable enums in experimental parser
+30 -16
packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts
··· 407 407 408 408 for (const [index, enumValue] of schema.enum.entries()) { 409 409 const typeOfEnumValue = typeof enumValue; 410 + let enumType: SchemaType | 'null' | undefined; 411 + 410 412 if ( 411 413 typeOfEnumValue === 'string' || 412 414 typeOfEnumValue === 'number' || 413 415 typeOfEnumValue === 'boolean' 414 416 ) { 415 - const enumSchema = parseOneType({ 416 - context, 417 - schema: { 418 - description: schema['x-enum-descriptions']?.[index], 419 - title: 420 - schema['x-enum-varnames']?.[index] ?? 421 - schema['x-enumNames']?.[index], 422 - type: typeOfEnumValue, 423 - }, 424 - }); 425 - enumSchema.const = enumValue; 426 - schemaItems.push(enumSchema); 417 + enumType = typeOfEnumValue; 418 + } else if (enumValue === null) { 419 + // nullable must be true 420 + if (schema.nullable) { 421 + enumType = 'null'; 422 + } 427 423 } else { 428 424 console.warn( 429 425 '🚨', ··· 431 427 schema.enum, 432 428 ); 433 429 } 434 - } 435 430 436 - if (schema.nullable) { 437 - schemaItems.push({ 438 - type: 'null', 431 + if (!enumType) { 432 + continue; 433 + } 434 + 435 + const enumSchema = parseOneType({ 436 + context, 437 + schema: { 438 + description: schema['x-enum-descriptions']?.[index], 439 + title: 440 + schema['x-enum-varnames']?.[index] ?? schema['x-enumNames']?.[index], 441 + // cast enum to string temporarily 442 + type: enumType === 'null' ? 'string' : enumType, 443 + }, 439 444 }); 445 + 446 + enumSchema.const = enumValue; 447 + 448 + // cast enum back 449 + if (enumType === 'null') { 450 + enumSchema.type = enumType; 451 + } 452 + 453 + schemaItems.push(enumSchema); 440 454 } 441 455 442 456 irSchema = addItemsToSchema({
+27 -13
packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts
··· 443 443 irSchema.type = 'enum'; 444 444 445 445 const schemaItems: Array<IRSchemaObject> = []; 446 + const schemaTypes = getSchemaTypes({ schema }); 446 447 447 448 for (const [index, enumValue] of schema.enum.entries()) { 448 449 const typeOfEnumValue = typeof enumValue; 450 + let enumType: SchemaType | undefined; 451 + 449 452 if ( 450 453 typeOfEnumValue === 'string' || 451 454 typeOfEnumValue === 'number' || 452 455 typeOfEnumValue === 'boolean' 453 456 ) { 454 - schemaItems.push( 455 - parseOneType({ 456 - context, 457 - schema: { 458 - const: enumValue, 459 - description: schema['x-enum-descriptions']?.[index], 460 - title: 461 - schema['x-enum-varnames']?.[index] ?? 462 - schema['x-enumNames']?.[index], 463 - type: typeOfEnumValue, 464 - }, 465 - }), 466 - ); 457 + enumType = typeOfEnumValue; 458 + } else if (enumValue === null) { 459 + // type must contain null 460 + if (schemaTypes.includes('null')) { 461 + enumType = 'null'; 462 + } 467 463 } else { 468 464 console.warn( 469 465 '🚨', ··· 471 467 schema.enum, 472 468 ); 473 469 } 470 + 471 + if (!enumType) { 472 + continue; 473 + } 474 + 475 + schemaItems.push( 476 + parseOneType({ 477 + context, 478 + schema: { 479 + const: enumValue, 480 + description: schema['x-enum-descriptions']?.[index], 481 + title: 482 + schema['x-enum-varnames']?.[index] ?? 483 + schema['x-enumNames']?.[index], 484 + type: enumType, 485 + }, 486 + }), 487 + ); 474 488 } 475 489 476 490 irSchema = addItemsToSchema({
+5 -10
packages/openapi-ts/src/openApi/index.ts
··· 75 75 spec: spec as Record<string, any>, 76 76 }); 77 77 78 + // TODO: parser - handle Swagger 2.0 79 + 78 80 const ctx = context as IRContext<OpenApiV3_0_X | OpenApiV3_1_X>; 79 81 switch (ctx.spec.openapi) { 80 - // TODO: parser - handle Swagger 2.0 81 82 case '3.0.0': 82 83 case '3.0.1': 83 84 case '3.0.2': 84 85 case '3.0.3': 85 86 case '3.0.4': 86 87 parseV3_0_X(context as IRContext<OpenApiV3_0_X>); 87 - break; 88 + return context; 88 89 case '3.1.0': 89 90 case '3.1.1': 90 91 parseV3_1_X(context as IRContext<OpenApiV3_1_X>); 91 - break; 92 + return context; 92 93 default: 93 94 // TODO: parser - uncomment after removing legacy parser. 94 95 // For now, we fall back to legacy parser if spec version 95 96 // is not supported 96 97 // throw new Error('Unsupported OpenAPI specification'); 97 - break; 98 + return; 98 99 } 99 - 100 - if (!Object.keys(context.ir).length) { 101 - return; 102 - } 103 - 104 - return context; 105 100 };
+7
packages/openapi-ts/test/3.0.x.spec.ts
··· 55 55 }), 56 56 description: 'escapes enum values', 57 57 }, 58 + { 59 + config: createConfig({ 60 + input: 'enum-null.json', 61 + output: 'enum-null', 62 + }), 63 + description: 'handles null enums', 64 + }, 58 65 ]; 59 66 60 67 it.each(scenarios)('$description', async ({ config }) => {
+7
packages/openapi-ts/test/3.1.x.spec.ts
··· 64 64 }, 65 65 { 66 66 config: createConfig({ 67 + input: 'enum-null.json', 68 + output: 'enum-null', 69 + }), 70 + description: 'handles null enums', 71 + }, 72 + { 73 + config: createConfig({ 67 74 input: 'object-properties-all-of.json', 68 75 output: 'object-properties-all-of', 69 76 }),
+2
packages/openapi-ts/test/__snapshots__/3.0.x/enum-null/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen';
+7
packages/openapi-ts/test/__snapshots__/3.0.x/enum-null/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type Foo = 'foo' | 'bar' | null; 4 + 5 + export type Bar = 'foo' | 'bar'; 6 + 7 + export type Baz = 'foo' | 'bar';
+2
packages/openapi-ts/test/__snapshots__/3.1.x/enum-null/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './types.gen';
+7
packages/openapi-ts/test/__snapshots__/3.1.x/enum-null/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type Foo = 'foo' | 'bar' | null; 4 + 5 + export type Bar = 'foo' | 'bar'; 6 + 7 + export type Baz = 'foo' | 'bar';
+9 -9
packages/openapi-ts/test/sample.cjs
··· 11 11 // debug: true, 12 12 experimentalParser: true, 13 13 input: { 14 - include: 15 - '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$', 16 - path: './test/spec/3.1.x/full.json', 14 + // include: 15 + // '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$', 16 + path: './test/spec/3.1.x/enum-null.json', 17 17 // path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', 18 18 }, 19 19 // name: 'foo', ··· 27 27 // name: '@hey-api/schemas', 28 28 // type: 'json', 29 29 // }, 30 - { 31 - // asClass: true, 32 - // include... 33 - name: '@hey-api/services', 34 - // serviceNameBuilder: '^Parameters', 35 - }, 30 + // { 31 + // // asClass: true, 32 + // // include... 33 + // name: '@hey-api/services', 34 + // // serviceNameBuilder: '^Parameters', 35 + // }, 36 36 // { 37 37 // dates: true, 38 38 // name: '@hey-api/transformers',
+25
packages/openapi-ts/test/spec/3.0.x/enum-null.json
··· 1 + { 2 + "openapi": "3.0.2", 3 + "info": { 4 + "title": "OpenAPI 3.0.2 enum null example", 5 + "version": "1" 6 + }, 7 + "components": { 8 + "schemas": { 9 + "Foo": { 10 + "enum": ["foo", "bar", null], 11 + "nullable": true, 12 + "type": "string" 13 + }, 14 + "Bar": { 15 + "enum": ["foo", "bar", null], 16 + "type": "string" 17 + }, 18 + "Baz": { 19 + "enum": ["foo", "bar"], 20 + "nullable": true, 21 + "type": "string" 22 + } 23 + } 24 + } 25 + }
+23
packages/openapi-ts/test/spec/3.1.x/enum-null.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { 4 + "title": "OpenAPI 3.1.0 enum null example", 5 + "version": "1" 6 + }, 7 + "components": { 8 + "schemas": { 9 + "Foo": { 10 + "enum": ["foo", "bar", null], 11 + "type": ["string", "null"] 12 + }, 13 + "Bar": { 14 + "enum": ["foo", "bar", null], 15 + "type": "string" 16 + }, 17 + "Baz": { 18 + "enum": ["foo", "bar"], 19 + "type": ["string", "null"] 20 + } 21 + } 22 + } 23 + }