From 2bd8fa60eb8e1f253d63332d007b6b9e9516a269 Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Wed, 21 Sep 2022 11:26:09 +0200 Subject: [PATCH 1/3] x-extensible-enum generate real extensible type definition --- __mocks__/api.yaml | 2 +- .../__tests__/test-api-v3/definitions.test.ts | 60 ++++++++++---- .../__tests__/test-api/definitions.test.ts | 73 +++++++++++------ .../__snapshots__/index.test.ts.snap | 80 ++++++++++++++++++- .../gen-api-models/__tests__/index.test.ts | 16 ++++ .../gen-api-models/__tests__/parse.test.ts | 12 +-- src/commands/gen-api-models/parse.v2.ts | 12 +-- src/commands/gen-api-models/parse.v3.ts | 12 +-- src/commands/gen-api-models/types.ts | 2 + templates/macros.njk | 13 +-- 10 files changed, 220 insertions(+), 62 deletions(-) diff --git a/__mocks__/api.yaml b/__mocks__/api.yaml index dfeba0ed..7e2a6131 100644 --- a/__mocks__/api.yaml +++ b/__mocks__/api.yaml @@ -454,7 +454,7 @@ definitions: type: object properties: description: - type: string + type: string enabled: type: boolean enum: diff --git a/e2e/src/__tests__/test-api-v3/definitions.test.ts b/e2e/src/__tests__/test-api-v3/definitions.test.ts index 81f1c8fb..fc2a83f4 100644 --- a/e2e/src/__tests__/test-api-v3/definitions.test.ts +++ b/e2e/src/__tests__/test-api-v3/definitions.test.ts @@ -19,10 +19,15 @@ import { DisjointUnionsUserTest } from "../../generated/testapiV3/DisjointUnions import { EnabledUserTest } from "../../generated/testapiV3/EnabledUserTest"; import { EnumFalseTest } from "../../generated/testapiV3/EnumFalseTest"; import { EnumTrueTest } from "../../generated/testapiV3/EnumTrueTest"; -import {AllOfWithOneElementTest} from "../../generated/testapiV3/AllOfWithOneElementTest"; -import {AllOfWithOneRefElementTest} from "../../generated/testapiV3/AllOfWithOneRefElementTest"; +import { AllOfWithOneElementTest } from "../../generated/testapiV3/AllOfWithOneElementTest"; +import { AllOfWithOneRefElementTest } from "../../generated/testapiV3/AllOfWithOneRefElementTest"; import * as E from "fp-ts/lib/Either"; +import { + PreferredLanguage, + PreferredLanguageEnum +} from "../../generated/testapi/PreferredLanguage"; +import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; const { generatedFilesDir, isSpecEnabled } = config.specs.testapiV3; @@ -324,24 +329,48 @@ describe("EnumFalseTest definition", () => { }); }); -describe("AllOfWithOneElementTest definition", () => { +describe("ExtensiblePreferredLanguage definition", () => { + const l1Ok = PreferredLanguageEnum.de_DE; + const l2Extensible = "NON_EMPY" as NonEmptyString; + const l3Ko = 10; + const l4Ko = ""; + + it("should decode extensible enum with definied enum value", () => { + const result = PreferredLanguage.decode(l1Ok); + expect(E.isRight(result)).toBe(true); + }); + + it("should decode extensible enum with string value", () => { + const result = PreferredLanguage.decode(l2Extensible); + expect(E.isRight(result)).toBe(true); + }); + + it("should fail decode extensible enum with invalid value", () => { + const result = PreferredLanguage.decode(l3Ko); + expect(E.isLeft(result)).toBe(true); + }); + + it("should fail decode extensible enum with empty string", () => { + const result = PreferredLanguage.decode(l4Ko); + expect(E.isLeft(result)).toBe(true); + }); +}); - const okElement = {key: "string"}; - const notOkElement = {key: 1}; +describe("AllOfWithOneElementTest definition", () => { + const okElement = { key: "string" }; + const notOkElement = { key: 1 }; it("Should return a right", () => { expect(E.isRight(AllOfWithOneElementTest.decode(okElement))).toBeTruthy(); - }) + }); it("Should return a left", () => { expect(E.isLeft(AllOfWithOneElementTest.decode(notOkElement))).toBeTruthy(); - }) - -}) + }); +}); describe("AllOfWithOneRefElementTest", () => { - -const basicProfile = { + const basicProfile = { family_name: "Rossi", fiscal_code: "RSSMRA80A01F205X", has_profile: true, @@ -351,10 +380,11 @@ const basicProfile = { }; it("Should return a right", () => { - expect(E.isRight(AllOfWithOneRefElementTest.decode(basicProfile))).toBeTruthy(); - }) - -}) + expect( + E.isRight(AllOfWithOneRefElementTest.decode(basicProfile)) + ).toBeTruthy(); + }); +}); describe("DisjointUnionsUserTest definition", () => { const enabledUser = { diff --git a/e2e/src/__tests__/test-api/definitions.test.ts b/e2e/src/__tests__/test-api/definitions.test.ts index 5a54354a..45ee3fa1 100644 --- a/e2e/src/__tests__/test-api/definitions.test.ts +++ b/e2e/src/__tests__/test-api/definitions.test.ts @@ -23,8 +23,12 @@ import { AllOfWithOneElementTest } from "../../generated/testapi/AllOfWithOneEle import { AllOfWithOneRefElementTest } from "../../generated/testapi/AllOfWithOneRefElementTest"; import { AdditionalPropsTest } from "../../generated/testapi/AdditionalPropsTest"; - -import * as E from "fp-ts/lib/Either" +import * as E from "fp-ts/lib/Either"; +import { + PreferredLanguage, + PreferredLanguageEnum +} from "../../generated/testapi/PreferredLanguage"; +import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; const { generatedFilesDir } = config.specs.testapi; @@ -326,43 +330,65 @@ describe("EnumFalseTest definition", () => { }); }); -describe("AllOfWithOneElementTest definition", () => { +describe("ExtensiblePreferredLanguage definition", () => { + const l1Ok = PreferredLanguageEnum.de_DE; + const l2Extensible = "NON_EMPY" as NonEmptyString; + const l3Ko = 10; + const l4Ko = ""; + + it("should decode extensible enum with definied enum value", () => { + const result = PreferredLanguage.decode(l1Ok); + expect(E.isRight(result)).toBe(true); + }); + + it("should decode extensible enum with string value", () => { + const result = PreferredLanguage.decode(l2Extensible); + expect(E.isRight(result)).toBe(true); + }); - const okElement = {key: "string"}; - const notOkElement = {key: 1}; + it("should fail decode extensible enum with invalid value", () => { + const result = PreferredLanguage.decode(l3Ko); + expect(E.isLeft(result)).toBe(true); + }); + + it("should fail decode extensible enum with empty string", () => { + const result = PreferredLanguage.decode(l4Ko); + expect(E.isLeft(result)).toBe(true); + }); +}); + +describe("AllOfWithOneElementTest definition", () => { + const okElement = { key: "string" }; + const notOkElement = { key: 1 }; it("Should return a right", () => { expect(E.isRight(AllOfWithOneElementTest.decode(okElement))).toBeTruthy(); - }) + }); it("Should return a left", () => { expect(E.isLeft(AllOfWithOneElementTest.decode(notOkElement))).toBeTruthy(); - }) - -}) + }); +}); describe("AdditionalPropsTest should be an object with a string as key and an array of number as value", () => { - - const okElement = {"okElementProperty": [1, 2, 3]}; - const notOkElement = {"notOkElementProperty": ["1", "2", "3"]}; + const okElement = { okElementProperty: [1, 2, 3] }; + const notOkElement = { notOkElementProperty: ["1", "2", "3"] }; it("Should return a right with a valid type", () => { expect(E.isRight(AdditionalPropsTest.decode(okElement))).toBeTruthy(); - }) + }); it("Should return a left with a non valid element", () => { expect(E.isLeft(AdditionalPropsTest.decode(notOkElement))).toBeTruthy(); - }) + }); it("Should return a left with undefined input", () => { expect(E.isLeft(AdditionalPropsTest.decode(undefined))).toBeTruthy(); - }) - -}) + }); +}); describe("AllOfWithOneRefElementTest", () => { - -const basicProfile = { + const basicProfile = { family_name: "Rossi", fiscal_code: "RSSMRA80A01F205X", has_profile: true, @@ -372,10 +398,11 @@ const basicProfile = { }; it("Should return a right", () => { - expect(E.isRight(AllOfWithOneRefElementTest.decode(basicProfile))).toBeTruthy(); - }) - -}) + expect( + E.isRight(AllOfWithOneRefElementTest.decode(basicProfile)) + ).toBeTruthy(); + }); +}); describe("DisjointUnionsUserTest definition", () => { const enabledUser = { diff --git a/src/commands/gen-api-models/__tests__/__snapshots__/index.test.ts.snap b/src/commands/gen-api-models/__tests__/__snapshots__/index.test.ts.snap index b071a6e7..b7d97a72 100644 --- a/src/commands/gen-api-models/__tests__/__snapshots__/index.test.ts.snap +++ b/src/commands/gen-api-models/__tests__/__snapshots__/index.test.ts.snap @@ -573,6 +573,40 @@ export type EnumTest = t.TypeOf; " `; +exports[`Openapi V2 |> gen-api-models should handle extensible enums: extesible-enum 1`] = ` +"/** + * Do not edit this file it is auto-generated by io-utils / gen-api-models. + * See https://github.com/pagopa/io-utils + */ +/* eslint-disable */ + +import { enumType } from \\"@pagopa/ts-commons/lib/types\\"; +import { NonEmptyString } from \\"@pagopa/ts-commons/lib/strings\\"; +import * as t from \\"io-ts\\"; + +export enum PreferredLanguageEnum { + \\"it_IT\\" = \\"it_IT\\", + + \\"en_GB\\" = \\"en_GB\\", + + \\"es_ES\\" = \\"es_ES\\", + + \\"de_DE\\" = \\"de_DE\\", + + \\"fr_FR\\" = \\"fr_FR\\" +} + +export type PreferredLanguage = t.TypeOf; +export const PreferredLanguage = t.union( + [ + enumType(PreferredLanguageEnum, \\"PreferredLanguage\\"), + NonEmptyString + ], + \\"ExtensiblePreferredLanguage\\" +); +" +`; + exports[`Openapi V2 |> gen-api-models should handle list of defintions 1`] = ` "/** * Do not edit this file it is auto-generated by io-utils / gen-api-models. @@ -640,6 +674,7 @@ exports[`Openapi V2 |> gen-api-models should not contain a t.string but an enum: import * as t from \\"io-ts\\"; import { Profile } from \\"./Profile\\"; import { enumType } from \\"@pagopa/ts-commons/lib/types\\"; +import { NonEmptyString } from \\"@pagopa/ts-commons/lib/strings\\"; export enum StatusEnum { \\"ACTIVATED\\" = \\"ACTIVATED\\" @@ -651,7 +686,10 @@ export enum StatusEnum { // required attributes const AllOfWithXExtensibleEnum2R = t.interface({ - status: enumType(StatusEnum, \\"status\\") + status: t.union( + [enumType(StatusEnum, \\"status\\"), NonEmptyString], + \\"Extensiblestatus\\" + ) }); // optional attributes @@ -2295,6 +2333,40 @@ export type EnumTest = t.TypeOf; " `; +exports[`Openapi V3 |> gen-api-models should handle extensible enums: extesible-enum 1`] = ` +"/** + * Do not edit this file it is auto-generated by io-utils / gen-api-models. + * See https://github.com/pagopa/io-utils + */ +/* eslint-disable */ + +import { enumType } from \\"@pagopa/ts-commons/lib/types\\"; +import { NonEmptyString } from \\"@pagopa/ts-commons/lib/strings\\"; +import * as t from \\"io-ts\\"; + +export enum PreferredLanguageEnum { + \\"it_IT\\" = \\"it_IT\\", + + \\"en_GB\\" = \\"en_GB\\", + + \\"es_ES\\" = \\"es_ES\\", + + \\"de_DE\\" = \\"de_DE\\", + + \\"fr_FR\\" = \\"fr_FR\\" +} + +export type PreferredLanguage = t.TypeOf; +export const PreferredLanguage = t.union( + [ + enumType(PreferredLanguageEnum, \\"PreferredLanguage\\"), + NonEmptyString + ], + \\"ExtensiblePreferredLanguage\\" +); +" +`; + exports[`Openapi V3 |> gen-api-models should handle list of defintions 1`] = ` "/** * Do not edit this file it is auto-generated by io-utils / gen-api-models. @@ -2362,6 +2434,7 @@ exports[`Openapi V3 |> gen-api-models should not contain a t.string but an enum: import * as t from \\"io-ts\\"; import { Profile } from \\"./Profile\\"; import { enumType } from \\"@pagopa/ts-commons/lib/types\\"; +import { NonEmptyString } from \\"@pagopa/ts-commons/lib/strings\\"; export enum StatusEnum { \\"ACTIVATED\\" = \\"ACTIVATED\\" @@ -2373,7 +2446,10 @@ export enum StatusEnum { // required attributes const AllOfWithXExtensibleEnum2R = t.interface({ - status: enumType(StatusEnum, \\"status\\") + status: t.union( + [enumType(StatusEnum, \\"status\\"), NonEmptyString], + \\"Extensiblestatus\\" + ) }); // optional attributes diff --git a/src/commands/gen-api-models/__tests__/index.test.ts b/src/commands/gen-api-models/__tests__/index.test.ts index e8c04643..0df9ee30 100644 --- a/src/commands/gen-api-models/__tests__/index.test.ts +++ b/src/commands/gen-api-models/__tests__/index.test.ts @@ -194,6 +194,22 @@ describe.each` expect(code).toMatchSnapshot("enum-simple"); }); + it("should handle extensible enums", async () => { + const definitonName = "PreferredLanguage"; + const definition = getDefinitionOrFail(spec, definitonName); + + const code = await renderDefinitionCode( + definitonName, + getParser(spec).parseDefinition( + // @ts-ignore + definition + ), + false + ); + + expect(code).toMatchSnapshot("extesible-enum"); + }); + it("should generate a record from additionalProperties", async () => { const definitonName = "AdditionalPropsTest"; const definition = getDefinitionOrFail(spec, definitonName); diff --git a/src/commands/gen-api-models/__tests__/parse.test.ts b/src/commands/gen-api-models/__tests__/parse.test.ts index 7714e01b..0667aeb7 100644 --- a/src/commands/gen-api-models/__tests__/parse.test.ts +++ b/src/commands/gen-api-models/__tests__/parse.test.ts @@ -132,11 +132,11 @@ describe.each` expect(parsed).toEqual( expect.objectContaining({ - method: 'get', - operationId: 'testBinaryFileDownload', - responses: [ { e1: '200', e2: 'Buffer', e3: [] } ], - path: '/test-binary-file-download', - consumes: undefined, + method: "get", + operationId: "testBinaryFileDownload", + responses: [{ e1: "200", e2: "Buffer", e3: [] }], + path: "/test-binary-file-download", + consumes: undefined }) ); }); @@ -297,7 +297,7 @@ describe.each` definition ); - expect(parsed.enum).toEqual(expect.any(Array)); + expect(parsed["x-extensible-enum"]).toEqual(expect.any(Array)); }); it("should handle AnObjectWithAnItemsField", async () => { diff --git a/src/commands/gen-api-models/parse.v2.ts b/src/commands/gen-api-models/parse.v2.ts index c59315ef..649c698b 100644 --- a/src/commands/gen-api-models/parse.v2.ts +++ b/src/commands/gen-api-models/parse.v2.ts @@ -53,11 +53,12 @@ function parseInnerDefinition(source: IJsonSchema): IDefinition { }; // enum used to be defined with "x-extensible-enum" custom field - const enumm = looselySource.enum - ? looselySource.enum - : "x-extensible-enum" in looselySource - ? looselySource["x-extensible-enum"] - : undefined; + const enumm = looselySource.enum ? looselySource.enum : undefined; + + const extesibleEnumm = + "x-extensible-enum" in looselySource + ? looselySource["x-extensible-enum"] + : undefined; const format = "format" in looselySource ? looselySource.format : undefined; @@ -95,6 +96,7 @@ function parseInnerDefinition(source: IJsonSchema): IDefinition { required: source.required, title: source.title, type: source.type, + ["x-extensible-enum"]: extesibleEnumm, // eslint-disable-next-line @typescript-eslint/no-explicit-any ["x-import"]: (source as any)["x-import"] }; diff --git a/src/commands/gen-api-models/parse.v3.ts b/src/commands/gen-api-models/parse.v3.ts index 82c2ef4c..fe460113 100644 --- a/src/commands/gen-api-models/parse.v3.ts +++ b/src/commands/gen-api-models/parse.v3.ts @@ -54,11 +54,12 @@ function parseInnerDefinition(source: IJsonSchema): IDefinition { }; // enum used to be defined with "x-extensible-enum" custom field - const enumm = looselySource.enum - ? looselySource.enum - : "x-extensible-enum" in looselySource - ? looselySource["x-extensible-enum"] - : undefined; + const enumm = looselySource.enum ? looselySource.enum : undefined; + + const extesibleEnumm = + "x-extensible-enum" in looselySource + ? looselySource["x-extensible-enum"] + : undefined; const format = "format" in looselySource ? looselySource.format : undefined; @@ -104,6 +105,7 @@ function parseInnerDefinition(source: IJsonSchema): IDefinition { required: source.required, title: source.title, type: source.type, + ["x-extensible-enum"]: extesibleEnumm, // eslint-disable-next-line @typescript-eslint/no-explicit-any ["x-import"]: (source as any)["x-import"] }; diff --git a/src/commands/gen-api-models/types.ts b/src/commands/gen-api-models/types.ts index a8080545..d5f09b99 100644 --- a/src/commands/gen-api-models/types.ts +++ b/src/commands/gen-api-models/types.ts @@ -103,6 +103,8 @@ export interface IDefinition { readonly required?: ReadonlyArray; // eslint-disable-next-line @typescript-eslint/no-explicit-any readonly enum?: ReadonlyArray; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly ["x-extensible-enum"]?: ReadonlyArray; readonly properties?: { readonly [name: string]: ReferenceObject | IDefinition; }; diff --git a/templates/macros.njk b/templates/macros.njk index 09bfa875..2b53faea 100644 --- a/templates/macros.njk +++ b/templates/macros.njk @@ -200,7 +200,8 @@ "{{ definitionName }}" ){% endset %} {% elif definition["x-extensible-enum"] %} - {{- 'import { enumType } from "@pagopa/ts-commons/lib/types";' | addImport -}} + {{- 'import { enumType } from "@pagopa/ts-commons/lib/types"; + import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";' | addImport -}} {% set enumTypeAlias %} export enum {{ definitionName | capitalizeFirst }}Enum { {% for enum in definition["x-extensible-enum"] %} @@ -209,10 +210,12 @@ } {% endset %} {{- enumTypeAlias | addTypeAlias -}} - {% set typedef %}enumType<{{ definitionName | capitalizeFirst }}Enum>( - {{ definitionName | capitalizeFirst }}Enum, - "{{ definitionName }}" - ){% endset %} + {% set typedef %}t.union([ + enumType<{{ definitionName | capitalizeFirst }}Enum>( + {{ definitionName | capitalizeFirst }}Enum, + "{{ definitionName }}" + ), + NonEmptyString], "Extensible{{ definitionName }}"){% endset %} {% else %} {% set typedef %}t.string{% endset %} {% endif %} From 5d87681c1744a7ab50582df347b9c78dbb938b1c Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Fri, 23 Sep 2022 10:00:08 +0200 Subject: [PATCH 2/3] Generate not extensible enum type on strict generation --- .../__snapshots__/index.test.ts.snap | 158 ++++++++++++++++++ .../gen-api-models/__tests__/index.test.ts | 34 ++++ templates/macros.njk | 28 ++-- 3 files changed, 208 insertions(+), 12 deletions(-) diff --git a/src/commands/gen-api-models/__tests__/__snapshots__/index.test.ts.snap b/src/commands/gen-api-models/__tests__/__snapshots__/index.test.ts.snap index b7d97a72..835a8c94 100644 --- a/src/commands/gen-api-models/__tests__/__snapshots__/index.test.ts.snap +++ b/src/commands/gen-api-models/__tests__/__snapshots__/index.test.ts.snap @@ -573,6 +573,36 @@ export type EnumTest = t.TypeOf; " `; +exports[`Openapi V2 |> gen-api-models should handle extensible enums with strict option: strict-extesible-enum 1`] = ` +"/** + * Do not edit this file it is auto-generated by io-utils / gen-api-models. + * See https://github.com/pagopa/io-utils + */ +/* eslint-disable */ + +import { enumType } from \\"@pagopa/ts-commons/lib/types\\"; +import * as t from \\"io-ts\\"; + +export enum PreferredLanguageEnum { + \\"it_IT\\" = \\"it_IT\\", + + \\"en_GB\\" = \\"en_GB\\", + + \\"es_ES\\" = \\"es_ES\\", + + \\"de_DE\\" = \\"de_DE\\", + + \\"fr_FR\\" = \\"fr_FR\\" +} + +export type PreferredLanguage = t.TypeOf; +export const PreferredLanguage = enumType( + PreferredLanguageEnum, + \\"PreferredLanguage\\" +); +" +`; + exports[`Openapi V2 |> gen-api-models should handle extensible enums: extesible-enum 1`] = ` "/** * Do not edit this file it is auto-generated by io-utils / gen-api-models. @@ -664,6 +694,55 @@ export const OrganizationFiscalCodeTest = OrganizationFiscalCodeT; " `; +exports[`Openapi V2 |> gen-api-models should not contain a t.string but an enum with strict option: all-of-test 1`] = ` +"/** + * Do not edit this file it is auto-generated by io-utils / gen-api-models. + * See https://github.com/pagopa/io-utils + */ +/* eslint-disable */ + +import * as t from \\"io-ts\\"; +import { Profile } from \\"./Profile\\"; +import { enumType } from \\"@pagopa/ts-commons/lib/types\\"; + +export enum StatusEnum { + \\"ACTIVATED\\" = \\"ACTIVATED\\" +} + +/** + * test if allOf with x-extensible-enum works fine + */ + +// required attributes +const AllOfWithXExtensibleEnum2R = t.interface({ + status: enumType(StatusEnum, \\"status\\") +}); + +// optional attributes +const AllOfWithXExtensibleEnum2O = t.partial({}); + +export const AllOfWithXExtensibleEnum2 = t.exact( + t.intersection( + [AllOfWithXExtensibleEnum2R, AllOfWithXExtensibleEnum2O], + \\"AllOfWithXExtensibleEnum2\\" + ) +); + +export type AllOfWithXExtensibleEnum2 = t.TypeOf< + typeof AllOfWithXExtensibleEnum2 +>; + +export const AllOfWithXExtensibleEnum = t.intersection( + [Profile, AllOfWithXExtensibleEnum2], + \\"AllOfWithXExtensibleEnum\\" +); + +export type AllOfWithXExtensibleEnum = t.TypeOf< + typeof AllOfWithXExtensibleEnum +>; +" +`; + exports[`Openapi V2 |> gen-api-models should not contain a t.string but an enum: all-of-test 1`] = ` "/** * Do not edit this file it is auto-generated by io-utils / gen-api-models. @@ -2333,6 +2412,36 @@ export type EnumTest = t.TypeOf; " `; +exports[`Openapi V3 |> gen-api-models should handle extensible enums with strict option: strict-extesible-enum 1`] = ` +"/** + * Do not edit this file it is auto-generated by io-utils / gen-api-models. + * See https://github.com/pagopa/io-utils + */ +/* eslint-disable */ + +import { enumType } from \\"@pagopa/ts-commons/lib/types\\"; +import * as t from \\"io-ts\\"; + +export enum PreferredLanguageEnum { + \\"it_IT\\" = \\"it_IT\\", + + \\"en_GB\\" = \\"en_GB\\", + + \\"es_ES\\" = \\"es_ES\\", + + \\"de_DE\\" = \\"de_DE\\", + + \\"fr_FR\\" = \\"fr_FR\\" +} + +export type PreferredLanguage = t.TypeOf; +export const PreferredLanguage = enumType( + PreferredLanguageEnum, + \\"PreferredLanguage\\" +); +" +`; + exports[`Openapi V3 |> gen-api-models should handle extensible enums: extesible-enum 1`] = ` "/** * Do not edit this file it is auto-generated by io-utils / gen-api-models. @@ -2424,6 +2533,55 @@ export const OrganizationFiscalCodeTest = OrganizationFiscalCodeT; " `; +exports[`Openapi V3 |> gen-api-models should not contain a t.string but an enum with strict option: all-of-test 1`] = ` +"/** + * Do not edit this file it is auto-generated by io-utils / gen-api-models. + * See https://github.com/pagopa/io-utils + */ +/* eslint-disable */ + +import * as t from \\"io-ts\\"; +import { Profile } from \\"./Profile\\"; +import { enumType } from \\"@pagopa/ts-commons/lib/types\\"; + +export enum StatusEnum { + \\"ACTIVATED\\" = \\"ACTIVATED\\" +} + +/** + * test if allOf with x-extensible-enum works fine + */ + +// required attributes +const AllOfWithXExtensibleEnum2R = t.interface({ + status: enumType(StatusEnum, \\"status\\") +}); + +// optional attributes +const AllOfWithXExtensibleEnum2O = t.partial({}); + +export const AllOfWithXExtensibleEnum2 = t.exact( + t.intersection( + [AllOfWithXExtensibleEnum2R, AllOfWithXExtensibleEnum2O], + \\"AllOfWithXExtensibleEnum2\\" + ) +); + +export type AllOfWithXExtensibleEnum2 = t.TypeOf< + typeof AllOfWithXExtensibleEnum2 +>; + +export const AllOfWithXExtensibleEnum = t.intersection( + [Profile, AllOfWithXExtensibleEnum2], + \\"AllOfWithXExtensibleEnum\\" +); + +export type AllOfWithXExtensibleEnum = t.TypeOf< + typeof AllOfWithXExtensibleEnum +>; +" +`; + exports[`Openapi V3 |> gen-api-models should not contain a t.string but an enum: all-of-test 1`] = ` "/** * Do not edit this file it is auto-generated by io-utils / gen-api-models. diff --git a/src/commands/gen-api-models/__tests__/index.test.ts b/src/commands/gen-api-models/__tests__/index.test.ts index 0df9ee30..d841dd4e 100644 --- a/src/commands/gen-api-models/__tests__/index.test.ts +++ b/src/commands/gen-api-models/__tests__/index.test.ts @@ -210,6 +210,22 @@ describe.each` expect(code).toMatchSnapshot("extesible-enum"); }); + it("should handle extensible enums with strict option", async () => { + const definitonName = "PreferredLanguage"; + const definition = getDefinitionOrFail(spec, definitonName); + + const code = await renderDefinitionCode( + definitonName, + getParser(spec).parseDefinition( + // @ts-ignore + definition + ), + true + ); + + expect(code).toMatchSnapshot("strict-extesible-enum"); + }); + it("should generate a record from additionalProperties", async () => { const definitonName = "AdditionalPropsTest"; const definition = getDefinitionOrFail(spec, definitonName); @@ -334,6 +350,24 @@ describe.each` expect(code).toMatchSnapshot("all-of-test"); }); + it("should not contain a t.string but an enum with strict option", async () => { + const definitonName = "AllOfWithXExtensibleEnum"; + const definition = getDefinitionOrFail(spec, definitonName); + + const code = await renderDefinitionCode( + definitonName, + getParser(spec).parseDefinition( + // @ts-ignore + definition + ), + true + ); + + expect(code).not.toContain("t.string"); + expect(code).toContain("enumType"); + expect(code).toMatchSnapshot("all-of-test"); + }); + it("should generate a type union from oneOf", async () => { const definitonName = "OneOfTest"; const definition = getDefinitionOrFail(spec, definitonName); diff --git a/templates/macros.njk b/templates/macros.njk index 2b53faea..0da6163d 100644 --- a/templates/macros.njk +++ b/templates/macros.njk @@ -161,7 +161,7 @@ {## # defines a string property with constraints #} -{% macro defineString(definitionName, definition, inline = false) -%} +{% macro defineString(definitionName, definition, inline = false, strictInterfaces = false) -%} {% if definition.pattern %} {{- 'import { PatternString } from "@pagopa/ts-commons/lib/strings";' | addImport -}} {% set typedef %}PatternString("{{ definition.pattern | safe }}"){% endset %} @@ -200,8 +200,12 @@ "{{ definitionName }}" ){% endset %} {% elif definition["x-extensible-enum"] %} - {{- 'import { enumType } from "@pagopa/ts-commons/lib/types"; - import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";' | addImport -}} + {% if strictInterfaces %} + {{- 'import { enumType } from "@pagopa/ts-commons/lib/types";' | addImport -}} + {% else %} + {{- 'import { enumType } from "@pagopa/ts-commons/lib/types";' | addImport -}} + {{- 'import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";' | addImport -}} + {% endif %} {% set enumTypeAlias %} export enum {{ definitionName | capitalizeFirst }}Enum { {% for enum in definition["x-extensible-enum"] %} @@ -210,12 +214,12 @@ } {% endset %} {{- enumTypeAlias | addTypeAlias -}} - {% set typedef %}t.union([ + {% set typedef %}{% if not strictInterfaces %}t.union([{% endif %} enumType<{{ definitionName | capitalizeFirst }}Enum>( {{ definitionName | capitalizeFirst }}Enum, "{{ definitionName }}" - ), - NonEmptyString], "Extensible{{ definitionName }}"){% endset %} + ){% if not strictInterfaces %}, + NonEmptyString], "Extensible{{ definitionName }}"){% endif %}{% endset %} {% else %} {% set typedef %}t.string{% endset %} {% endif %} @@ -269,7 +273,7 @@ {## # defines an object property of some prop.type #} -{% macro defineObjectProperty(propName, prop, parentPropName, camelCasedPropNames) -%} +{% macro defineObjectProperty(propName, prop, parentPropName, strictInterfaces, camelCasedPropNames) -%} {% if propName %} {% if camelCasedPropNames %} "{{ propName | camelCase }}": @@ -288,7 +292,7 @@ {% elif prop.type == "integer" %} {{ defineInteger(propName, prop, true) }} {% elif prop.type == "string" %} - {{ defineString(propName, prop, true) }} + {{ defineString(propName, prop, true, strictInterfaces) }} {% elif prop.type == "boolean" %} {{ defineBoolean(propName, prop, true) }} {% else %} @@ -351,7 +355,7 @@ {% set typedef %}t.record(t.string, t.any, "{{ definitionName }}"){% endset %} {% else %} {% set typedef %}t.record(t.string, - {{ defineObjectProperty(false, definition.additionalProperties, camelCasedPropNames) }} "{{ definitionName }}") + {{ defineObjectProperty(false, definition.additionalProperties, strictInterfaces, camelCasedPropNames) }} "{{ definitionName }}") {% endset %} {%- endif %} {% set defaultValue = definition.default | dump | safe if definition.default else undefined %} @@ -361,7 +365,7 @@ const {{ definitionName }}R = t.interface({ {% for propName, prop in definition.properties -%} {% if definition.required and (definition.required | contains(propName)) %} - {{ defineObjectProperty(propName, prop, definitionName, camelCasedPropNames) }} + {{ defineObjectProperty(propName, prop, definitionName, strictInterfaces, camelCasedPropNames) }} {% endif %} {% endfor %} }); @@ -370,7 +374,7 @@ const {{ definitionName }}O = t.partial({ {% for propName, prop in definition.properties -%} {% if (not definition.required) or (definition.required and not (definition.required | contains(propName))) %} - {{ defineObjectProperty(propName, prop, definitionName, camelCasedPropNames) }} + {{ defineObjectProperty(propName, prop, definitionName, strictInterfaces, camelCasedPropNames) }} {% endif %} {% endfor %} }); @@ -455,7 +459,7 @@ {% elif definition.type == "string" %} - {{ defineString(definitionName, definition) }} + {{ defineString(definitionName, definition, false, strictInterfaces) }} {% elif definition.type == "boolean" %} From de2e1e02694e7988a071d4943ec5347a805a2be6 Mon Sep 17 00:00:00 2001 From: Daniele Manni Date: Fri, 23 Sep 2022 10:23:56 +0200 Subject: [PATCH 3/3] Fix e2e tests adding non strict type generation --- .../__tests__/test-api-v3/definitions.test.ts | 19 ++++++++++++----- .../__tests__/test-api/definitions.test.ts | 17 +++++++++++---- e2e/src/config.ts | 21 +++++++++++++++++-- e2e/src/setup.ts | 4 ++-- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/e2e/src/__tests__/test-api-v3/definitions.test.ts b/e2e/src/__tests__/test-api-v3/definitions.test.ts index fc2a83f4..357809ad 100644 --- a/e2e/src/__tests__/test-api-v3/definitions.test.ts +++ b/e2e/src/__tests__/test-api-v3/definitions.test.ts @@ -26,7 +26,8 @@ import * as E from "fp-ts/lib/Either"; import { PreferredLanguage, PreferredLanguageEnum -} from "../../generated/testapi/PreferredLanguage"; +} from "../../generated/testapiV3/PreferredLanguage"; +import { PreferredLanguage as ExtensiblePreferredLanguage } from "../../generated/testapiV3-unstrict/PreferredLanguage"; import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; const { generatedFilesDir, isSpecEnabled } = config.specs.testapiV3; @@ -336,23 +337,31 @@ describe("ExtensiblePreferredLanguage definition", () => { const l4Ko = ""; it("should decode extensible enum with definied enum value", () => { - const result = PreferredLanguage.decode(l1Ok); + const result = ExtensiblePreferredLanguage.decode(l1Ok); + const strictResult = PreferredLanguage.decode(l1Ok); expect(E.isRight(result)).toBe(true); + expect(E.isRight(strictResult)).toBe(true); }); it("should decode extensible enum with string value", () => { - const result = PreferredLanguage.decode(l2Extensible); + const result = ExtensiblePreferredLanguage.decode(l2Extensible); + const strictResult = PreferredLanguage.decode(l2Extensible); expect(E.isRight(result)).toBe(true); + expect(E.isLeft(strictResult)).toBe(true); }); it("should fail decode extensible enum with invalid value", () => { - const result = PreferredLanguage.decode(l3Ko); + const result = ExtensiblePreferredLanguage.decode(l3Ko); + const strictResult = PreferredLanguage.decode(l3Ko); expect(E.isLeft(result)).toBe(true); + expect(E.isLeft(strictResult)).toBe(true); }); it("should fail decode extensible enum with empty string", () => { - const result = PreferredLanguage.decode(l4Ko); + const result = ExtensiblePreferredLanguage.decode(l4Ko); + const strictResult = PreferredLanguage.decode(l4Ko); expect(E.isLeft(result)).toBe(true); + expect(E.isLeft(strictResult)).toBe(true); }); }); diff --git a/e2e/src/__tests__/test-api/definitions.test.ts b/e2e/src/__tests__/test-api/definitions.test.ts index 45ee3fa1..0d6f32b9 100644 --- a/e2e/src/__tests__/test-api/definitions.test.ts +++ b/e2e/src/__tests__/test-api/definitions.test.ts @@ -28,6 +28,7 @@ import { PreferredLanguage, PreferredLanguageEnum } from "../../generated/testapi/PreferredLanguage"; +import { PreferredLanguage as ExtensiblePreferredLanguage } from "../../generated/testapi-unstrict/PreferredLanguage"; import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; const { generatedFilesDir } = config.specs.testapi; @@ -337,23 +338,31 @@ describe("ExtensiblePreferredLanguage definition", () => { const l4Ko = ""; it("should decode extensible enum with definied enum value", () => { - const result = PreferredLanguage.decode(l1Ok); + const result = ExtensiblePreferredLanguage.decode(l1Ok); + const strictResult = PreferredLanguage.decode(l1Ok); expect(E.isRight(result)).toBe(true); + expect(E.isRight(strictResult)).toBe(true); }); it("should decode extensible enum with string value", () => { - const result = PreferredLanguage.decode(l2Extensible); + const result = ExtensiblePreferredLanguage.decode(l2Extensible); + const strictResult = PreferredLanguage.decode(l2Extensible); expect(E.isRight(result)).toBe(true); + expect(E.isLeft(strictResult)).toBe(true); }); it("should fail decode extensible enum with invalid value", () => { - const result = PreferredLanguage.decode(l3Ko); + const result = ExtensiblePreferredLanguage.decode(l3Ko); + const strictResult = PreferredLanguage.decode(l3Ko); expect(E.isLeft(result)).toBe(true); + expect(E.isLeft(strictResult)).toBe(true); }); it("should fail decode extensible enum with empty string", () => { - const result = PreferredLanguage.decode(l4Ko); + const result = ExtensiblePreferredLanguage.decode(l4Ko); + const strictResult = PreferredLanguage.decode(l4Ko); expect(E.isLeft(result)).toBe(true); + expect(E.isLeft(strictResult)).toBe(true); }); }); diff --git a/e2e/src/config.ts b/e2e/src/config.ts index 7497feb5..38890b07 100644 --- a/e2e/src/config.ts +++ b/e2e/src/config.ts @@ -36,18 +36,35 @@ export default { generatedFilesDir: `${GENERATED_BASE_DIR}/be`, isSpecEnabled: includeInList(process.env.INCLUDE_SPECS, "be"), mockPort: 4102, + strictInterfaces: true, url: `${ROOT_DIRECTORY_FOR_E2E}/../__mocks__/be.yaml` }, - testapi: { + strictTestapi: { generatedFilesDir: `${GENERATED_BASE_DIR}/testapi`, isSpecEnabled: includeInList(process.env.INCLUDE_SPECS, "testapi"), mockPort: 4101, + strictInterfaces: true, url: `${ROOT_DIRECTORY_FOR_E2E}/../__mocks__/api.yaml` }, - testapiV3: { + strictTestapiV3: { generatedFilesDir: `${GENERATED_BASE_DIR}/testapiV3`, isSpecEnabled: includeInList(process.env.INCLUDE_SPECS, "testapiV3"), mockPort: 4103, + strictInterfaces: true, + url: `${ROOT_DIRECTORY_FOR_E2E}/../__mocks__/openapi_v3/api.yaml` + }, + testapi: { + generatedFilesDir: `${GENERATED_BASE_DIR}/testapi-unstrict`, + isSpecEnabled: includeInList(process.env.INCLUDE_SPECS, "testapi"), + mockPort: 4104, + strictInterfaces: false, + url: `${ROOT_DIRECTORY_FOR_E2E}/../__mocks__/api.yaml` + }, + testapiV3: { + generatedFilesDir: `${GENERATED_BASE_DIR}/testapiV3-unstrict`, + isSpecEnabled: includeInList(process.env.INCLUDE_SPECS, "testapiV3"), + mockPort: 4105, + strictInterfaces: false, url: `${ROOT_DIRECTORY_FOR_E2E}/../__mocks__/openapi_v3/api.yaml` } } diff --git a/e2e/src/setup.ts b/e2e/src/setup.ts index 2fb2f0de..e964a0c4 100644 --- a/e2e/src/setup.ts +++ b/e2e/src/setup.ts @@ -48,7 +48,7 @@ export default async () => { const { specs, skipClient, skipGeneration } = config; const tasks = Object.values(specs) .filter(({ isSpecEnabled }) => isSpecEnabled) - .map(({ url, mockPort, generatedFilesDir }) => { + .map(({ url, mockPort, generatedFilesDir, strictInterfaces }) => { // eslint-disable-next-line sonarjs/prefer-immediate-return const p = pipe( skipGeneration @@ -58,7 +58,7 @@ export default async () => { definitionsDirPath: generatedFilesDir, generateClient: true, specFilePath: url, - strictInterfaces: true + strictInterfaces }), TE.chain(() => (skipClient ? noopTE : tsStartServer(url, mockPort))) );