From ad846711aa64247e77c0ef7ac1ad1adecafce31f Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 18 Dec 2023 03:58:54 +0200 Subject: [PATCH] feat: support x-enum-varnames --- e2e/kubb.config.js | 1 + e2e/schemas/enums.yaml | 39 ++++++++++++ packages/swagger-faker/src/FakerGenerator.ts | 21 ++++--- packages/swagger-ts/mocks/enums.yaml | 39 ++++++++++++ packages/swagger-ts/src/TypeGenerator.test.ts | 44 ++++++++++++++ packages/swagger-ts/src/TypeGenerator.ts | 14 ++++- .../__snapshots__/TypeGenerator.test.ts.snap | 23 +++++++ packages/swagger-zod/mocks/enums.yaml | 39 ++++++++++++ packages/swagger-zod/src/ZodGenerator.test.ts | 33 +++++++++- packages/swagger-zod/src/ZodGenerator.ts | 23 ++++--- .../__snapshots__/ZodGenerator.test.ts.snap | 60 +++++++++++++++++++ 11 files changed, 317 insertions(+), 19 deletions(-) create mode 100644 e2e/schemas/enums.yaml create mode 100644 packages/swagger-ts/mocks/enums.yaml create mode 100644 packages/swagger-zod/mocks/enums.yaml diff --git a/e2e/kubb.config.js b/e2e/kubb.config.js index 40a4576ae..0ade3cb71 100644 --- a/e2e/kubb.config.js +++ b/e2e/kubb.config.js @@ -15,6 +15,7 @@ const schemas = [ ['requestBody', './schemas/requestBody.yaml'], ['box', './schemas/box.json'], ['digitalocean', './schemas/digitalocean.yaml'], + ['enums', './schemas/enums.yaml'] ] /** @type {import('@kubb/core').KubbUserConfig} */ diff --git a/e2e/schemas/enums.yaml b/e2e/schemas/enums.yaml new file mode 100644 index 000000000..c3cb89d0a --- /dev/null +++ b/e2e/schemas/enums.yaml @@ -0,0 +1,39 @@ +definitions: + enumVarNames.Type: + enum: + - 0 + - 1 + type: integer + x-enum-varnames: + - Pending + - Received + enumNames.Type: + enum: + - 0 + - 1 + x-enumNames: + - Pending + - Received +swagger: "2.0" +info: + title: test + version: "1.0.0" +paths: + /var-names: + get: + consumes: + - application/json + responses: + 200: + description: Success + schema: + $ref: '#/definitions/enumVarNames.Type' + /names: + get: + consumes: + - application/json + responses: + 200: + description: Success + schema: + $ref: '#/definitions/enumNames.Type' \ No newline at end of file diff --git a/packages/swagger-faker/src/FakerGenerator.ts b/packages/swagger-faker/src/FakerGenerator.ts index e1655e2b5..2bf83b489 100644 --- a/packages/swagger-faker/src/FakerGenerator.ts +++ b/packages/swagger-faker/src/FakerGenerator.ts @@ -243,15 +243,22 @@ export class FakerGenerator extends Generator `\`${value}\``).join(', ')}]`], - }, - ] + const extensionEnums = (['x-enumNames', 'x-enum-varnames'] as Array) + .filter(extensionKey => extensionKey in schema) + .map((extensionKey) => { + return [ + { + keyword: fakerKeywords.enum, + args: [`[${[...new Set(schema[extensionKey] as string[])].map((value) => `\`${value}\``).join(', ')}]`], + }, + ] + }) + + if (extensionEnums.length > 0 && extensionEnums[0]) { + return extensionEnums[0] } + if (schema.type === 'number' || schema.type === 'integer') { return [ { diff --git a/packages/swagger-ts/mocks/enums.yaml b/packages/swagger-ts/mocks/enums.yaml new file mode 100644 index 000000000..c3cb89d0a --- /dev/null +++ b/packages/swagger-ts/mocks/enums.yaml @@ -0,0 +1,39 @@ +definitions: + enumVarNames.Type: + enum: + - 0 + - 1 + type: integer + x-enum-varnames: + - Pending + - Received + enumNames.Type: + enum: + - 0 + - 1 + x-enumNames: + - Pending + - Received +swagger: "2.0" +info: + title: test + version: "1.0.0" +paths: + /var-names: + get: + consumes: + - application/json + responses: + 200: + description: Success + schema: + $ref: '#/definitions/enumVarNames.Type' + /names: + get: + consumes: + - application/json + responses: + 200: + description: Success + schema: + $ref: '#/definitions/enumNames.Type' \ No newline at end of file diff --git a/packages/swagger-ts/src/TypeGenerator.test.ts b/packages/swagger-ts/src/TypeGenerator.test.ts index fc9fbe50e..690fb7784 100644 --- a/packages/swagger-ts/src/TypeGenerator.test.ts +++ b/packages/swagger-ts/src/TypeGenerator.test.ts @@ -314,3 +314,47 @@ describe('TypeGenerator with discriminators', async () => { expect(ast_output).toMatchSnapshot() }) }) + + +describe('TypeGenerator enums', async () => { + const schemaPath = path.resolve(__dirname, '../mocks/enums.yaml') + const oas = await new OasManager().parse(schemaPath) + const generator = new TypeGenerator({ + usedEnumNames: {}, + enumType: 'asConst', + dateType: 'string', + optionalType: 'questionToken', + transformers: {}, + oasType: false, + }, { + oas, + pluginManager: mockedPluginManager, + }) + + const schemas = oas.getDefinition().components?.schemas + + + test('generate x-enum-varnames types', async () => { + + + const node = generator.build({ schema: schemas?.['enumVarNames.Type'] as OasTypes.SchemaObject, baseName: 'enumVarNames' }) + + const output = print(node, undefined) + + expect(output).toBeDefined() + + expect(await format(output)).toMatchSnapshot() + }) + + test('generate x-enumNames types', async () => { + + + const node = generator.build({ schema: schemas?.['enumNames.Type'] as OasTypes.SchemaObject, baseName: 'enumNames' }) + + const output = print(node, undefined) + + expect(output).toBeDefined() + + expect(await format(output)).toMatchSnapshot() + }) +}) \ No newline at end of file diff --git a/packages/swagger-ts/src/TypeGenerator.ts b/packages/swagger-ts/src/TypeGenerator.ts index 00c2714c2..907748afd 100644 --- a/packages/swagger-ts/src/TypeGenerator.ts +++ b/packages/swagger-ts/src/TypeGenerator.ts @@ -280,12 +280,20 @@ export class TypeGenerator extends Generator [key, key]) - if ('x-enumNames' in schema) { - enums = [...new Set(schema['x-enumNames'] as string[])].map((key: string, index) => { - return [key, schema.enum?.[index] as string] + const extensionEnums : Array = (['x-enumNames', 'x-enum-varnames'] as Array) + .filter(extensionKey => extensionKey in schema) + .map((extensionKey) => { + return [...new Set(schema[extensionKey] as string[])].map((key, index) => { + return [key, schema.enum?.[index] as string] as const + }) }) + + if (extensionEnums.length > 0 && extensionEnums[0]) { + enums = extensionEnums[0] } + + this.extraNodes.push( ...factory.createEnumDeclaration({ name: transformers.camelCase(enumName), diff --git a/packages/swagger-ts/src/__snapshots__/TypeGenerator.test.ts.snap b/packages/swagger-ts/src/__snapshots__/TypeGenerator.test.ts.snap index 39be3554e..e17316bf5 100644 --- a/packages/swagger-ts/src/__snapshots__/TypeGenerator.test.ts.snap +++ b/packages/swagger-ts/src/__snapshots__/TypeGenerator.test.ts.snap @@ -1,5 +1,28 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`TypeGenerator enums > generate x-enum-varnames types 1`] = ` +"export const enumVarNames = { + Pending: 0, + Received: 1, +} as const +export type enumVarNames = (typeof enumVarNames)[keyof typeof enumVarNames] +" +`; + +exports[`TypeGenerator enums > generate x-enumNames types 1`] = ` +"export const enumVarNames = { + Pending: 0, + Received: 1, +} as const +export type enumVarNames = (typeof enumVarNames)[keyof typeof enumVarNames] +export const enumNames = { + Pending: 0, + Received: 1, +} as const +export type enumNames = (typeof enumNames)[keyof typeof enumNames] +" +`; + exports[`TypeGenerator simple > generate type for Pet with optionalType \`questionToken\` 1`] = ` "export type Pet = { /** diff --git a/packages/swagger-zod/mocks/enums.yaml b/packages/swagger-zod/mocks/enums.yaml new file mode 100644 index 000000000..c3cb89d0a --- /dev/null +++ b/packages/swagger-zod/mocks/enums.yaml @@ -0,0 +1,39 @@ +definitions: + enumVarNames.Type: + enum: + - 0 + - 1 + type: integer + x-enum-varnames: + - Pending + - Received + enumNames.Type: + enum: + - 0 + - 1 + x-enumNames: + - Pending + - Received +swagger: "2.0" +info: + title: test + version: "1.0.0" +paths: + /var-names: + get: + consumes: + - application/json + responses: + 200: + description: Success + schema: + $ref: '#/definitions/enumVarNames.Type' + /names: + get: + consumes: + - application/json + responses: + 200: + description: Success + schema: + $ref: '#/definitions/enumNames.Type' \ No newline at end of file diff --git a/packages/swagger-zod/src/ZodGenerator.test.ts b/packages/swagger-zod/src/ZodGenerator.test.ts index e3e7915bb..09ec8eaef 100644 --- a/packages/swagger-zod/src/ZodGenerator.test.ts +++ b/packages/swagger-zod/src/ZodGenerator.test.ts @@ -31,7 +31,7 @@ describe('ZodGenerator simple', async () => { }) }) -describe('TypeGenerator with const', async () => { +describe('ZodGenerator with const', async () => { const discriminatorPath = path.resolve(__dirname, '../mocks/constCases.yaml') const oas = await new OasManager().parse(discriminatorPath) const generator = new ZodGenerator({ @@ -124,3 +124,34 @@ describe('ZodGenerator lazy import', async () => { expect(node).toMatchSnapshot() }) }) + + + +describe('ZodGenerator enums', async () => { + const schemaPath = path.resolve(__dirname, '../mocks/enums.yaml') + const oas = await new OasManager().parse(schemaPath) + const generator = new ZodGenerator({ + exclude: undefined, + include: undefined, + override: undefined, + transformers: {}, + }, { + oas, + pluginManager: mockedPluginManager, + }) + + const schemas = oas.getDefinition().components?.schemas + + + test('generate x-enum-varnames types', async () => { + const node = generator.build({ schema: schemas?.['enumVarNames.Type'] as OasTypes.SchemaObject, baseName: 'enumVarNames' }) + + expect(node).toMatchSnapshot() + }) + + test('generate x-enumNames types', async () => { + const node = generator.build({ schema: schemas?.['enumNames.Type'] as OasTypes.SchemaObject, baseName: 'enumNames' }) + + expect(node).toMatchSnapshot() + }) +}) \ No newline at end of file diff --git a/packages/swagger-zod/src/ZodGenerator.ts b/packages/swagger-zod/src/ZodGenerator.ts index 1a628bbf4..f1c1075d2 100644 --- a/packages/swagger-zod/src/ZodGenerator.ts +++ b/packages/swagger-zod/src/ZodGenerator.ts @@ -249,16 +249,23 @@ export class ZodGenerator extends Generator `\`${value}\``), - }, - ...baseItems, - ] + const extensionEnums = (['x-enumNames', 'x-enum-varnames'] as Array) + .filter(extensionKey => extensionKey in schema) + .map((extensionKey) => { + return [ + { + keyword: zodKeywords.enum, + args: [...new Set(schema[extensionKey] as string[])].map((value: string) => `\`${value}\``), + }, + ...baseItems, + ] + }) + + if (extensionEnums.length > 0 && extensionEnums[0]) { + return extensionEnums[0] } + if (schema.type === 'number' || schema.type === 'integer') { // we cannot use z.enum when enum type is number/integer return [ diff --git a/packages/swagger-zod/src/__snapshots__/ZodGenerator.test.ts.snap b/packages/swagger-zod/src/__snapshots__/ZodGenerator.test.ts.snap index 36cf1f368..60759d3dd 100644 --- a/packages/swagger-zod/src/__snapshots__/ZodGenerator.test.ts.snap +++ b/packages/swagger-zod/src/__snapshots__/ZodGenerator.test.ts.snap @@ -48,6 +48,18 @@ exports[`TypeGenerator with const > UuidSchema generates a string with uuid form ] `; +exports[`ZodGenerator enums > generate x-enum-varnames types 1`] = ` +[ + "export const enumVarNames = z.enum([\`Pending\`,\`Received\`]);", +] +`; + +exports[`ZodGenerator enums > generate x-enumNames types 1`] = ` +[ + "export const enumNames = z.enum([\`Pending\`,\`Received\`]);", +] +`; + exports[`ZodGenerator lazy import > generate schema for Example 1`] = ` [ "export const Example = z.object({"nestedExamples": z.lazy(() => Example).optional()});", @@ -59,3 +71,51 @@ exports[`ZodGenerator simple > generate schema for Pet 1`] = ` "export const Pet = z.object({"id": z.number(),"name": z.string(),"tag": z.string().optional()});", ] `; + +exports[`ZodGenerator with const > MixedValueTypeConst generates zod literal value correctly, overriding the type constraint 1`] = ` +[ + "export const MixedValueTypeConst = z.object({"foobar": z.literal("foobar")});", +] +`; + +exports[`ZodGenerator with const > NullConst zodifies correctly 1`] = ` +[ + "export const NullConst = z.literal(z.null());", +] +`; + +exports[`ZodGenerator with const > NullableString zodifies correctly 1`] = ` +[ + "export const NullableString = z.string().nullable();", +] +`; + +exports[`ZodGenerator with const > NullableStringUuid zodifies correctly to a uuid or null 1`] = ` +[ + "export const NullableStringUuid = z.string().uuid().nullable();", +] +`; + +exports[`ZodGenerator with const > NullableStringWithAnyOf results in union of string and null 1`] = ` +[ + "export const NullableStringWithAnyOf = z.union([z.string(),z.null()]);", +] +`; + +exports[`ZodGenerator with const > NumberValueConst correctly generates zod literal 1`] = ` +[ + "export const NumberValueConst = z.object({"foobar": z.literal(42)});", +] +`; + +exports[`ZodGenerator with const > StringValueConst correctly generates zod literal 1`] = ` +[ + "export const StringValueConst = z.object({"foobar": z.literal("foobar")});", +] +`; + +exports[`ZodGenerator with const > UuidSchema generates a string with uuid format constraint 1`] = ` +[ + "export const UuidSchema = z.string().uuid();", +] +`;