From 787d76a6f8192da82cb630884119efac9c1ab434 Mon Sep 17 00:00:00 2001 From: Giovanni Berti Date: Tue, 8 Aug 2023 11:36:41 +0200 Subject: [PATCH 01/10] feat(oneof): implement first draft of `oneOf` handling for object properties --- __mocks__/openapi_v3/__tmp_oneOf.yaml | 79 +++++++++++++++++++ .../__tests__/__tmp_oneOf.test.ts | 19 +++++ templates/macros.njk | 45 +++++++++++ 3 files changed, 143 insertions(+) create mode 100644 __mocks__/openapi_v3/__tmp_oneOf.yaml create mode 100644 src/commands/gen-api-models/__tests__/__tmp_oneOf.test.ts diff --git a/__mocks__/openapi_v3/__tmp_oneOf.yaml b/__mocks__/openapi_v3/__tmp_oneOf.yaml new file mode 100644 index 0000000..16a19b6 --- /dev/null +++ b/__mocks__/openapi_v3/__tmp_oneOf.yaml @@ -0,0 +1,79 @@ +openapi: 3.0.0 +info: + version: 0.0.1 + title: Pagopa eCommerce payment methods service + description: This microservice handles payment methods. + contact: + name: pagoPA - Touchpoints team +tags: + - name: payment-methods + description: Api's for handle payment methods + externalDocs: + url: https://pagopa.atlassian.net/wiki/spaces/I/pages/611516433/-servizio+payment+methods+service + description: Technical specifications +externalDocs: + url: https://pagopa.atlassian.net/wiki/spaces/I/pages/492339720/pagoPA+eCommerce+Design+Review + description: Design review +servers: + - url: https://api.platform.pagopa.it/payment-methods +paths: + /dubidubidu: + post: + operationId: postDubidubidu + responses: + 200: + description: Desc + content: + application/json: + schema: + $ref: '#/components/schemas/DubidubiduResponse' + +components: + schemas: + DubidubiduResponse: + type: object + description: Form data needed to create a payment method input form + properties: + sessionId: + type: string + description: Identifier of the payment gateway session associated to the form + fields: + oneOf: + - type: array + items: + $ref: "#/components/schemas/CardFormField" + - type: array + items: + $ref: "#/components/schemas/FieldAA" + discriminator: + propertyName: paymentMethod + mapping: + CARD: "#/components/schemas/CardFormFields" + required: + - fields + - sessionId + FieldAA: + type: object + properties: + paymentMethod: + type: string + required: + - paymentMethod + CardFormField: + type: object + description: Form fields for credit cards + properties: + paymentMethod: + type: string + form: + type: array + items: + $ref: '#/components/schemas/Field' + required: + - paymentMethod + - form + Field: + type: object + properties: + foo: + type: string diff --git a/src/commands/gen-api-models/__tests__/__tmp_oneOf.test.ts b/src/commands/gen-api-models/__tests__/__tmp_oneOf.test.ts new file mode 100644 index 0000000..6b4b79a --- /dev/null +++ b/src/commands/gen-api-models/__tests__/__tmp_oneOf.test.ts @@ -0,0 +1,19 @@ +import {getParser} from "./utils/parser.utils"; +import {renderDefinitionCode} from "../render"; +import * as SwaggerParser from "swagger-parser"; +import {OpenAPIV3} from "openapi-types"; +import * as fs from "fs"; + +it("should render a client", async () => { + let specPath = `${process.cwd()}/__mocks__/openapi_v3/__tmp_oneOf.yaml`; + + let spec = (await SwaggerParser.bundle(specPath)) as + | OpenAPIV3.Document; + + const allOperations = getParser(spec).parseDefinition( + // @ts-ignore + spec.components.schemas["DubidubiduResponse"] + ); + + const code = await renderDefinitionCode("DubidubiduResponse", allOperations, true); +}); \ No newline at end of file diff --git a/templates/macros.njk b/templates/macros.njk index 7c41a44..53dec5e 100644 --- a/templates/macros.njk +++ b/templates/macros.njk @@ -265,6 +265,15 @@ {{ defineConst(definition.default, definitionName, typedef, false, inline) }} {% endmacro %} +{## + # defines a oneOf property + #} + {% macro defineOneOf(parentPropName, propName, definition, inline = false) -%} + {{- 'import * as t from "io-ts";' | addImport -}} + {% set definitionName %}{{ parentPropName }}{{ propName | capitalizeFirst }}{% endset %} + {{ definitionName }} + {% endmacro %} + {## # defines an object property of some prop.type #} @@ -290,6 +299,8 @@ {{ defineString(propName, prop, true) }} {% elif prop.type == "boolean" %} {{ defineBoolean(propName, prop, true) }} + {% elif prop.oneOf %} + {{ defineOneOf(parentPropName, propName, prop.oneOf, true)}} {% else %} // TODO: generate model for definition "{{ propName }}: {{ prop.type }}" {% endif %} @@ -323,6 +334,37 @@ {% endfor %} {% endmacro -%} +{% macro defineOneOfProperty(composedPropName, prop, strictInterfaces, camelCasedPropNames) -%} + {% set definitionName = composedPropName %} + {% for schema in prop -%} + {% if schema.type == "object" %} + {% set name = [definitionName, loop.index] | join %} + {{ defineObject(name, schema, strictInterfaces, camelCasedPropNames) }} + {% elif schema.$ref %} + {%- set realPropName = schema.$ref | splitBy("/") | last -%} + {{ importLocalProp(realPropName) }} + {% elif schema.type == "array" %} + {% set name %}{{ definitionName }}{{ loop.index }}{% endset %} + {{ defineArray(name, schema) }} + {% endif %} + {% endfor %} + + export const {{ definitionName }} = + t.union([ + {% for schema in prop -%} + {% if schema.type == "object" or schema.type == "array" %} + {{ definitionName }}{{ loop.index }}, + {% elif schema.$ref %} + {{ schema.$ref | splitBy("/") | last }}, + {% endif %} + {% endfor %} + ], + "{{ definitionName }}" + ); + + export type {{ definitionName }} = t.TypeOf; +{% endmacro -%} + {## # define object properties recursively, # supports additionaProperties, allOf and oneOf. @@ -341,6 +383,9 @@ {% if prop.type == "object" %} {% set composedPropName %}{{ definitionName }}{{ propName | capitalizeFirst }}{% endset %} {{ defineObject(composedPropName, prop, strictInterfaces, camelCasedPropNames) }} + {% elif prop.oneOf %} + {% set composedPropName %}{{ definitionName }}{{ propName | capitalizeFirst }}{% endset %} + {{ defineOneOfProperty(composedPropName, prop.oneOf, strictInterfaces, camelCasedPropNames) }} {% endif %} {% endfor %} From 9909b3f644be3b22816e161615518ade56119638 Mon Sep 17 00:00:00 2001 From: Giovanni Berti Date: Wed, 9 Aug 2023 10:53:53 +0200 Subject: [PATCH 02/10] fix(oneof): fix generated variant name for `oneOf` in properties --- templates/macros.njk | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/templates/macros.njk b/templates/macros.njk index 53dec5e..0e9b265 100644 --- a/templates/macros.njk +++ b/templates/macros.njk @@ -334,11 +334,10 @@ {% endfor %} {% endmacro -%} -{% macro defineOneOfProperty(composedPropName, prop, strictInterfaces, camelCasedPropNames) -%} - {% set definitionName = composedPropName %} +{% macro defineOneOfProperty(definitionName, prop, strictInterfaces, camelCasedPropNames) -%} {% for schema in prop -%} {% if schema.type == "object" %} - {% set name = [definitionName, loop.index] | join %} + {% set name %}{{ definitionName }}{{ loop.index }}{% endset %} {{ defineObject(name, schema, strictInterfaces, camelCasedPropNames) }} {% elif schema.$ref %} {%- set realPropName = schema.$ref | splitBy("/") | last -%} From c632e72b43e92eebbe75f971066cda539afc50c1 Mon Sep 17 00:00:00 2001 From: Giovanni Berti Date: Wed, 9 Aug 2023 10:52:47 +0200 Subject: [PATCH 03/10] test: add test case with snapshots for `oneOf` property --- __mocks__/api.yaml | 14 ++ __mocks__/openapi_v3/api.yaml | 14 ++ .../__snapshots__/index.test.ts.snap | 142 ++++++++++++++++++ .../gen-api-models/__tests__/index.test.ts | 17 +++ 4 files changed, 187 insertions(+) diff --git a/__mocks__/api.yaml b/__mocks__/api.yaml index 2c50aa5..65daea3 100644 --- a/__mocks__/api.yaml +++ b/__mocks__/api.yaml @@ -366,6 +366,20 @@ definitions: properties: unlimited: type: boolean + OneOfPropertyTest: + type: object + description: test if we can use `oneOf` as an object property + properties: + fields: + oneOf: + - type: object + properties: + foo: + type: string + - type: object + properties: + bar: + type: string AdditionalPropsTest: type: object additionalProperties: diff --git a/__mocks__/openapi_v3/api.yaml b/__mocks__/openapi_v3/api.yaml index abc4938..5a73278 100644 --- a/__mocks__/openapi_v3/api.yaml +++ b/__mocks__/openapi_v3/api.yaml @@ -500,6 +500,20 @@ components: properties: unlimited: type: boolean + OneOfPropertyTest: + type: object + description: test if we can use `oneOf` as an object property + properties: + fields: + oneOf: + - type: object + properties: + foo: + type: string + - type: object + properties: + bar: + type: string AllOfWithOneElementTest: description: test if we can use allOf with just one element inside allOf: 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 e8ea05e..f339557 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 @@ -310,6 +310,77 @@ export type OneOfTest = t.TypeOf; " `; +exports[`Openapi V2 |> gen-api-models should generate an object with a union property when that property has oneOf: oneof-property-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\\"; + +/** + * test if we can use \`oneOf\` as an object property + */ + +// required attributes +const OneOfPropertyTestFields1R = t.interface({}); + +// optional attributes +const OneOfPropertyTestFields1O = t.partial({ + foo: t.string +}); + +export const OneOfPropertyTestFields1 = t.intersection( + [OneOfPropertyTestFields1R, OneOfPropertyTestFields1O], + \\"OneOfPropertyTestFields1\\" +); + +export type OneOfPropertyTestFields1 = t.TypeOf< + typeof OneOfPropertyTestFields1 +>; + +// required attributes +const OneOfPropertyTestFields2R = t.interface({}); + +// optional attributes +const OneOfPropertyTestFields2O = t.partial({ + bar: t.string +}); + +export const OneOfPropertyTestFields2 = t.intersection( + [OneOfPropertyTestFields2R, OneOfPropertyTestFields2O], + \\"OneOfPropertyTestFields2\\" +); + +export type OneOfPropertyTestFields2 = t.TypeOf< + typeof OneOfPropertyTestFields2 +>; + +export const OneOfPropertyTestFields = t.union( + [OneOfPropertyTestFields1, OneOfPropertyTestFields2], + \\"OneOfPropertyTestFields\\" +); + +export type OneOfPropertyTestFields = t.TypeOf; + +// required attributes +const OneOfPropertyTestR = t.interface({}); + +// optional attributes +const OneOfPropertyTestO = t.partial({ + fields: OneOfPropertyTestFields +}); + +export const OneOfPropertyTest = t.intersection( + [OneOfPropertyTestR, OneOfPropertyTestO], + \\"OneOfPropertyTest\\" +); + +export type OneOfPropertyTest = t.TypeOf; +" +`; + exports[`Openapi V2 |> gen-api-models should generate decoder definitions for (get, /test-auth-bearer) 1`] = ` " /**************************************************************** @@ -2196,6 +2267,77 @@ export type OneOfTest = t.TypeOf; " `; +exports[`Openapi V3 |> gen-api-models should generate an object with a union property when that property has oneOf: oneof-property-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\\"; + +/** + * test if we can use \`oneOf\` as an object property + */ + +// required attributes +const OneOfPropertyTestFields1R = t.interface({}); + +// optional attributes +const OneOfPropertyTestFields1O = t.partial({ + foo: t.string +}); + +export const OneOfPropertyTestFields1 = t.intersection( + [OneOfPropertyTestFields1R, OneOfPropertyTestFields1O], + \\"OneOfPropertyTestFields1\\" +); + +export type OneOfPropertyTestFields1 = t.TypeOf< + typeof OneOfPropertyTestFields1 +>; + +// required attributes +const OneOfPropertyTestFields2R = t.interface({}); + +// optional attributes +const OneOfPropertyTestFields2O = t.partial({ + bar: t.string +}); + +export const OneOfPropertyTestFields2 = t.intersection( + [OneOfPropertyTestFields2R, OneOfPropertyTestFields2O], + \\"OneOfPropertyTestFields2\\" +); + +export type OneOfPropertyTestFields2 = t.TypeOf< + typeof OneOfPropertyTestFields2 +>; + +export const OneOfPropertyTestFields = t.union( + [OneOfPropertyTestFields1, OneOfPropertyTestFields2], + \\"OneOfPropertyTestFields\\" +); + +export type OneOfPropertyTestFields = t.TypeOf; + +// required attributes +const OneOfPropertyTestR = t.interface({}); + +// optional attributes +const OneOfPropertyTestO = t.partial({ + fields: OneOfPropertyTestFields +}); + +export const OneOfPropertyTest = t.intersection( + [OneOfPropertyTestR, OneOfPropertyTestO], + \\"OneOfPropertyTest\\" +); + +export type OneOfPropertyTest = t.TypeOf; +" +`; + exports[`Openapi V3 |> gen-api-models should generate decoder definitions for (get, /test-auth-bearer) 1`] = ` " /**************************************************************** diff --git a/src/commands/gen-api-models/__tests__/index.test.ts b/src/commands/gen-api-models/__tests__/index.test.ts index b529e5f..978b02f 100644 --- a/src/commands/gen-api-models/__tests__/index.test.ts +++ b/src/commands/gen-api-models/__tests__/index.test.ts @@ -350,6 +350,23 @@ describe.each` expect(code).toMatchSnapshot("oneof-test"); }); + it("should generate an object with a union property when that property has oneOf", async () => { + const definitonName = "OneOfPropertyTest"; + const definition = getDefinitionOrFail(spec, definitonName); + + const code = await renderDefinitionCode( + definitonName, + getParser(spec).parseDefinition( + // @ts-ignore + definition + ), + false + ); + + expect(code).toContain("t.union"); + expect(code).toMatchSnapshot("oneof-property-test"); + }); + it("should generate a type union from allOf when x-one-of is used", async () => { if (version === 2) { const definitonName = "AllOfOneOfTest"; From 96db5e6b32b06a71f91044e286ee1485d59cddcb Mon Sep 17 00:00:00 2001 From: Giovanni Berti Date: Wed, 9 Aug 2023 10:53:29 +0200 Subject: [PATCH 04/10] test: remove old temporary tests --- __mocks__/openapi_v3/__tmp_oneOf.yaml | 79 ------------------- .../__tests__/__tmp_oneOf.test.ts | 19 ----- 2 files changed, 98 deletions(-) delete mode 100644 __mocks__/openapi_v3/__tmp_oneOf.yaml delete mode 100644 src/commands/gen-api-models/__tests__/__tmp_oneOf.test.ts diff --git a/__mocks__/openapi_v3/__tmp_oneOf.yaml b/__mocks__/openapi_v3/__tmp_oneOf.yaml deleted file mode 100644 index 16a19b6..0000000 --- a/__mocks__/openapi_v3/__tmp_oneOf.yaml +++ /dev/null @@ -1,79 +0,0 @@ -openapi: 3.0.0 -info: - version: 0.0.1 - title: Pagopa eCommerce payment methods service - description: This microservice handles payment methods. - contact: - name: pagoPA - Touchpoints team -tags: - - name: payment-methods - description: Api's for handle payment methods - externalDocs: - url: https://pagopa.atlassian.net/wiki/spaces/I/pages/611516433/-servizio+payment+methods+service - description: Technical specifications -externalDocs: - url: https://pagopa.atlassian.net/wiki/spaces/I/pages/492339720/pagoPA+eCommerce+Design+Review - description: Design review -servers: - - url: https://api.platform.pagopa.it/payment-methods -paths: - /dubidubidu: - post: - operationId: postDubidubidu - responses: - 200: - description: Desc - content: - application/json: - schema: - $ref: '#/components/schemas/DubidubiduResponse' - -components: - schemas: - DubidubiduResponse: - type: object - description: Form data needed to create a payment method input form - properties: - sessionId: - type: string - description: Identifier of the payment gateway session associated to the form - fields: - oneOf: - - type: array - items: - $ref: "#/components/schemas/CardFormField" - - type: array - items: - $ref: "#/components/schemas/FieldAA" - discriminator: - propertyName: paymentMethod - mapping: - CARD: "#/components/schemas/CardFormFields" - required: - - fields - - sessionId - FieldAA: - type: object - properties: - paymentMethod: - type: string - required: - - paymentMethod - CardFormField: - type: object - description: Form fields for credit cards - properties: - paymentMethod: - type: string - form: - type: array - items: - $ref: '#/components/schemas/Field' - required: - - paymentMethod - - form - Field: - type: object - properties: - foo: - type: string diff --git a/src/commands/gen-api-models/__tests__/__tmp_oneOf.test.ts b/src/commands/gen-api-models/__tests__/__tmp_oneOf.test.ts deleted file mode 100644 index 6b4b79a..0000000 --- a/src/commands/gen-api-models/__tests__/__tmp_oneOf.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {getParser} from "./utils/parser.utils"; -import {renderDefinitionCode} from "../render"; -import * as SwaggerParser from "swagger-parser"; -import {OpenAPIV3} from "openapi-types"; -import * as fs from "fs"; - -it("should render a client", async () => { - let specPath = `${process.cwd()}/__mocks__/openapi_v3/__tmp_oneOf.yaml`; - - let spec = (await SwaggerParser.bundle(specPath)) as - | OpenAPIV3.Document; - - const allOperations = getParser(spec).parseDefinition( - // @ts-ignore - spec.components.schemas["DubidubiduResponse"] - ); - - const code = await renderDefinitionCode("DubidubiduResponse", allOperations, true); -}); \ No newline at end of file From d37684520ce645ab0806e85415d91893fbe0cf9f Mon Sep 17 00:00:00 2001 From: Giovanni Berti Date: Wed, 9 Aug 2023 15:09:02 +0200 Subject: [PATCH 05/10] test: add `$ref` variant to `oneOf` property test --- __mocks__/api.yaml | 1 + __mocks__/openapi_v3/api.yaml | 1 + .../__tests__/__snapshots__/index.test.ts.snap | 6 ++++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/__mocks__/api.yaml b/__mocks__/api.yaml index 65daea3..26fb68f 100644 --- a/__mocks__/api.yaml +++ b/__mocks__/api.yaml @@ -380,6 +380,7 @@ definitions: properties: bar: type: string + - $ref: "#/definitions/Person" AdditionalPropsTest: type: object additionalProperties: diff --git a/__mocks__/openapi_v3/api.yaml b/__mocks__/openapi_v3/api.yaml index 5a73278..75a10d0 100644 --- a/__mocks__/openapi_v3/api.yaml +++ b/__mocks__/openapi_v3/api.yaml @@ -514,6 +514,7 @@ components: properties: bar: type: string + - $ref: "#/components/schemas/Person" AllOfWithOneElementTest: description: test if we can use allOf with just one element inside allOf: 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 f339557..5ed8fb8 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 @@ -318,6 +318,7 @@ exports[`Openapi V2 |> gen-api-models should generate an object with a union pro /* eslint-disable */ import * as t from \\"io-ts\\"; +import { Person } from \\"./Person\\"; /** * test if we can use \`oneOf\` as an object property @@ -358,7 +359,7 @@ export type OneOfPropertyTestFields2 = t.TypeOf< >; export const OneOfPropertyTestFields = t.union( - [OneOfPropertyTestFields1, OneOfPropertyTestFields2], + [OneOfPropertyTestFields1, OneOfPropertyTestFields2, Person], \\"OneOfPropertyTestFields\\" ); @@ -2275,6 +2276,7 @@ exports[`Openapi V3 |> gen-api-models should generate an object with a union pro /* eslint-disable */ import * as t from \\"io-ts\\"; +import { Person } from \\"./Person\\"; /** * test if we can use \`oneOf\` as an object property @@ -2315,7 +2317,7 @@ export type OneOfPropertyTestFields2 = t.TypeOf< >; export const OneOfPropertyTestFields = t.union( - [OneOfPropertyTestFields1, OneOfPropertyTestFields2], + [OneOfPropertyTestFields1, OneOfPropertyTestFields2, Person], \\"OneOfPropertyTestFields\\" ); From adfa48695d31907d82744095dd0d20390067ab27 Mon Sep 17 00:00:00 2001 From: Giovanni Berti Date: Wed, 9 Aug 2023 15:31:31 +0200 Subject: [PATCH 06/10] refactor: deduplicate `oneOf` definition code --- templates/macros.njk | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/templates/macros.njk b/templates/macros.njk index 0e9b265..5eddefd 100644 --- a/templates/macros.njk +++ b/templates/macros.njk @@ -335,6 +335,8 @@ {% endmacro -%} {% macro defineOneOfProperty(definitionName, prop, strictInterfaces, camelCasedPropNames) -%} + {{- 'import * as t from "io-ts";' | addImport -}} + {% for schema in prop -%} {% if schema.type == "object" %} {% set name %}{{ definitionName }}{{ loop.index }}{% endset %} @@ -462,31 +464,7 @@ {% elif definition.oneOf %} {% set oneOfProps = definition.oneOf if definition.oneOf else definition.allOf %} - {{- 'import * as t from "io-ts";' | addImport -}} - - {% for schema in oneOfProps -%} - {% if schema.type == "object" %} - {{ defineObject(definitionName + loop.index, schema, strictInterfaces, camelCasedPropNames) }} - {% elif schema.$ref %} - {%- set realPropName = schema.$ref | splitBy("/") | last -%} - {{ importLocalProp(realPropName) }} - {% endif %} - {% endfor %} - - export const {{ definitionName }} = - t.union([ - {% for schema in oneOfProps -%} - {% if schema.type == "object" %} - {{ definitionName + loop.index }}, - {% elif schema.$ref %} - {{ schema.$ref | splitBy("/") | last }}, - {% endif %} - {% endfor %} - ], - "{{ definitionName }}" - ); - - export type {{ definitionName }} = t.TypeOf; + {{ defineOneOfProperty(definitionName, oneOfProps, strictInterfaces, camelCasedPropNames) }} {% elif definition.type == "number" %} From 72934faa2a9d6f14839a4ad0200bda34d060f284 Mon Sep 17 00:00:00 2001 From: Giovanni Berti Date: Wed, 9 Aug 2023 15:34:11 +0200 Subject: [PATCH 07/10] refactor(macros): rename `defineOneOf` <-> `defineOneOfProperty` --- templates/macros.njk | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/macros.njk b/templates/macros.njk index 5eddefd..ea6c3e1 100644 --- a/templates/macros.njk +++ b/templates/macros.njk @@ -268,7 +268,7 @@ {## # defines a oneOf property #} - {% macro defineOneOf(parentPropName, propName, definition, inline = false) -%} + {% macro defineOneOfProperty(parentPropName, propName, definition, inline = false) -%} {{- 'import * as t from "io-ts";' | addImport -}} {% set definitionName %}{{ parentPropName }}{{ propName | capitalizeFirst }}{% endset %} {{ definitionName }} @@ -300,7 +300,7 @@ {% elif prop.type == "boolean" %} {{ defineBoolean(propName, prop, true) }} {% elif prop.oneOf %} - {{ defineOneOf(parentPropName, propName, prop.oneOf, true)}} + {{ defineOneOfProperty(parentPropName, propName, prop.oneOf, true)}} {% else %} // TODO: generate model for definition "{{ propName }}: {{ prop.type }}" {% endif %} @@ -334,7 +334,7 @@ {% endfor %} {% endmacro -%} -{% macro defineOneOfProperty(definitionName, prop, strictInterfaces, camelCasedPropNames) -%} +{% macro defineOneOf(definitionName, prop, strictInterfaces, camelCasedPropNames) -%} {{- 'import * as t from "io-ts";' | addImport -}} {% for schema in prop -%} @@ -386,7 +386,7 @@ {{ defineObject(composedPropName, prop, strictInterfaces, camelCasedPropNames) }} {% elif prop.oneOf %} {% set composedPropName %}{{ definitionName }}{{ propName | capitalizeFirst }}{% endset %} - {{ defineOneOfProperty(composedPropName, prop.oneOf, strictInterfaces, camelCasedPropNames) }} + {{ defineOneOf(composedPropName, prop.oneOf, strictInterfaces, camelCasedPropNames) }} {% endif %} {% endfor %} @@ -464,7 +464,7 @@ {% elif definition.oneOf %} {% set oneOfProps = definition.oneOf if definition.oneOf else definition.allOf %} - {{ defineOneOfProperty(definitionName, oneOfProps, strictInterfaces, camelCasedPropNames) }} + {{ defineOneOf(definitionName, oneOfProps, strictInterfaces, camelCasedPropNames) }} {% elif definition.type == "number" %} From f41a08f9d857bfb457dc5d5f99737290b6a0db92 Mon Sep 17 00:00:00 2001 From: Giovanni Berti Date: Mon, 16 Oct 2023 14:58:19 +0200 Subject: [PATCH 08/10] refactor: use `defineConst` instad of manually interpolating new oneOf variant name --- templates/macros.njk | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/templates/macros.njk b/templates/macros.njk index ea6c3e1..a3b5080 100644 --- a/templates/macros.njk +++ b/templates/macros.njk @@ -269,9 +269,8 @@ # defines a oneOf property #} {% macro defineOneOfProperty(parentPropName, propName, definition, inline = false) -%} - {{- 'import * as t from "io-ts";' | addImport -}} - {% set definitionName %}{{ parentPropName }}{{ propName | capitalizeFirst }}{% endset %} - {{ definitionName }} + {{ parentPropName }}{{ propName | capitalizeFirst }} + {{ defineConst(definition.default, typedef, typedef, false, inline) }} {% endmacro %} {## From b768f814f29ca60fdc72fafdb3a236b1e3225235 Mon Sep 17 00:00:00 2001 From: Giovanni Berti Date: Mon, 16 Oct 2023 15:28:44 +0200 Subject: [PATCH 09/10] test: add test case for `oneOf` with array variant --- __mocks__/api.yaml | 6 ++++ __mocks__/openapi_v3/api.yaml | 6 ++++ .../__snapshots__/index.test.ts.snap | 36 +++++++++++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/__mocks__/api.yaml b/__mocks__/api.yaml index 26fb68f..dd949af 100644 --- a/__mocks__/api.yaml +++ b/__mocks__/api.yaml @@ -381,6 +381,12 @@ definitions: bar: type: string - $ref: "#/definitions/Person" + - type: array + items: + type: object + properties: + name: + type: string AdditionalPropsTest: type: object additionalProperties: diff --git a/__mocks__/openapi_v3/api.yaml b/__mocks__/openapi_v3/api.yaml index 75a10d0..2249b35 100644 --- a/__mocks__/openapi_v3/api.yaml +++ b/__mocks__/openapi_v3/api.yaml @@ -514,6 +514,12 @@ components: properties: bar: type: string + - type: array + items: + type: object + properties: + name: + type: string - $ref: "#/components/schemas/Person" AllOfWithOneElementTest: description: test if we can use allOf with just one element inside 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 5ed8fb8..9ea1fce 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 @@ -358,8 +358,24 @@ export type OneOfPropertyTestFields2 = t.TypeOf< typeof OneOfPropertyTestFields2 >; +export type OneOfPropertyTestFields4 = t.TypeOf< + typeof OneOfPropertyTestFields4 +>; +export const OneOfPropertyTestFields4 = t.readonlyArray( + t.object, + \\"array of object\\" +); + export const OneOfPropertyTestFields = t.union( - [OneOfPropertyTestFields1, OneOfPropertyTestFields2, Person], + [ + OneOfPropertyTestFields1, + + OneOfPropertyTestFields2, + + Person, + + OneOfPropertyTestFields4 + ], \\"OneOfPropertyTestFields\\" ); @@ -2316,8 +2332,24 @@ export type OneOfPropertyTestFields2 = t.TypeOf< typeof OneOfPropertyTestFields2 >; +export type OneOfPropertyTestFields3 = t.TypeOf< + typeof OneOfPropertyTestFields3 +>; +export const OneOfPropertyTestFields3 = t.readonlyArray( + t.object, + \\"array of object\\" +); + export const OneOfPropertyTestFields = t.union( - [OneOfPropertyTestFields1, OneOfPropertyTestFields2, Person], + [ + OneOfPropertyTestFields1, + + OneOfPropertyTestFields2, + + OneOfPropertyTestFields3, + + Person + ], \\"OneOfPropertyTestFields\\" ); From 4c70dee8807bb29783e139ff2f7b941c5a60d548 Mon Sep 17 00:00:00 2001 From: Giovanni Berti Date: Tue, 17 Oct 2023 14:49:17 +0200 Subject: [PATCH 10/10] test: added e2e tests --- .../__tests__/test-api-v3/definitions.test.ts | 42 +++++++++++++++++++ .../__tests__/test-api/definitions.test.ts | 42 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/e2e/src/__tests__/test-api-v3/definitions.test.ts b/e2e/src/__tests__/test-api-v3/definitions.test.ts index 83cf5c4..47fd44f 100644 --- a/e2e/src/__tests__/test-api-v3/definitions.test.ts +++ b/e2e/src/__tests__/test-api-v3/definitions.test.ts @@ -417,3 +417,45 @@ describe("DisjointUnionsUserTest definition", () => { expect(E.isLeft(enabledUserTest)).toBe(true); }); }); + +describe("OneOfPropertyTest definition", () => { + const variant1 = { + fields: { + foo: "foo" + } + }; + + const variant2 = { + fields: { + bar: "bar" + } + }; + + const variant3 = { + fields: [ + { + name: "name" + } + ] + }; + + const variant4 = { + fields: { + name: "name", + address: "address" + } + }; + + it.each` + title | example | expected + ${"should decode variant 1"} | ${variant1} | ${true} + ${"should decode variant 2"} | ${variant2} | ${true} + ${"should decode variant 3"} | ${variant3} | ${true} + ${"should decode variant 4"} | ${variant4} | ${true} + `("$title", async ({ example, expected }) => { + const { OneOfPropertyTest } = await loadModule("OneOfPropertyTest"); + const result = E.isRight(OneOfPropertyTest.decode(example)); + + expect(result).toEqual(expected); + }); +}); diff --git a/e2e/src/__tests__/test-api/definitions.test.ts b/e2e/src/__tests__/test-api/definitions.test.ts index 07bb6b3..8486778 100644 --- a/e2e/src/__tests__/test-api/definitions.test.ts +++ b/e2e/src/__tests__/test-api/definitions.test.ts @@ -435,3 +435,45 @@ describe("DisjointUnionsUserTest definition", () => { expect(E.isLeft(enabledUserTest)).toBe(true); }); }); + +describe("OneOfPropertyTest definition", () => { + const variant1 = { + fields: { + foo: "foo" + } + }; + + const variant2 = { + fields: { + bar: "bar" + } + }; + + const variant3 = { + fields: [ + { + name: "name" + } + ] + }; + + const variant4 = { + fields: { + name: "name", + address: "address" + } + }; + + it.each` + title | example | expected + ${"should decode variant 1"} | ${variant1} | ${true} + ${"should decode variant 2"} | ${variant2} | ${true} + ${"should decode variant 3"} | ${variant3} | ${true} + ${"should decode variant 4"} | ${variant4} | ${true} + `("$title", async ({ example, expected }) => { + const { OneOfPropertyTest } = await loadModule("OneOfPropertyTest"); + const result = E.isRight(OneOfPropertyTest.decode(example)); + + expect(result).toEqual(expected); + }); +});