From c1d8979c626e44b413b6bee37e7c4b77ad0e324e Mon Sep 17 00:00:00 2001 From: Lubos Date: Sun, 4 Feb 2024 18:40:36 +0800 Subject: [PATCH] fix(parser): parse array items only if parent definition has type --- bin/index.js | 2 +- bin/index.spec.js | 1 - src/client/interfaces/ModelComposition.d.ts | 4 +- src/openApi/v2/parser/getModel.ts | 2 +- src/openApi/v2/parser/getModelComposition.ts | 4 +- src/openApi/v3/interfaces/OpenApiSchema.d.ts | 2 +- src/openApi/v3/parser/getModel.ts | 66 ++++++-------- src/openApi/v3/parser/getModelComposition.ts | 54 +++++++++--- test/__snapshots__/index.spec.ts.snap | 91 +++++++++++++++----- test/spec/v3.json | 25 ++++++ 10 files changed, 174 insertions(+), 77 deletions(-) diff --git a/bin/index.js b/bin/index.js index ecc915aa8..ce1c2ea81 100755 --- a/bin/index.js +++ b/bin/index.js @@ -16,7 +16,7 @@ const params = program .option('--name ', 'Custom client class name') .option('--useOptions', 'Use options instead of arguments') .option('--useUnionTypes', 'Use union types instead of enums') - .option('--autoformat ', 'Process generated files with autoformatter', false) + .option('--autoformat', 'Process generated files with autoformatter', false) .option('--exportCore ', 'Write core files to disk', true) .option('--exportServices ', 'Write services to disk', true) .option('--exportModels ', 'Write models to disk', true) diff --git a/bin/index.spec.js b/bin/index.spec.js index 5d6d86aec..66d5bcd9f 100755 --- a/bin/index.spec.js +++ b/bin/index.spec.js @@ -67,7 +67,6 @@ describe('bin', () => { '--output', './test/generated/bin', '--autoformat', - 'true', ]); expect(result.stdout.toString()).toBe(''); expect(result.stderr.toString()).toBe(''); diff --git a/src/client/interfaces/ModelComposition.d.ts b/src/client/interfaces/ModelComposition.d.ts index f17fc5e64..b872651d2 100644 --- a/src/client/interfaces/ModelComposition.d.ts +++ b/src/client/interfaces/ModelComposition.d.ts @@ -1,8 +1,8 @@ import type { Model } from './Model'; export interface ModelComposition { - type: 'one-of' | 'any-of' | 'all-of'; - imports: string[]; enums: Model[]; + export: 'one-of' | 'any-of' | 'all-of'; + imports: string[]; properties: Model[]; } diff --git a/src/openApi/v2/parser/getModel.ts b/src/openApi/v2/parser/getModel.ts index 22f3528aa..53495b08f 100644 --- a/src/openApi/v2/parser/getModel.ts +++ b/src/openApi/v2/parser/getModel.ts @@ -112,7 +112,7 @@ export const getModel = ( if (definition.allOf?.length) { const composition = getModelComposition(openApi, definition, definition.allOf, 'all-of', getModel); - model.export = composition.type; + model.export = composition.export; model.imports.push(...composition.imports); model.properties.push(...composition.properties); model.enums.push(...composition.enums); diff --git a/src/openApi/v2/parser/getModelComposition.ts b/src/openApi/v2/parser/getModelComposition.ts index 6b5e4c305..41a3eed94 100644 --- a/src/openApi/v2/parser/getModelComposition.ts +++ b/src/openApi/v2/parser/getModelComposition.ts @@ -17,9 +17,9 @@ export const getModelComposition = ( getModel: GetModelFn ): ModelComposition => { const composition: ModelComposition = { - type, - imports: [], enums: [], + export: type, + imports: [], properties: [], }; diff --git a/src/openApi/v3/interfaces/OpenApiSchema.d.ts b/src/openApi/v3/interfaces/OpenApiSchema.d.ts index dcc97bb1a..ff1b63b59 100644 --- a/src/openApi/v3/interfaces/OpenApiSchema.d.ts +++ b/src/openApi/v3/interfaces/OpenApiSchema.d.ts @@ -26,7 +26,7 @@ export interface OpenApiSchema extends OpenApiReference, WithEnumExtension { required?: string[]; enum?: (string | number)[]; type?: string | string[]; - const?: string | number | null; + const?: string | number | boolean | null; allOf?: OpenApiSchema[]; oneOf?: OpenApiSchema[]; anyOf?: OpenApiSchema[]; diff --git a/src/openApi/v3/parser/getModel.ts b/src/openApi/v3/parser/getModel.ts index b48d70344..0393cc999 100644 --- a/src/openApi/v3/parser/getModel.ts +++ b/src/openApi/v3/parser/getModel.ts @@ -4,7 +4,7 @@ import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; import { extendEnum } from './extendEnum'; import { getEnum } from './getEnum'; -import { getModelComposition } from './getModelComposition'; +import { findModelComposition, getModelComposition } from './getModelComposition'; import { getModelDefault } from './getModelDefault'; import { getModelProperties } from './getModelProperties'; import { getType } from './getType'; @@ -83,19 +83,24 @@ export const getModel = ( model.imports.push(...arrayItems.imports); model.default = getModelDefault(definition, model); return model; - } else if (definition.items.anyOf && parentDefinition) { - return getModel(openApi, definition.items); - } else { - const arrayItems = getModel(openApi, definition.items); - model.export = 'array'; - model.type = arrayItems.type; - model.base = arrayItems.base; - model.template = arrayItems.template; - model.link = arrayItems; - model.imports.push(...arrayItems.imports); - model.default = getModelDefault(definition, model); - return model; } + + if (definition.items.anyOf && parentDefinition && parentDefinition.type) { + const foundComposition = findModelComposition(parentDefinition); + if (foundComposition && foundComposition.definitions.some(definition => definition.type !== 'array')) { + return getModel(openApi, definition.items); + } + } + + const arrayItems = getModel(openApi, definition.items); + model.export = 'array'; + model.type = arrayItems.type; + model.base = arrayItems.base; + model.template = arrayItems.template; + model.link = arrayItems; + model.imports.push(...arrayItems.imports); + model.default = getModelDefault(definition, model); + return model; } if ( @@ -125,31 +130,16 @@ export const getModel = ( } } - if (definition.oneOf?.length) { - const composition = getModelComposition(openApi, definition, definition.oneOf, 'one-of', getModel); - model.export = composition.type; - model.imports.push(...composition.imports); - model.properties.push(...composition.properties); - model.enums.push(...composition.enums); - return model; - } - - if (definition.anyOf?.length) { - const composition = getModelComposition(openApi, definition, definition.anyOf, 'any-of', getModel); - model.export = composition.type; - model.imports.push(...composition.imports); - model.properties.push(...composition.properties); - model.enums.push(...composition.enums); - return model; - } - - if (definition.allOf?.length) { - const composition = getModelComposition(openApi, definition, definition.allOf, 'all-of', getModel); - model.export = composition.type; - model.imports.push(...composition.imports); - model.properties.push(...composition.properties); - model.enums.push(...composition.enums); - return model; + const foundComposition = findModelComposition(definition); + if (foundComposition) { + const composition = getModelComposition({ + ...foundComposition, + definition, + getModel, + model, + openApi, + }); + return { ...model, ...composition }; } if (definition.type === 'object') { diff --git a/src/openApi/v3/parser/getModelComposition.ts b/src/openApi/v3/parser/getModelComposition.ts index 41dc4d321..c6d4fca72 100644 --- a/src/openApi/v3/parser/getModelComposition.ts +++ b/src/openApi/v3/parser/getModelComposition.ts @@ -9,18 +9,50 @@ import { getRequiredPropertiesFromComposition } from './getRequiredPropertiesFro // Fix for circular dependency export type GetModelFn = typeof getModel; -export const getModelComposition = ( - openApi: OpenApi, - definition: OpenApiSchema, - definitions: OpenApiSchema[], - type: 'one-of' | 'any-of' | 'all-of', - getModel: GetModelFn -): ModelComposition => { +type Composition = { + definitions: OpenApiSchema[]; + type: ModelComposition['export']; +}; + +export const findModelComposition = (definition: OpenApiSchema): Composition | undefined => { + const compositions: ReadonlyArray<{ + definitions: Composition['definitions'] | undefined; + type: Composition['type']; + }> = [ + { + definitions: definition.allOf, + type: 'all-of', + }, + { + definitions: definition.anyOf, + type: 'any-of', + }, + { + definitions: definition.oneOf, + type: 'one-of', + }, + ]; + return compositions.find(composition => composition.definitions?.length) as ReturnType; +}; + +export const getModelComposition = ({ + definition, + definitions, + getModel, + model, + openApi, + type, +}: Composition & { + definition: OpenApiSchema; + getModel: GetModelFn; + model: Model; + openApi: OpenApi; +}): ModelComposition => { const composition: ModelComposition = { - type, - imports: [], - enums: [], - properties: [], + enums: model.enums, + export: type, + imports: model.imports, + properties: model.properties, }; const properties: Model[] = []; diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index d86e8a7ac..10e585282 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -3758,6 +3758,7 @@ export type { ModelWithPattern } from './models/ModelWithPattern'; export type { ModelWithProperties } from './models/ModelWithProperties'; export type { ModelWithReference } from './models/ModelWithReference'; export type { ModelWithString } from './models/ModelWithString'; +export type { NestedAnyOfArraysNullable } from './models/NestedAnyOfArraysNullable'; export type { Pageable } from './models/Pageable'; export type { SimpleBoolean } from './models/SimpleBoolean'; export type { SimpleFile } from './models/SimpleFile'; @@ -3836,6 +3837,7 @@ export { $ModelWithPattern } from './schemas/$ModelWithPattern'; export { $ModelWithProperties } from './schemas/$ModelWithProperties'; export { $ModelWithReference } from './schemas/$ModelWithReference'; export { $ModelWithString } from './schemas/$ModelWithString'; +export { $NestedAnyOfArraysNullable } from './schemas/$NestedAnyOfArraysNullable'; export { $Pageable } from './schemas/$Pageable'; export { $SimpleBoolean } from './schemas/$SimpleBoolean'; export { $SimpleFile } from './schemas/$SimpleFile'; @@ -4201,7 +4203,7 @@ import type { ModelWithDictionary } from './ModelWithDictionary'; * This is a model with nested 'any of' property with a type null */ export type CompositionWithNestedAnyAndTypeNull = { - propA?: ((ModelWithDictionary | null) | (ModelWithArray | null)); + propA?: (Array<(ModelWithDictionary | null)> | Array<(ModelWithArray | null)>); }; " @@ -4218,7 +4220,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?: ((Enum1 | ConstValue) | null); + propA?: (Array<(Enum1 | ConstValue)> | null); }; " @@ -5012,6 +5014,18 @@ export type ModelWithString = { " `; +exports[`v3 should generate: test/generated/v3/models/NestedAnyOfArraysNullable.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type NestedAnyOfArraysNullable = { + nullableArray?: (Array<(string | boolean)> | null); +}; + +" +`; + exports[`v3 should generate: test/generated/v3/models/Pageable.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -5549,19 +5563,25 @@ export const $CompositionWithNestedAnyAndTypeNull = { propA: { type: 'any-of', contains: [{ - type: 'any-of', - contains: [{ - type: 'ModelWithDictionary', - }, { - type: 'null', - }], + type: 'array', + contains: { + type: 'any-of', + contains: [{ + type: 'ModelWithDictionary', + }, { + type: 'null', + }], + }, }, { - type: 'any-of', - contains: [{ - type: 'ModelWithArray', - }, { - type: 'null', - }], + type: 'array', + contains: { + type: 'any-of', + contains: [{ + type: 'ModelWithArray', + }, { + type: 'null', + }], + }, }], }, }, @@ -5580,12 +5600,15 @@ export const $CompositionWithNestedAnyOfAndNull = { propA: { type: 'any-of', contains: [{ - type: 'any-of', - contains: [{ - type: 'Enum1', - }, { - type: 'ConstValue', - }], + type: 'array', + contains: { + type: 'any-of', + contains: [{ + type: 'Enum1', + }, { + type: 'ConstValue', + }], + }, }, { type: 'null', }], @@ -6585,6 +6608,34 @@ export const $ModelWithString = { " `; +exports[`v3 should generate: test/generated/v3/schemas/$NestedAnyOfArraysNullable.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $NestedAnyOfArraysNullable = { + properties: { + nullableArray: { + type: 'any-of', + contains: [{ + type: 'array', + contains: { + type: 'any-of', + contains: [{ + type: 'string', + }, { + type: 'boolean', + }], + }, + }, { + type: 'null', + }], + }, + }, +} as const; +" +`; + exports[`v3 should generate: test/generated/v3/schemas/$Pageable.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ diff --git a/test/spec/v3.json b/test/spec/v3.json index c901a6526..70376f1ab 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -2736,6 +2736,31 @@ "const": "Some string" } } + }, + "NestedAnyOfArraysNullable": { + "properties": { + "nullableArray": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "boolean" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" } } }