diff --git a/__mocks__/api.yaml b/__mocks__/api.yaml index 2c50aa5..dd949af 100644 --- a/__mocks__/api.yaml +++ b/__mocks__/api.yaml @@ -366,6 +366,27 @@ 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 + - $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 abc4938..2249b35 100644 --- a/__mocks__/openapi_v3/api.yaml +++ b/__mocks__/openapi_v3/api.yaml @@ -500,6 +500,27 @@ 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 + - 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 allOf: 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); + }); +}); 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..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 @@ -310,6 +310,94 @@ 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\\"; +import { Person } from \\"./Person\\"; + +/** + * 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 type OneOfPropertyTestFields4 = t.TypeOf< + typeof OneOfPropertyTestFields4 +>; +export const OneOfPropertyTestFields4 = t.readonlyArray( + t.object, + \\"array of object\\" +); + +export const OneOfPropertyTestFields = t.union( + [ + OneOfPropertyTestFields1, + + OneOfPropertyTestFields2, + + Person, + + OneOfPropertyTestFields4 + ], + \\"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 +2284,94 @@ 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\\"; +import { Person } from \\"./Person\\"; + +/** + * 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 type OneOfPropertyTestFields3 = t.TypeOf< + typeof OneOfPropertyTestFields3 +>; +export const OneOfPropertyTestFields3 = t.readonlyArray( + t.object, + \\"array of object\\" +); + +export const OneOfPropertyTestFields = t.union( + [ + OneOfPropertyTestFields1, + + OneOfPropertyTestFields2, + + OneOfPropertyTestFields3, + + Person + ], + \\"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"; diff --git a/templates/macros.njk b/templates/macros.njk index 7c41a44..a3b5080 100644 --- a/templates/macros.njk +++ b/templates/macros.njk @@ -265,6 +265,14 @@ {{ defineConst(definition.default, definitionName, typedef, false, inline) }} {% endmacro %} +{## + # defines a oneOf property + #} + {% macro defineOneOfProperty(parentPropName, propName, definition, inline = false) -%} + {{ parentPropName }}{{ propName | capitalizeFirst }} + {{ defineConst(definition.default, typedef, typedef, false, inline) }} + {% endmacro %} + {## # defines an object property of some prop.type #} @@ -290,6 +298,8 @@ {{ defineString(propName, prop, true) }} {% elif prop.type == "boolean" %} {{ defineBoolean(propName, prop, true) }} + {% elif prop.oneOf %} + {{ defineOneOfProperty(parentPropName, propName, prop.oneOf, true)}} {% else %} // TODO: generate model for definition "{{ propName }}: {{ prop.type }}" {% endif %} @@ -323,6 +333,38 @@ {% endfor %} {% endmacro -%} +{% macro defineOneOf(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 %} + {{ 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 %} + {{ defineOneOf(composedPropName, prop.oneOf, strictInterfaces, camelCasedPropNames) }} {% endif %} {% endfor %} @@ -418,31 +463,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; + {{ defineOneOf(definitionName, oneOfProps, strictInterfaces, camelCasedPropNames) }} {% elif definition.type == "number" %}