diff --git a/rollup.config.mjs b/rollup.config.mjs index ee563c43e..65d57f8f2 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -37,6 +37,7 @@ const handlebarsPlugin = () => ({ escapeDescription: true, escapeEnumName: true, escapeNewline: true, + exactArray: true, ifdef: true, ifOperationDataOptional: true, intersection: true, diff --git a/src/openApi/v3/parser/getModel.ts b/src/openApi/v3/parser/getModel.ts index b673cbd2e..c92839c74 100644 --- a/src/openApi/v3/parser/getModel.ts +++ b/src/openApi/v3/parser/getModel.ts @@ -92,7 +92,16 @@ export const getModel = ( } } - const arrayItems = getModel(openApi, definition.items); + /** + * if items are a plain array, infer any-of composition + * {@link} https://github.com/ferdikoomen/openapi-typescript-codegen/issues/2062 + */ + const arrayItemsDefinition: OpenApiSchema = Array.isArray(definition.items) + ? { + anyOf: definition.items, + } + : definition.items; + const arrayItems = getModel(openApi, arrayItemsDefinition); model.base = arrayItems.base; model.export = 'array'; model.imports.push(...arrayItems.imports); diff --git a/src/templates/partials/typeArray.hbs b/src/templates/partials/typeArray.hbs index c3d44374f..1f934a1a3 100644 --- a/src/templates/partials/typeArray.hbs +++ b/src/templates/partials/typeArray.hbs @@ -1,5 +1,9 @@ +{{~#exactArray this}} +[{{>type link filterProperties="exact"}}]{{>isNullable}} +{{~else~}} {{~#if link~}} Array<{{>type link}}>{{>isNullable}} {{~else~}} Array<{{>base}}>{{>isNullable}} {{~/if~}} +{{~/exactArray~}} diff --git a/src/templates/partials/typeUnion.hbs b/src/templates/partials/typeUnion.hbs index 59371dfe4..df6a570ab 100644 --- a/src/templates/partials/typeUnion.hbs +++ b/src/templates/partials/typeUnion.hbs @@ -1 +1 @@ -{{#union properties parent}}{{this}}{{/union}}{{>isNullable}} +{{#union properties parent filterProperties}}{{this}}{{/union}}{{>isNullable}} diff --git a/src/utils/registerHandlebarHelpers.spec.ts b/src/utils/__tests__/registerHandlebarHelpers.spec.ts similarity index 87% rename from src/utils/registerHandlebarHelpers.spec.ts rename to src/utils/__tests__/registerHandlebarHelpers.spec.ts index cae869811..f5ff25d6b 100644 --- a/src/utils/registerHandlebarHelpers.spec.ts +++ b/src/utils/__tests__/registerHandlebarHelpers.spec.ts @@ -1,7 +1,7 @@ import Handlebars from 'handlebars/runtime'; -import { HttpClient } from '../HttpClient'; -import { registerHandlebarHelpers } from './registerHandlebarHelpers'; +import { HttpClient } from '../../HttpClient'; +import { registerHandlebarHelpers } from '../registerHandlebarHelpers'; describe('registerHandlebarHelpers', () => { it('should register the helpers', () => { @@ -20,6 +20,7 @@ describe('registerHandlebarHelpers', () => { expect(helpers).toContain('escapeDescription'); expect(helpers).toContain('escapeEnumName'); expect(helpers).toContain('escapeNewline'); + expect(helpers).toContain('exactArray'); expect(helpers).toContain('ifdef'); expect(helpers).toContain('ifOperationDataOptional'); expect(helpers).toContain('intersection'); diff --git a/src/utils/registerHandlebarHelpers.ts b/src/utils/registerHandlebarHelpers.ts index 7f9bbe4a9..478e592fb 100644 --- a/src/utils/registerHandlebarHelpers.ts +++ b/src/utils/registerHandlebarHelpers.ts @@ -85,6 +85,13 @@ export const registerHandlebarHelpers = ( return value.replace(/\n/g, '\\n'); }); + Handlebars.registerHelper('exactArray', function (this: any, model: Model, options: Handlebars.HelperOptions) { + if (model.export === 'array' && model.maxItems && model.minItems && model.maxItems === model.minItems) { + return options.fn(this); + } + return options.inverse(this); + }); + Handlebars.registerHelper('ifdef', function (this: any, ...args): string { const options = args.pop(); if (!args.every(value => !value)) { @@ -127,15 +134,22 @@ export const registerHandlebarHelpers = ( Handlebars.registerHelper( 'union', - function (this: any, properties: Model[], parent: string | undefined, options: Handlebars.HelperOptions) { + function ( + this: any, + properties: Model[], + parent: string | undefined, + filterProperties: 'exact' | undefined, + options: Handlebars.HelperOptions + ) { const type = Handlebars.partials['type']; - const types = properties.map(property => type({ ...root, ...property, parent })); - const uniqueTypes = types.filter(unique); - let uniqueTypesString = uniqueTypes.join(' | '); - if (uniqueTypes.length > 1) { - uniqueTypesString = `(${uniqueTypesString})`; + const types = properties + .map(property => type({ ...root, ...property, parent })) + .filter((...args) => filterProperties === 'exact' || unique(...args)); + let output = types.join(' | '); + if (types.length > 1 && types.length !== properties.length) { + output = `(${output})`; } - return options.fn(uniqueTypesString); + return options.fn(output); } ); diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index 6cc6fee9d..468bbd1ca 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -4726,6 +4726,7 @@ export type { ModelWithNestedEnums } from './models/ModelWithNestedEnums'; export type { ModelWithNestedProperties } from './models/ModelWithNestedProperties'; export type { ModelWithNullableObject } from './models/ModelWithNullableObject'; export type { ModelWithNullableString } from './models/ModelWithNullableString'; +export { ModelWithOneOfEnum } from './models/ModelWithOneOfEnum'; export type { ModelWithOrderedProperties } from './models/ModelWithOrderedProperties'; export type { ModelWithPattern } from './models/ModelWithPattern'; export type { ModelWithProperties } from './models/ModelWithProperties'; @@ -4811,6 +4812,7 @@ export { $ModelWithNestedEnums } from './schemas/$ModelWithNestedEnums'; export { $ModelWithNestedProperties } from './schemas/$ModelWithNestedProperties'; export { $ModelWithNullableObject } from './schemas/$ModelWithNullableObject'; export { $ModelWithNullableString } from './schemas/$ModelWithNullableString'; +export { $ModelWithOneOfEnum } from './schemas/$ModelWithOneOfEnum'; export { $ModelWithOrderedProperties } from './schemas/$ModelWithOrderedProperties'; export { $ModelWithPattern } from './schemas/$ModelWithPattern'; export { $ModelWithProperties } from './schemas/$ModelWithProperties'; @@ -4871,7 +4873,7 @@ exports[`v3 should generate: test/generated/v3/models/AnyOfAnyAndNull.ts 1`] = ` /* tslint:disable */ /* eslint-disable */ export type AnyOfAnyAndNull = { - data?: (any | null); + data?: any | null; }; " @@ -4886,11 +4888,11 @@ exports[`v3 should generate: test/generated/v3/models/AnyOfArrays.ts 1`] = ` * This is a simple array with any of properties */ export type AnyOfArrays = { - results?: Array<({ + results?: Array<{ foo?: string; } | { bar?: string; - })>; + }>; }; " @@ -4904,11 +4906,11 @@ exports[`v3 should generate: test/generated/v3/models/ArrayWithAnyOfProperties.t /** * This is a simple array with any of properties */ -export type ArrayWithAnyOfProperties = Array<({ +export type ArrayWithAnyOfProperties = Array<{ foo?: string; } | { bar?: string; -})>; +}>; " `; @@ -5131,7 +5133,7 @@ import type { ModelWithString } from './ModelWithString'; * This is a model with one property with a 'any of' relationship */ export type CompositionWithAnyOf = { - propA?: (ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary); + propA?: ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary; }; " @@ -5149,9 +5151,9 @@ import type { ModelWithEnum } from './ModelWithEnum'; * This is a model with one property with a 'any of' relationship */ export type CompositionWithAnyOfAndNullable = { - propA?: ({ + propA?: { boolean?: boolean; - } | ModelWithEnum | ModelWithArray | ModelWithDictionary) | null; + } | ModelWithEnum | ModelWithArray | ModelWithDictionary | null; }; " @@ -5166,9 +5168,9 @@ exports[`v3 should generate: test/generated/v3/models/CompositionWithAnyOfAnonym * This is a model with one property with a 'any of' relationship where the options are not $ref */ export type CompositionWithAnyOfAnonymous = { - propA?: ({ + propA?: { propA?: string; - } | string | number); + } | string | number; }; " @@ -5185,7 +5187,7 @@ import type { ModelWithDictionary } from './ModelWithDictionary'; * This is a model with nested 'any of' property with a type null */ export type CompositionWithNestedAnyAndTypeNull = { - propA?: (Array<(ModelWithDictionary | null)> | Array<(ModelWithArray | null)>); + propA?: Array | Array; }; " @@ -5202,7 +5204,7 @@ import type { Enum1 } from './Enum1'; * This is a model with one property with a 'any of' relationship where the options are not $ref */ export type CompositionWithNestedAnyOfAndNull = { - propA?: (Array<(Enum1 | ConstValue)> | null); + propA?: Array | null; }; " @@ -5221,7 +5223,7 @@ import type { ModelWithString } from './ModelWithString'; * This is a model with one property with a 'one of' relationship */ export type CompositionWithOneOf = { - propA?: (ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary); + propA?: ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary; }; " @@ -5236,7 +5238,7 @@ exports[`v3 should generate: test/generated/v3/models/CompositionWithOneOfAndCom * This is a model that contains a dictionary of complex arrays (composited) within composition */ export type CompositionWithOneOfAndComplexArrayDictionary = { - propA?: (boolean | Record>); + propA?: boolean | Record>; }; " @@ -5254,9 +5256,9 @@ import type { ModelWithEnum } from './ModelWithEnum'; * This is a model with one property with a 'one of' relationship */ export type CompositionWithOneOfAndNullable = { - propA?: ({ + propA?: { boolean?: boolean; - } | ModelWithEnum | ModelWithArray | ModelWithDictionary) | null; + } | ModelWithEnum | ModelWithArray | ModelWithDictionary | null; }; " @@ -5269,7 +5271,7 @@ exports[`v3 should generate: test/generated/v3/models/CompositionWithOneOfAndPro /* eslint-disable */ import type { NonAsciiStringæøåÆØÅöôêÊ字符串 } from './NonAsciiStringæøåÆØÅöôêÊ字符串'; import type { SimpleParameter } from './SimpleParameter'; -export type CompositionWithOneOfAndProperties = ({ +export type CompositionWithOneOfAndProperties = { foo: SimpleParameter; baz: number | null; qux: number; @@ -5277,7 +5279,7 @@ export type CompositionWithOneOfAndProperties = ({ bar: NonAsciiStringæøåÆØÅöôêÊ字符串; baz: number | null; qux: number; -}); +}; " `; @@ -5291,7 +5293,7 @@ exports[`v3 should generate: test/generated/v3/models/CompositionWithOneOfAndSim * This is a model that contains a dictionary of simple arrays within composition */ export type CompositionWithOneOfAndSimpleArrayDictionary = { - propA?: (boolean | Record>); + propA?: boolean | Record>; }; " @@ -5306,7 +5308,7 @@ exports[`v3 should generate: test/generated/v3/models/CompositionWithOneOfAndSim * This is a model that contains a simple dictionary within composition */ export type CompositionWithOneOfAndSimpleDictionary = { - propA?: (boolean | Record); + propA?: boolean | Record; }; " @@ -5321,9 +5323,9 @@ exports[`v3 should generate: test/generated/v3/models/CompositionWithOneOfAnonym * This is a model with one property with a 'one of' relationship where the options are not $ref */ export type CompositionWithOneOfAnonymous = { - propA?: ({ + propA?: { propA?: string; - } | string | number); + } | string | number; }; " @@ -5339,7 +5341,7 @@ import type { ModelSquare } from './ModelSquare'; /** * This is a model with one property with a 'one of' relationship where the options are not $ref */ -export type CompositionWithOneOfDiscriminator = (ModelCircle | ModelSquare); +export type CompositionWithOneOfDiscriminator = ModelCircle | ModelSquare; " `; @@ -5971,6 +5973,29 @@ export type ModelWithNullableString = { " `; +exports[`v3 should generate: test/generated/v3/models/ModelWithOneOfEnum.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ModelWithOneOfEnum = ({ + foo: ModelWithOneOfEnum.foo; +} | { + content: string; + foo: ModelWithOneOfEnum.foo; +} | { + content: [string | string]; + foo: ModelWithOneOfEnum.foo; +}); +export namespace ModelWithOneOfEnum { + export enum foo { + BAR = 'Bar', + } +} + +" +`; + exports[`v3 should generate: test/generated/v3/models/ModelWithOrderedProperties.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ @@ -6078,7 +6103,7 @@ exports[`v3 should generate: test/generated/v3/models/NestedAnyOfArraysNullable. /* tslint:disable */ /* eslint-disable */ export type NestedAnyOfArraysNullable = { - nullableArray?: (Array<(string | boolean)> | null); + nullableArray?: Array | null; }; " @@ -7637,6 +7662,71 @@ export const $ModelWithNullableString = { " `; +exports[`v3 should generate: test/generated/v3/schemas/$ModelWithOneOfEnum.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ModelWithOneOfEnum = { + type: 'one-of', + contains: [{ + properties: { + foo: { + type: 'Enum', + isRequired: true, + }, + }, + }, { + properties: { + foo: { + type: 'Enum', + isRequired: true, + }, + }, + }, { + properties: { + foo: { + type: 'Enum', + isRequired: true, + }, + }, + }, { + properties: { + content: { + type: 'string', + isRequired: true, + format: 'date-time', + }, + foo: { + type: 'Enum', + isRequired: true, + }, + }, + }, { + properties: { + content: { + type: 'array', + contains: { + type: 'any-of', + contains: [{ + type: 'string', + format: 'date-time', + }, { + type: 'string', + }], + }, + isRequired: true, + }, + foo: { + type: 'Enum', + isRequired: true, + }, + }, + }], +} as const; +" +`; + exports[`v3 should generate: test/generated/v3/schemas/$ModelWithOrderedProperties.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do not edit */ /* istanbul ignore file */ @@ -8069,7 +8159,7 @@ export class ComplexService { readonly type: 'Monkey' | 'Horse' | 'Bird'; listOfModels?: Array | null; listOfStrings?: Array | null; - parameters: (ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary); + parameters: ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary; readonly user?: { readonly id?: number; readonly name?: string | null; diff --git a/test/spec/v3.json b/test/spec/v3.json index 2e1e78b23..b47325427 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -2899,7 +2899,101 @@ "$ref": "#/components/schemas/NullableObject" } } - } + }, + "ModelWithOneOfEnum": { + "oneOf": [ + { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "string", + "enum": [ + "Bar" + ] + } + } + }, + { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "string", + "enum": [ + "Baz" + ] + } + } + }, + { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "string", + "enum": [ + "Qux" + ] + } + } + }, + { + "type": "object", + "required": [ + "content", + "foo" + ], + "properties": { + "content": { + "type": "string", + "format": "date-time" + }, + "foo": { + "type": "string", + "enum": [ + "Quux" + ] + } + } + }, + { + "type": "object", + "required": [ + "content", + "foo" + ], + "properties": { + "content": { + "type": "array", + "items": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 + }, + "foo": { + "type": "string", + "enum": [ + "Corge" + ] + } + } + } + ] + } } } } \ No newline at end of file