diff --git a/.changeset/cool-shrimps-juggle.md b/.changeset/cool-shrimps-juggle.md new file mode 100644 index 000000000..7bdc36c67 --- /dev/null +++ b/.changeset/cool-shrimps-juggle.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +fix: handle nullable enums in experimental parser diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts index 976684220..5049d4811 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts @@ -407,23 +407,19 @@ const parseEnum = ({ for (const [index, enumValue] of schema.enum.entries()) { const typeOfEnumValue = typeof enumValue; + let enumType: SchemaType | 'null' | undefined; + if ( typeOfEnumValue === 'string' || typeOfEnumValue === 'number' || typeOfEnumValue === 'boolean' ) { - const enumSchema = parseOneType({ - context, - schema: { - description: schema['x-enum-descriptions']?.[index], - title: - schema['x-enum-varnames']?.[index] ?? - schema['x-enumNames']?.[index], - type: typeOfEnumValue, - }, - }); - enumSchema.const = enumValue; - schemaItems.push(enumSchema); + enumType = typeOfEnumValue; + } else if (enumValue === null) { + // nullable must be true + if (schema.nullable) { + enumType = 'null'; + } } else { console.warn( '🚨', @@ -431,12 +427,30 @@ const parseEnum = ({ schema.enum, ); } - } - if (schema.nullable) { - schemaItems.push({ - type: 'null', + if (!enumType) { + continue; + } + + const enumSchema = parseOneType({ + context, + schema: { + description: schema['x-enum-descriptions']?.[index], + title: + schema['x-enum-varnames']?.[index] ?? schema['x-enumNames']?.[index], + // cast enum to string temporarily + type: enumType === 'null' ? 'string' : enumType, + }, }); + + enumSchema.const = enumValue; + + // cast enum back + if (enumType === 'null') { + enumSchema.type = enumType; + } + + schemaItems.push(enumSchema); } irSchema = addItemsToSchema({ diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts index d44311f7d..cdc124915 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts @@ -443,27 +443,23 @@ const parseEnum = ({ irSchema.type = 'enum'; const schemaItems: Array = []; + const schemaTypes = getSchemaTypes({ schema }); for (const [index, enumValue] of schema.enum.entries()) { const typeOfEnumValue = typeof enumValue; + let enumType: SchemaType | undefined; + if ( typeOfEnumValue === 'string' || typeOfEnumValue === 'number' || typeOfEnumValue === 'boolean' ) { - schemaItems.push( - parseOneType({ - context, - schema: { - const: enumValue, - description: schema['x-enum-descriptions']?.[index], - title: - schema['x-enum-varnames']?.[index] ?? - schema['x-enumNames']?.[index], - type: typeOfEnumValue, - }, - }), - ); + enumType = typeOfEnumValue; + } else if (enumValue === null) { + // type must contain null + if (schemaTypes.includes('null')) { + enumType = 'null'; + } } else { console.warn( '🚨', @@ -471,6 +467,24 @@ const parseEnum = ({ schema.enum, ); } + + if (!enumType) { + continue; + } + + schemaItems.push( + parseOneType({ + context, + schema: { + const: enumValue, + description: schema['x-enum-descriptions']?.[index], + title: + schema['x-enum-varnames']?.[index] ?? + schema['x-enumNames']?.[index], + type: enumType, + }, + }), + ); } irSchema = addItemsToSchema({ diff --git a/packages/openapi-ts/src/openApi/index.ts b/packages/openapi-ts/src/openApi/index.ts index f50418b10..fe8b41ea7 100644 --- a/packages/openapi-ts/src/openApi/index.ts +++ b/packages/openapi-ts/src/openApi/index.ts @@ -75,31 +75,26 @@ export const parseExperimental = ({ spec: spec as Record, }); + // TODO: parser - handle Swagger 2.0 + const ctx = context as IRContext; switch (ctx.spec.openapi) { - // TODO: parser - handle Swagger 2.0 case '3.0.0': case '3.0.1': case '3.0.2': case '3.0.3': case '3.0.4': parseV3_0_X(context as IRContext); - break; + return context; case '3.1.0': case '3.1.1': parseV3_1_X(context as IRContext); - break; + return context; default: // TODO: parser - uncomment after removing legacy parser. // For now, we fall back to legacy parser if spec version // is not supported // throw new Error('Unsupported OpenAPI specification'); - break; - } - - if (!Object.keys(context.ir).length) { - return; + return; } - - return context; }; diff --git a/packages/openapi-ts/test/3.0.x.spec.ts b/packages/openapi-ts/test/3.0.x.spec.ts index d3517284e..f55092f2f 100644 --- a/packages/openapi-ts/test/3.0.x.spec.ts +++ b/packages/openapi-ts/test/3.0.x.spec.ts @@ -55,6 +55,13 @@ describe(`OpenAPI ${VERSION}`, () => { }), description: 'escapes enum values', }, + { + config: createConfig({ + input: 'enum-null.json', + output: 'enum-null', + }), + description: 'handles null enums', + }, ]; it.each(scenarios)('$description', async ({ config }) => { diff --git a/packages/openapi-ts/test/3.1.x.spec.ts b/packages/openapi-ts/test/3.1.x.spec.ts index 037d7ee8a..012c285c0 100644 --- a/packages/openapi-ts/test/3.1.x.spec.ts +++ b/packages/openapi-ts/test/3.1.x.spec.ts @@ -62,6 +62,13 @@ describe(`OpenAPI ${VERSION}`, () => { }), description: 'escapes enum values', }, + { + config: createConfig({ + input: 'enum-null.json', + output: 'enum-null', + }), + description: 'handles null enums', + }, { config: createConfig({ input: 'object-properties-all-of.json', diff --git a/packages/openapi-ts/test/__snapshots__/3.0.x/enum-null/index.ts b/packages/openapi-ts/test/__snapshots__/3.0.x/enum-null/index.ts new file mode 100644 index 000000000..56bade120 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/3.0.x/enum-null/index.ts @@ -0,0 +1,2 @@ +// This file is auto-generated by @hey-api/openapi-ts +export * from './types.gen'; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/3.0.x/enum-null/types.gen.ts b/packages/openapi-ts/test/__snapshots__/3.0.x/enum-null/types.gen.ts new file mode 100644 index 000000000..1e159325e --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/3.0.x/enum-null/types.gen.ts @@ -0,0 +1,7 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type Foo = 'foo' | 'bar' | null; + +export type Bar = 'foo' | 'bar'; + +export type Baz = 'foo' | 'bar'; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/enum-null/index.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/enum-null/index.ts new file mode 100644 index 000000000..56bade120 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/3.1.x/enum-null/index.ts @@ -0,0 +1,2 @@ +// This file is auto-generated by @hey-api/openapi-ts +export * from './types.gen'; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/enum-null/types.gen.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/enum-null/types.gen.ts new file mode 100644 index 000000000..1e159325e --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/3.1.x/enum-null/types.gen.ts @@ -0,0 +1,7 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type Foo = 'foo' | 'bar' | null; + +export type Bar = 'foo' | 'bar'; + +export type Baz = 'foo' | 'bar'; \ No newline at end of file diff --git a/packages/openapi-ts/test/sample.cjs b/packages/openapi-ts/test/sample.cjs index 80fe4d0be..b5028a268 100644 --- a/packages/openapi-ts/test/sample.cjs +++ b/packages/openapi-ts/test/sample.cjs @@ -11,9 +11,9 @@ const main = async () => { // debug: true, experimentalParser: true, input: { - include: - '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$', - path: './test/spec/3.1.x/full.json', + // include: + // '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$', + path: './test/spec/3.1.x/enum-null.json', // path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', }, // name: 'foo', @@ -27,12 +27,12 @@ const main = async () => { // name: '@hey-api/schemas', // type: 'json', // }, - { - // asClass: true, - // include... - name: '@hey-api/services', - // serviceNameBuilder: '^Parameters', - }, + // { + // // asClass: true, + // // include... + // name: '@hey-api/services', + // // serviceNameBuilder: '^Parameters', + // }, // { // dates: true, // name: '@hey-api/transformers', diff --git a/packages/openapi-ts/test/spec/3.0.x/enum-null.json b/packages/openapi-ts/test/spec/3.0.x/enum-null.json new file mode 100644 index 000000000..445112288 --- /dev/null +++ b/packages/openapi-ts/test/spec/3.0.x/enum-null.json @@ -0,0 +1,25 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "OpenAPI 3.0.2 enum null example", + "version": "1" + }, + "components": { + "schemas": { + "Foo": { + "enum": ["foo", "bar", null], + "nullable": true, + "type": "string" + }, + "Bar": { + "enum": ["foo", "bar", null], + "type": "string" + }, + "Baz": { + "enum": ["foo", "bar"], + "nullable": true, + "type": "string" + } + } + } +} diff --git a/packages/openapi-ts/test/spec/3.1.x/enum-null.json b/packages/openapi-ts/test/spec/3.1.x/enum-null.json new file mode 100644 index 000000000..a75baeada --- /dev/null +++ b/packages/openapi-ts/test/spec/3.1.x/enum-null.json @@ -0,0 +1,23 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI 3.1.0 enum null example", + "version": "1" + }, + "components": { + "schemas": { + "Foo": { + "enum": ["foo", "bar", null], + "type": ["string", "null"] + }, + "Bar": { + "enum": ["foo", "bar", null], + "type": "string" + }, + "Baz": { + "enum": ["foo", "bar"], + "type": ["string", "null"] + } + } + } +}