From 94fceb2c9512e3f81f5cfa4bce555690d8cf67ee Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 10 Jan 2024 11:28:23 -0500 Subject: [PATCH 01/28] Adding const support --- src/client/interfaces/Model.d.ts | 12 +++++- src/openApi/v3/interfaces/OpenApiSchema.d.ts | 1 + src/openApi/v3/parser/getModel.ts | 9 +++++ test/__snapshots__/index.spec.ts.snap | 41 ++++++++++++++++++++ test/spec/v3.json | 17 ++++++++ 5 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/client/interfaces/Model.d.ts b/src/client/interfaces/Model.d.ts index 5f0318942..90ddb26f4 100644 --- a/src/client/interfaces/Model.d.ts +++ b/src/client/interfaces/Model.d.ts @@ -3,7 +3,17 @@ import type { Schema } from './Schema'; export interface Model extends Schema { name: string; - export: 'reference' | 'generic' | 'enum' | 'array' | 'dictionary' | 'interface' | 'one-of' | 'any-of' | 'all-of'; + export: + | 'reference' + | 'generic' + | 'enum' + | 'array' + | 'dictionary' + | 'interface' + | 'one-of' + | 'any-of' + | 'all-of' + | 'const'; type: string; base: string; template: string | null; diff --git a/src/openApi/v3/interfaces/OpenApiSchema.d.ts b/src/openApi/v3/interfaces/OpenApiSchema.d.ts index a51456f3b..ff1b63b59 100644 --- a/src/openApi/v3/interfaces/OpenApiSchema.d.ts +++ b/src/openApi/v3/interfaces/OpenApiSchema.d.ts @@ -26,6 +26,7 @@ export interface OpenApiSchema extends OpenApiReference, WithEnumExtension { required?: string[]; enum?: (string | number)[]; type?: string | string[]; + 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 9e9c60a98..f9b0b43a3 100644 --- a/src/openApi/v3/parser/getModel.ts +++ b/src/openApi/v3/parser/getModel.ts @@ -192,5 +192,14 @@ export const getModel = ( return model; } + if (definition.const !== undefined) { + model.export = 'const'; + const definitionConst = definition.const; + const modelConst = typeof definitionConst === 'string' ? `"${definitionConst}"` : `${definitionConst}`; + model.type = modelConst; + model.base = modelConst; + return model; + } + return model; }; diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index e1c221495..d28b8d042 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -3714,6 +3714,7 @@ export type { ModelThatExtendsExtends } from './models/ModelThatExtendsExtends'; export type { ModelWithArray } from './models/ModelWithArray'; export type { ModelWithBoolean } from './models/ModelWithBoolean'; export type { ModelWithCircularReference } from './models/ModelWithCircularReference'; +export type { ModelWithConst } from './models/ModelWithConst'; export type { ModelWithDictionary } from './models/ModelWithDictionary'; export type { ModelWithDuplicateImports } from './models/ModelWithDuplicateImports'; export type { ModelWithDuplicateProperties } from './models/ModelWithDuplicateProperties'; @@ -3784,6 +3785,7 @@ export { $ModelThatExtendsExtends } from './schemas/$ModelThatExtendsExtends'; export { $ModelWithArray } from './schemas/$ModelWithArray'; export { $ModelWithBoolean } from './schemas/$ModelWithBoolean'; export { $ModelWithCircularReference } from './schemas/$ModelWithCircularReference'; +export { $ModelWithConst } from './schemas/$ModelWithConst'; export { $ModelWithDictionary } from './schemas/$ModelWithDictionary'; export { $ModelWithDuplicateImports } from './schemas/$ModelWithDuplicateImports'; export { $ModelWithDuplicateProperties } from './schemas/$ModelWithDuplicateProperties'; @@ -4559,6 +4561,21 @@ export type ModelWithCircularReference = { " `; +exports[`v3 should generate: test/generated/v3/models/ModelWithConst.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ModelWithConst = { + string?: "string"; + number?: 0; + boolean?: false; + null?: null; +}; + +" +`; + exports[`v3 should generate: test/generated/v3/models/ModelWithDictionary.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -5870,6 +5887,30 @@ export const $ModelWithCircularReference = { " `; +exports[`v3 should generate: test/generated/v3/schemas/$ModelWithConst.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ModelWithConst = { + properties: { + string: { + type: '"string"', + }, + number: { + type: '0', + }, + boolean: { + type: 'false', + }, + null: { + type: 'null', + }, + }, +} as const; +" +`; + exports[`v3 should generate: test/generated/v3/schemas/$ModelWithDictionary.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 cb590d0b7..a5e14faf1 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -2553,6 +2553,23 @@ "description": "This is a free-form object with additionalProperties: {}.", "type": "object", "additionalProperties": {} + }, + "ModelWithConst": { + "type": "object", + "properties": { + "string": { + "const": "string" + }, + "number": { + "const": 0 + }, + "boolean": { + "const": false + }, + "null": { + "const": null + } + } } } } From e3bc08c79c81b9bcb4b62563c0a88230eae9cb6a Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 10 Jan 2024 11:44:52 -0500 Subject: [PATCH 02/28] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d0b03835d..cb0c7ef41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openapi-typescript-codegen", - "version": "0.26.0", + "version": "0.26.1", "description": "Library that generates Typescript clients based on the OpenAPI specification.", "author": "Ferdi Koomen", "homepage": "https://github.com/ferdikoomen/openapi-typescript-codegen", From 5568964d71fa4c47758876dd99841059a8c6671c Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 10 Jan 2024 13:42:57 -0500 Subject: [PATCH 03/28] Fix some more const --- package.json | 2 +- src/openApi/v3/parser/getModel.ts | 18 +++++++++--------- test/__snapshots__/index.spec.ts.snap | 14 +++++++------- test/spec/v3.json | 11 ++++++----- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index cb0c7ef41..42b4af9ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openapi-typescript-codegen", - "version": "0.26.1", + "version": "0.26.2", "description": "Library that generates Typescript clients based on the OpenAPI specification.", "author": "Ferdi Koomen", "homepage": "https://github.com/ferdikoomen/openapi-typescript-codegen", diff --git a/src/openApi/v3/parser/getModel.ts b/src/openApi/v3/parser/getModel.ts index f9b0b43a3..8488ee00e 100644 --- a/src/openApi/v3/parser/getModel.ts +++ b/src/openApi/v3/parser/getModel.ts @@ -179,6 +179,15 @@ export const getModel = ( } } + if (definition.const !== undefined) { + model.export = 'const'; + const definitionConst = definition.const; + const modelConst = typeof definitionConst === 'string' ? `"${definitionConst}"` : `${definitionConst}`; + model.type = modelConst; + model.base = modelConst; + return model; + } + // If the schema has a type than it can be a basic or generic type. if (definition.type) { const definitionType = getType(definition.type, definition.format); @@ -192,14 +201,5 @@ export const getModel = ( return model; } - if (definition.const !== undefined) { - model.export = 'const'; - const definitionConst = definition.const; - const modelConst = typeof definitionConst === 'string' ? `"${definitionConst}"` : `${definitionConst}`; - model.type = modelConst; - model.base = modelConst; - return model; - } - return model; }; diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index d28b8d042..05c820739 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -4567,10 +4567,10 @@ exports[`v3 should generate: test/generated/v3/models/ModelWithConst.ts 1`] = ` /* tslint:disable */ /* eslint-disable */ export type ModelWithConst = { - string?: "string"; + String?: "String"; number?: 0; - boolean?: false; null?: null; + withType?: "Some string"; }; " @@ -5894,18 +5894,18 @@ exports[`v3 should generate: test/generated/v3/schemas/$ModelWithConst.ts 1`] = /* eslint-disable */ export const $ModelWithConst = { properties: { - string: { - type: '"string"', + String: { + type: '"String"', }, number: { type: '0', }, - boolean: { - type: 'false', - }, null: { type: 'null', }, + withType: { + type: '"Some string"', + }, }, } as const; " diff --git a/test/spec/v3.json b/test/spec/v3.json index a5e14faf1..18495c7d9 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -2557,17 +2557,18 @@ "ModelWithConst": { "type": "object", "properties": { - "string": { - "const": "string" + "String": { + "const": "String" }, "number": { "const": 0 }, - "boolean": { - "const": false - }, "null": { "const": null + }, + "withType": { + "type": "string", + "const": "Some string" } } } From 4a01f7d591de0e18940a483d20481164788f4632 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 10 Jan 2024 16:05:56 -0500 Subject: [PATCH 04/28] Fix nested any-of --- src/openApi/v3/interfaces/OpenApiSchema.d.ts | 2 +- src/openApi/v3/parser/getModel.spec.ts | 92 ++++++++++++++++++++ src/openApi/v3/parser/getModel.ts | 2 + test/__snapshots__/index.spec.ts.snap | 92 ++++++++++++++++++++ test/spec/v3.json | 40 ++++++++- 5 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 src/openApi/v3/parser/getModel.spec.ts diff --git a/src/openApi/v3/interfaces/OpenApiSchema.d.ts b/src/openApi/v3/interfaces/OpenApiSchema.d.ts index ff1b63b59..dcc97bb1a 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 | boolean | null; + const?: string | number | null; allOf?: OpenApiSchema[]; oneOf?: OpenApiSchema[]; anyOf?: OpenApiSchema[]; diff --git a/src/openApi/v3/parser/getModel.spec.ts b/src/openApi/v3/parser/getModel.spec.ts new file mode 100644 index 000000000..efb720a40 --- /dev/null +++ b/src/openApi/v3/parser/getModel.spec.ts @@ -0,0 +1,92 @@ +import { reservedWords } from '../../../utils/reservedWords'; +import { getModel } from './getModel'; +import { getType } from './getType'; + +const openApi = { + openapi: '3.0', + info: { + title: 'dummy', + version: '1.0', + }, + paths: {}, + servers: [ + { + url: 'https://localhost:8080/api', + }, + ], + components: { + schemas: { + Enum1: { + enum: ['Bird', 'Dog'], + type: 'string', + }, + ConstValue: { + type: 'string', + const: 'ConstValue', + }, + CompositionWithAnyOfAndNull: { + description: + "This is a model with one property with a 'any of' relationship where the options are not $ref", + type: 'object', + properties: { + propA: { + anyOf: [ + { + items: { + anyOf: [ + { + $ref: '#/components/schemas/Enum1', + }, + { + $ref: '#/components/schemas/ConstValue', + }, + ], + }, + type: 'array', + }, + { + type: 'null', + }, + ], + }, + }, + }, + CompositionWithAny: { + description: + "This is a model with one property with a 'any of' relationship where the options are not $ref", + type: 'object', + properties: { + propA: { + anyOf: [ + { + $ref: '#/components/schemas/Enum1', + }, + { + $ref: '#/components/schemas/ConstValue', + }, + { + type: 'null', + }, + ], + }, + }, + }, + }, + }, +}; + +describe('getModel', () => { + it('Parses any of', () => { + const definition = openApi.components.schemas.CompositionWithAnyOfAndNull; + const definitionType = getType('CompositionWithAnyOfAndNull'); + const model = getModel(openApi, definition, true, definitionType.base.replace(reservedWords, '_$1')); + expect(model.properties[0].properties.length).toBe(2); + }); + + it('Parses any of 2', () => { + const definition = openApi.components.schemas.CompositionWithAny; + const definitionType = getType('CompositionWithAny'); + const model = getModel(openApi, definition, true, definitionType.base.replace(reservedWords, '_$1')); + expect(model.properties[0].properties.length).toBe(3); + }); +}); diff --git a/src/openApi/v3/parser/getModel.ts b/src/openApi/v3/parser/getModel.ts index 8488ee00e..ad55d0fc7 100644 --- a/src/openApi/v3/parser/getModel.ts +++ b/src/openApi/v3/parser/getModel.ts @@ -82,6 +82,8 @@ export const getModel = ( model.imports.push(...arrayItems.imports); model.default = getModelDefault(definition, model); return model; + } else if (definition.items.anyOf) { + return getModel(openApi, definition.items); } else { const arrayItems = getModel(openApi, definition.items); model.export = 'array'; diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index 05c820739..c225e9f6f 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -3686,6 +3686,7 @@ export type { CompositionWithAllOfAndNullable } from './models/CompositionWithAl export type { CompositionWithAnyOf } from './models/CompositionWithAnyOf'; export type { CompositionWithAnyOfAndNullable } from './models/CompositionWithAnyOfAndNullable'; export type { CompositionWithAnyOfAnonymous } from './models/CompositionWithAnyOfAnonymous'; +export type { CompositionWithNestedAnyOfAndNull } from './models/CompositionWithNestedAnyOfAndNull'; export type { CompositionWithOneOf } from './models/CompositionWithOneOf'; export type { CompositionWithOneOfAndComplexArrayDictionary } from './models/CompositionWithOneOfAndComplexArrayDictionary'; export type { CompositionWithOneOfAndNullable } from './models/CompositionWithOneOfAndNullable'; @@ -3693,12 +3694,14 @@ export type { CompositionWithOneOfAndSimpleArrayDictionary } from './models/Comp export type { CompositionWithOneOfAndSimpleDictionary } from './models/CompositionWithOneOfAndSimpleDictionary'; export type { CompositionWithOneOfAnonymous } from './models/CompositionWithOneOfAnonymous'; export type { CompositionWithOneOfDiscriminator } from './models/CompositionWithOneOfDiscriminator'; +export type { ConstValue } from './models/ConstValue'; export type { DeprecatedModel } from './models/DeprecatedModel'; export type { DictionaryWithArray } from './models/DictionaryWithArray'; export type { DictionaryWithDictionary } from './models/DictionaryWithDictionary'; export type { DictionaryWithProperties } from './models/DictionaryWithProperties'; export type { DictionaryWithReference } from './models/DictionaryWithReference'; export type { DictionaryWithString } from './models/DictionaryWithString'; +export { Enum1 } from './models/Enum1'; export type { EnumFromDescription } from './models/EnumFromDescription'; export { EnumWithExtensions } from './models/EnumWithExtensions'; export { EnumWithNumbers } from './models/EnumWithNumbers'; @@ -3757,6 +3760,7 @@ export { $CompositionWithAllOfAndNullable } from './schemas/$CompositionWithAllO export { $CompositionWithAnyOf } from './schemas/$CompositionWithAnyOf'; export { $CompositionWithAnyOfAndNullable } from './schemas/$CompositionWithAnyOfAndNullable'; export { $CompositionWithAnyOfAnonymous } from './schemas/$CompositionWithAnyOfAnonymous'; +export { $CompositionWithNestedAnyOfAndNull } from './schemas/$CompositionWithNestedAnyOfAndNull'; export { $CompositionWithOneOf } from './schemas/$CompositionWithOneOf'; export { $CompositionWithOneOfAndComplexArrayDictionary } from './schemas/$CompositionWithOneOfAndComplexArrayDictionary'; export { $CompositionWithOneOfAndNullable } from './schemas/$CompositionWithOneOfAndNullable'; @@ -3764,12 +3768,14 @@ export { $CompositionWithOneOfAndSimpleArrayDictionary } from './schemas/$Compos export { $CompositionWithOneOfAndSimpleDictionary } from './schemas/$CompositionWithOneOfAndSimpleDictionary'; export { $CompositionWithOneOfAnonymous } from './schemas/$CompositionWithOneOfAnonymous'; export { $CompositionWithOneOfDiscriminator } from './schemas/$CompositionWithOneOfDiscriminator'; +export { $ConstValue } from './schemas/$ConstValue'; export { $DeprecatedModel } from './schemas/$DeprecatedModel'; export { $DictionaryWithArray } from './schemas/$DictionaryWithArray'; export { $DictionaryWithDictionary } from './schemas/$DictionaryWithDictionary'; export { $DictionaryWithProperties } from './schemas/$DictionaryWithProperties'; export { $DictionaryWithReference } from './schemas/$DictionaryWithReference'; export { $DictionaryWithString } from './schemas/$DictionaryWithString'; +export { $Enum1 } from './schemas/$Enum1'; export { $EnumFromDescription } from './schemas/$EnumFromDescription'; export { $EnumWithExtensions } from './schemas/$EnumWithExtensions'; export { $EnumWithNumbers } from './schemas/$EnumWithNumbers'; @@ -4107,6 +4113,23 @@ export type CompositionWithAnyOfAnonymous = { " `; +exports[`v3 should generate: test/generated/v3/models/CompositionWithNestedAnyOfAndNull.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ConstValue } from './ConstValue'; +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); +}; + +" +`; + exports[`v3 should generate: test/generated/v3/models/CompositionWithOneOf.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -4223,6 +4246,15 @@ export type CompositionWithOneOfDiscriminator = (ModelCircle | ModelSquare); " `; +exports[`v3 should generate: test/generated/v3/models/ConstValue.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ConstValue = "ConstValue"; +" +`; + exports[`v3 should generate: test/generated/v3/models/DeprecatedModel.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -4308,6 +4340,18 @@ export type DictionaryWithString = Record; " `; +exports[`v3 should generate: test/generated/v3/models/Enum1.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export enum Enum1 { + BIRD = 'Bird', + DOG = 'Dog', +} +" +`; + exports[`v3 should generate: test/generated/v3/models/EnumFromDescription.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -5320,6 +5364,32 @@ export const $CompositionWithAnyOfAnonymous = { " `; +exports[`v3 should generate: test/generated/v3/schemas/$CompositionWithNestedAnyOfAndNull.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CompositionWithNestedAnyOfAndNull = { + description: \`This is a model with one property with a 'any of' relationship where the options are not $ref\`, + properties: { + propA: { + type: 'any-of', + contains: [{ + type: 'any-of', + contains: [{ + type: 'Enum1', + }, { + type: 'ConstValue', + }], + }, { + type: 'null', + }], + }, + }, +} as const; +" +`; + exports[`v3 should generate: test/generated/v3/schemas/$CompositionWithOneOf.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -5505,6 +5575,17 @@ export const $CompositionWithOneOfDiscriminator = { " `; +exports[`v3 should generate: test/generated/v3/schemas/$ConstValue.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ConstValue = { + type: '"ConstValue"', +} as const; +" +`; + exports[`v3 should generate: test/generated/v3/schemas/$DeprecatedModel.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -5605,6 +5686,17 @@ export const $DictionaryWithString = { " `; +exports[`v3 should generate: test/generated/v3/schemas/$Enum1.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $Enum1 = { + type: 'Enum', +} as const; +" +`; + exports[`v3 should generate: test/generated/v3/schemas/$EnumFromDescription.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 18495c7d9..3ce3175b2 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -2071,6 +2071,44 @@ } } }, + "Enum1":{ + "enum": [ + "Bird", + "Dog" + ], + "type": "string" + }, + "ConstValue": { + "type": "string", + "const": "ConstValue" + }, + "CompositionWithNestedAnyOfAndNull": { + "description": "This is a model with one property with a 'any of' relationship where the options are not $ref", + "type": "object", + "properties": { + "propA": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Enum1" + }, + { + "$ref": "#/components/schemas/ConstValue" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Scopes" + } + } + }, "CompositionWithOneOfAndNullable": { "description": "This is a model with one property with a 'one of' relationship", "type": "object", @@ -2574,4 +2612,4 @@ } } } -} +} \ No newline at end of file From 927cb3572ac312b1638b9d043542106df5e87d73 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 27 Jan 2024 16:58:43 +0000 Subject: [PATCH 05/28] feat(parser): return optional success response on 204 status code --- src/openApi/v2/parser/getOperationResponse.ts | 4 +- src/openApi/v2/parser/getOperationResults.ts | 26 +----------- src/openApi/v3/parser/getOperationResponse.ts | 4 +- src/openApi/v3/parser/getOperationResults.ts | 26 +----------- test/__snapshots__/index.spec.ts.snap | 40 +++++++++++++++++++ test/spec/v2.json | 24 +++++++++++ test/spec/v3.json | 24 +++++++++++ 7 files changed, 96 insertions(+), 52 deletions(-) diff --git a/src/openApi/v2/parser/getOperationResponse.ts b/src/openApi/v2/parser/getOperationResponse.ts index 8f6c3ca56..0c5e2bfa4 100644 --- a/src/openApi/v2/parser/getOperationResponse.ts +++ b/src/openApi/v2/parser/getOperationResponse.ts @@ -18,8 +18,8 @@ export const getOperationResponse = ( code: responseCode, description: response.description || null, export: 'generic', - type: 'any', - base: 'any', + type: responseCode !== 204 ? 'any' : 'void', + base: responseCode !== 204 ? 'any' : 'void', template: null, link: null, isDefinition: false, diff --git a/src/openApi/v2/parser/getOperationResults.ts b/src/openApi/v2/parser/getOperationResults.ts index 9d8111fe8..997c2059c 100644 --- a/src/openApi/v2/parser/getOperationResults.ts +++ b/src/openApi/v2/parser/getOperationResults.ts @@ -12,36 +12,14 @@ const areEqual = (a: Model, b: Model): boolean => { export const getOperationResults = (operationResponses: OperationResponse[]): OperationResponse[] => { const operationResults: OperationResponse[] = []; - // Filter out success response codes, but skip "204 No Content" + // Filter out success response codes operationResponses.forEach(operationResponse => { const { code } = operationResponse; - if (code && code !== 204 && code >= 200 && code < 300) { + if (code && code >= 200 && code < 300) { operationResults.push(operationResponse); } }); - if (!operationResults.length) { - operationResults.push({ - in: 'response', - name: '', - code: 200, - description: '', - export: 'generic', - type: 'void', - base: 'void', - template: null, - link: null, - isDefinition: false, - isReadOnly: false, - isRequired: false, - isNullable: false, - imports: [], - enum: [], - enums: [], - properties: [], - }); - } - return operationResults.filter((operationResult, index, arr) => { return ( arr.findIndex(item => { diff --git a/src/openApi/v3/parser/getOperationResponse.ts b/src/openApi/v3/parser/getOperationResponse.ts index dff19ec13..0fe5620c4 100644 --- a/src/openApi/v3/parser/getOperationResponse.ts +++ b/src/openApi/v3/parser/getOperationResponse.ts @@ -19,8 +19,8 @@ export const getOperationResponse = ( code: responseCode, description: response.description || null, export: 'generic', - type: 'any', - base: 'any', + type: responseCode !== 204 ? 'any' : 'void', + base: responseCode !== 204 ? 'any' : 'void', template: null, link: null, isDefinition: false, diff --git a/src/openApi/v3/parser/getOperationResults.ts b/src/openApi/v3/parser/getOperationResults.ts index 9d8111fe8..997c2059c 100644 --- a/src/openApi/v3/parser/getOperationResults.ts +++ b/src/openApi/v3/parser/getOperationResults.ts @@ -12,36 +12,14 @@ const areEqual = (a: Model, b: Model): boolean => { export const getOperationResults = (operationResponses: OperationResponse[]): OperationResponse[] => { const operationResults: OperationResponse[] = []; - // Filter out success response codes, but skip "204 No Content" + // Filter out success response codes operationResponses.forEach(operationResponse => { const { code } = operationResponse; - if (code && code !== 204 && code >= 200 && code < 300) { + if (code && code >= 200 && code < 300) { operationResults.push(operationResponse); } }); - if (!operationResults.length) { - operationResults.push({ - in: 'response', - name: '', - code: 200, - description: '', - export: 'generic', - type: 'void', - base: 'void', - template: null, - link: null, - isDefinition: false, - isReadOnly: false, - isRequired: false, - isNullable: false, - imports: [], - enum: [], - enums: [], - properties: [], - }); - } - return operationResults.filter((operationResult, index, arr) => { return ( arr.findIndex(item => { diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index e1c221495..8fe9ef9a2 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -2809,6 +2809,16 @@ export class NoContentService { url: '/api/v{api-version}/no-content', }); } + /** + * @returns any Response is a simple number + * @throws ApiError + */ + public static callWithResponseAndNoContentResponse(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v{api-version}/multiple-tags/response-and-no-content', + }); + } } " `; @@ -2913,6 +2923,16 @@ import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class ResponseService { + /** + * @returns any Response is a simple number + * @throws ApiError + */ + public static callWithResponseAndNoContentResponse(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v{api-version}/multiple-tags/response-and-no-content', + }); + } /** * @returns ModelWithString Message for default response * @throws ApiError @@ -6985,6 +7005,16 @@ export class NoContentService { url: '/api/v{api-version}/no-content', }); } + /** + * @returns number Response is a simple number + * @throws ApiError + */ + public static callWithResponseAndNoContentResponse(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v{api-version}/multiple-tags/response-and-no-content', + }); + } } " `; @@ -7174,6 +7204,16 @@ import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class ResponseService { + /** + * @returns number Response is a simple number + * @throws ApiError + */ + public static callWithResponseAndNoContentResponse(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v{api-version}/multiple-tags/response-and-no-content', + }); + } /** * @returns ModelWithString * @throws ApiError diff --git a/test/spec/v2.json b/test/spec/v2.json index e8eb19b51..d020c55f7 100644 --- a/test/spec/v2.json +++ b/test/spec/v2.json @@ -456,6 +456,30 @@ } } }, + "/api/v{api-version}/multiple-tags/response-and-no-content": { + "get": { + "tags": [ + "Response", + "NoContent" + ], + "operationId": "CallWithResponseAndNoContentResponse", + "responses": { + "200": { + "description": "Response is a simple number", + "content": { + "application/json": { + "schema": { + "type": "number" + } + } + } + }, + "204": { + "description": "Success" + } + } + } + }, "/api/v{api-version}/multiple-tags/a": { "get": { "tags": [ diff --git a/test/spec/v3.json b/test/spec/v3.json index cb590d0b7..ec6896d95 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -663,6 +663,30 @@ } } }, + "/api/v{api-version}/multiple-tags/response-and-no-content": { + "get": { + "tags": [ + "Response", + "NoContent" + ], + "operationId": "CallWithResponseAndNoContentResponse", + "responses": { + "200": { + "description": "Response is a simple number", + "content": { + "application/json": { + "schema": { + "type": "number" + } + } + } + }, + "204": { + "description": "Success" + } + } + } + }, "/api/v{api-version}/multiple-tags/a": { "get": { "tags": [ From f80bcb02ea876868bda510a03a1736dc8a696497 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sun, 28 Jan 2024 04:19:27 +0000 Subject: [PATCH 06/28] test(snapshot): fix snapshot --- src/index.ts | 36 +++++++++------------------ test/__snapshots__/index.spec.ts.snap | 36 +++++++++++++++------------ 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/index.ts b/src/index.ts index e63919085..d75c30177 100644 --- a/src/index.ts +++ b/src/index.ts @@ -75,35 +75,24 @@ export const generate = async ({ useOptions, }); + let parser: typeof parseV2 | typeof parseV3; + switch (openApiVersion) { case OpenApiVersion.V2: { - const client = parseV2(openApi); - const clientFinal = postProcessClient(client); - if (!write) break; - await writeClient( - clientFinal, - templates, - output, - httpClient, - useOptions, - useUnionTypes, - exportCore, - exportServices, - exportModels, - exportSchemas, - indent, - postfixServices, - postfixModels, - clientName, - request - ); + parser = parseV2; break; } case OpenApiVersion.V3: { - const client = parseV3(openApi); - const clientFinal = postProcessClient(client); - if (!write) break; + parser = parseV3; + break; + } + } + + if (parser) { + const client = parser(openApi); + const clientFinal = postProcessClient(client); + if (write) { await writeClient( clientFinal, templates, @@ -121,7 +110,6 @@ export const generate = async ({ clientName, request ); - break; } } }; diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index 431576822..ec4f8de9e 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -2711,7 +2711,7 @@ import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class MultipleTags1Service { /** - * @returns void + * @returns void Success * @throws ApiError */ public static dummyA(): CancelablePromise { @@ -2721,7 +2721,7 @@ export class MultipleTags1Service { }); } /** - * @returns void + * @returns void Success * @throws ApiError */ public static dummyB(): CancelablePromise { @@ -2744,7 +2744,7 @@ import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class MultipleTags2Service { /** - * @returns void + * @returns void Success * @throws ApiError */ public static dummyA(): CancelablePromise { @@ -2754,7 +2754,7 @@ export class MultipleTags2Service { }); } /** - * @returns void + * @returns void Success * @throws ApiError */ public static dummyB(): CancelablePromise { @@ -2777,7 +2777,7 @@ import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class MultipleTags3Service { /** - * @returns void + * @returns void Success * @throws ApiError */ public static dummyB(): CancelablePromise { @@ -2800,7 +2800,7 @@ import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class NoContentService { /** - * @returns void + * @returns void Success * @throws ApiError */ public static callWithNoContentResponse(): CancelablePromise { @@ -2811,9 +2811,10 @@ export class NoContentService { } /** * @returns any Response is a simple number + * @returns void Success * @throws ApiError */ - public static callWithResponseAndNoContentResponse(): CancelablePromise { + public static callWithResponseAndNoContentResponse(): CancelablePromise { return __request(OpenAPI, { method: 'GET', url: '/api/v{api-version}/multiple-tags/response-and-no-content', @@ -2925,9 +2926,10 @@ import { request as __request } from '../core/request'; export class ResponseService { /** * @returns any Response is a simple number + * @returns void Success * @throws ApiError */ - public static callWithResponseAndNoContentResponse(): CancelablePromise { + public static callWithResponseAndNoContentResponse(): CancelablePromise { return __request(OpenAPI, { method: 'GET', url: '/api/v{api-version}/multiple-tags/response-and-no-content', @@ -7040,7 +7042,7 @@ import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class MultipleTags1Service { /** - * @returns void + * @returns void Success * @throws ApiError */ public static dummyA(): CancelablePromise { @@ -7050,7 +7052,7 @@ export class MultipleTags1Service { }); } /** - * @returns void + * @returns void Success * @throws ApiError */ public static dummyB(): CancelablePromise { @@ -7073,7 +7075,7 @@ import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class MultipleTags2Service { /** - * @returns void + * @returns void Success * @throws ApiError */ public static dummyA(): CancelablePromise { @@ -7083,7 +7085,7 @@ export class MultipleTags2Service { }); } /** - * @returns void + * @returns void Success * @throws ApiError */ public static dummyB(): CancelablePromise { @@ -7106,7 +7108,7 @@ import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class MultipleTags3Service { /** - * @returns void + * @returns void Success * @throws ApiError */ public static dummyB(): CancelablePromise { @@ -7129,7 +7131,7 @@ import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class NoContentService { /** - * @returns void + * @returns void Success * @throws ApiError */ public static callWithNoContentResponse(): CancelablePromise { @@ -7140,9 +7142,10 @@ export class NoContentService { } /** * @returns number Response is a simple number + * @returns void Success * @throws ApiError */ - public static callWithResponseAndNoContentResponse(): CancelablePromise { + public static callWithResponseAndNoContentResponse(): CancelablePromise { return __request(OpenAPI, { method: 'GET', url: '/api/v{api-version}/multiple-tags/response-and-no-content', @@ -7339,9 +7342,10 @@ import { request as __request } from '../core/request'; export class ResponseService { /** * @returns number Response is a simple number + * @returns void Success * @throws ApiError */ - public static callWithResponseAndNoContentResponse(): CancelablePromise { + public static callWithResponseAndNoContentResponse(): CancelablePromise { return __request(OpenAPI, { method: 'GET', url: '/api/v{api-version}/multiple-tags/response-and-no-content', From 69fbc9400710ccd868c40554da2cdd59a41b10d2 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sun, 28 Jan 2024 05:13:27 +0000 Subject: [PATCH 07/28] feat(client): support regexp to select services to export --- README.md | 2 +- bin/index.js | 8 +++++++- bin/index.spec.js | 14 ++++++++++++++ src/index.ts | 2 +- src/utils/writeClient.ts | 7 ++++++- src/utils/writeClientIndex.ts | 2 +- types/index.d.ts | 2 +- 7 files changed, 31 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 83f0ae233..63f268332 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ $ openapi --help --useOptions Use options instead of arguments --useUnionTypes Use union types instead of enums --exportCore Write core files to disk (default: true) - --exportServices Write services to disk (default: true) + --exportServices Write services to disk [true, false, regexp] (default: true) --exportModels Write models to disk (default: true) --exportSchemas Write schemas to disk (default: false) --indent Indentation options [4, 2, tab] (default: "4") diff --git a/bin/index.js b/bin/index.js index 32f2fecbc..edb7879f1 100755 --- a/bin/index.js +++ b/bin/index.js @@ -30,6 +30,12 @@ const params = program const OpenAPI = require(path.resolve(__dirname, '../dist/index.js')); if (OpenAPI) { + let exportServices; + try { + exportServices = JSON.parse(params.exportServices) === true; + } catch (error) { + exportServices = params.exportServices; + } OpenAPI.generate({ input: params.input, output: params.output, @@ -38,7 +44,7 @@ if (OpenAPI) { useOptions: params.useOptions, useUnionTypes: params.useUnionTypes, exportCore: JSON.parse(params.exportCore) === true, - exportServices: JSON.parse(params.exportServices) === true, + exportServices, exportModels: JSON.parse(params.exportModels) === true, exportSchemas: JSON.parse(params.exportSchemas) === true, indent: params.indent, diff --git a/bin/index.spec.js b/bin/index.spec.js index 6030c07c8..e9dadabef 100755 --- a/bin/index.spec.js +++ b/bin/index.spec.js @@ -43,6 +43,20 @@ describe('bin', () => { expect(result.stderr.toString()).toBe(''); }); + it('it should support regexp in exportSchemas', async () => { + const result = crossSpawn.sync('node', [ + './bin/index.js', + '--input', + './test/spec/v3.json', + '--output', + './test/generated/bin', + '--exportServices', + '^(Simple|Types)', + ]); + expect(result.stdout.toString()).toBe(''); + expect(result.stderr.toString()).toBe(''); + }); + it('it should throw error without params', async () => { const result = crossSpawn.sync('node', ['./bin/index.js']); expect(result.stdout.toString()).toBe(''); diff --git a/src/index.ts b/src/index.ts index d75c30177..3763ed67d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ export type Options = { useOptions?: boolean; useUnionTypes?: boolean; exportCore?: boolean; - exportServices?: boolean; + exportServices?: boolean | string; exportModels?: boolean; exportSchemas?: boolean; indent?: Indent; diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index cea2f3d88..2f55b0e05 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -41,7 +41,7 @@ export const writeClient = async ( useOptions: boolean, useUnionTypes: boolean, exportCore: boolean, - exportServices: boolean, + exportServices: boolean | string, exportModels: boolean, exportSchemas: boolean, indent: Indent, @@ -60,6 +60,11 @@ export const writeClient = async ( throw new Error(`Output folder is not a subdirectory of the current working directory`); } + if (typeof exportServices === 'string') { + const regexp = new RegExp(exportServices); + client.services = client.services.filter(service => regexp.test(service.name)); + } + if (exportCore) { await rmdir(outputPathCore); await mkdir(outputPathCore); diff --git a/src/utils/writeClientIndex.ts b/src/utils/writeClientIndex.ts index 5044294d5..72723c0f1 100644 --- a/src/utils/writeClientIndex.ts +++ b/src/utils/writeClientIndex.ts @@ -29,7 +29,7 @@ export const writeClientIndex = async ( outputPath: string, useUnionTypes: boolean, exportCore: boolean, - exportServices: boolean, + exportServices: boolean | string, exportModels: boolean, exportSchemas: boolean, postfixServices: string, diff --git a/types/index.d.ts b/types/index.d.ts index e2b5247e0..f347d74bc 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -20,7 +20,7 @@ export type Options = { useOptions?: boolean; useUnionTypes?: boolean; exportCore?: boolean; - exportServices?: boolean; + exportServices?: boolean | string; exportModels?: boolean; exportSchemas?: boolean; indent?: Indent | '4' | '2' | 'tab'; From f5d8e815f502916aa35c822939b9d7ca762ad0de Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 29 Jan 2024 08:34:05 -0500 Subject: [PATCH 08/28] Adding a unittest action --- .github/workflows/unittest.yml | 20 ++++++++++++++++++++ .gitignore | 1 + package.json | 8 ++++---- 3 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/unittest.yml diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 000000000..2bc8ccaa2 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,20 @@ +name: unittest + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + - name: Cache Modules + uses: actions/cache@v3 + with: + path: "**/node_modules" + key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }} + - run: npm run install + - run: npm run test + diff --git a/.gitignore b/.gitignore index 2b7422568..484212c33 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ test/e2e/generated samples/generated samples/swagger-codegen-cli-v2.jar samples/swagger-codegen-cli-v3.jar +.env diff --git a/package.json b/package.json index ea94ce76e..f75f4fcf8 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "openapi-typescript-codegen", - "version": "0.27.0", + "name": "@canoapbc/openapi-typescript-codegen", + "version": "0.27.2", "description": "Library that generates Typescript clients based on the OpenAPI specification.", "author": "Ferdi Koomen", - "homepage": "https://github.com/ferdikoomen/openapi-typescript-codegen", + "homepage": "https://github.com/CanoaPBC/openapi-typescript-codegen", "repository": { "type": "git", - "url": "git+https://github.com/ferdikoomen/openapi-typescript-codegen.git" + "url": "git+https://github.com/CanoaPBC/openapi-typescript-codegen.git" }, "bugs": { "url": "https://github.com/ferdikoomen/openapi-typescript-codegen/issues" From 43545383f73795f89983f79fbe35f587012fa6b4 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 29 Jan 2024 08:36:02 -0500 Subject: [PATCH 09/28] Fix yaml file --- .github/workflows/unittest.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 2bc8ccaa2..93a3d0267 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -4,17 +4,17 @@ on: [push, pull_request] jobs: test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 20 - - name: Cache Modules - uses: actions/cache@v3 - with: - path: "**/node_modules" - key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }} - - run: npm run install - - run: npm run test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + - name: Cache Modules + uses: actions/cache@v3 + with: + path: "**/node_modules" + key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }} + - run: npm run install + - run: npm run test From df0ab80cab1cc7a13581e945935ec79640fafe58 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 29 Jan 2024 08:36:44 -0500 Subject: [PATCH 10/28] Fix yaml file --- .github/workflows/unittest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 93a3d0267..3cbc52e7d 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -15,6 +15,6 @@ jobs: with: path: "**/node_modules" key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }} - - run: npm run install + - run: npm install - run: npm run test From 664b4fe4f17f001c285a4a5f3771c91d99dde1e5 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 29 Jan 2024 08:39:05 -0500 Subject: [PATCH 11/28] Adjust flow trigger --- .github/workflows/unittest.yml | 2 +- package-lock.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 3cbc52e7d..5b78eb8c3 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -1,6 +1,6 @@ name: unittest -on: [push, pull_request] +on: [pull_request] jobs: test: diff --git a/package-lock.json b/package-lock.json index 8aa47d74d..0369699c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "openapi-typescript-codegen", - "version": "0.27.0", + "name": "@canoapbc/openapi-typescript-codegen", + "version": "0.27.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "openapi-typescript-codegen", - "version": "0.27.0", + "name": "@canoapbc/openapi-typescript-codegen", + "version": "0.27.2", "license": "MIT", "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", From 72094f6490634821654875e8c7ca662d9d3f8c79 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 29 Jan 2024 09:06:25 -0500 Subject: [PATCH 12/28] Introduce a couple of broken schema parsing --- test/__snapshots__/index.spec.ts.snap | 78 +++++++++++++++++++++++++++ test/spec/v3.json | 44 ++++++++++++++- 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index ec4f8de9e..bc9d216b3 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -3690,6 +3690,8 @@ export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; export type { _default } from './models/_default'; +export type { AnyOfAnyAndNull } from './models/AnyOfAnyAndNull'; +export type { AnyOfArrays } from './models/AnyOfArrays'; export type { ArrayWithArray } from './models/ArrayWithArray'; export type { ArrayWithBooleans } from './models/ArrayWithBooleans'; export type { ArrayWithNumbers } from './models/ArrayWithNumbers'; @@ -3764,6 +3766,8 @@ export type { SimpleString } from './models/SimpleString'; export type { SimpleStringWithPattern } from './models/SimpleStringWithPattern'; export { $_default } from './schemas/$_default'; +export { $AnyOfAnyAndNull } from './schemas/$AnyOfAnyAndNull'; +export { $AnyOfArrays } from './schemas/$AnyOfArrays'; export { $ArrayWithArray } from './schemas/$ArrayWithArray'; export { $ArrayWithBooleans } from './schemas/$ArrayWithBooleans'; export { $ArrayWithNumbers } from './schemas/$ArrayWithNumbers'; @@ -3873,6 +3877,34 @@ export type _default = { " `; +exports[`v3 should generate: test/generated/v3/models/AnyOfAnyAndNull.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type AnyOfAnyAndNull = { + data?: any | null; +}; + +" +`; + +exports[`v3 should generate: test/generated/v3/models/AnyOfArrays.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type AnyOfArrays = { + results?: Array<({ + onePro?: boolean; + } | { + anotherProp?: boolean; + })>; +}; + +" +`; + exports[`v3 should generate: test/generated/v3/models/ArrayWithArray.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -5054,6 +5086,52 @@ export const $_default = { " `; +exports[`v3 should generate: test/generated/v3/schemas/$AnyOfAnyAndNull.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $AnyOfAnyAndNull = { + properties: { + data: { + type: 'any-of', + contains: [{ + type: 'null', + }], + }, + }, +} as const; +" +`; + +exports[`v3 should generate: test/generated/v3/schemas/$AnyOfArrays.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $AnyOfArrays = { + properties: { + results: { + type: 'any-of', + contains: [{ + properties: { + onePro: { + type: 'boolean', + }, + }, + }, { + properties: { + anotherProp: { + type: 'boolean', + }, + }, + }], + }, + }, +} as const; +" +`; + exports[`v3 should generate: test/generated/v3/schemas/$ArrayWithArray.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 e11af1eb2..2528e3120 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -2095,7 +2095,7 @@ } } }, - "Enum1":{ + "Enum1": { "enum": [ "Bird", "Dog" @@ -2290,6 +2290,48 @@ } } }, + "AnyOfAnyAndNull": { + "type": "object", + "properties": { + "data": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + } + } + }, + "AnyOfArrays": { + "type": "object", + "properties": { + "results": { + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "oneProp": { + "type": "boolean" + } + } + }, + { + "type": "object", + "properties": { + "anotherProp": { + "type": "boolean" + } + } + } + ] + }, + "type": "array", + "title": "Results" + } + } + }, "CompositionBaseModel": { "description": "This is a base model with two simple optional properties", "type": "object", From d12d999f7090422687a46f6e71cf664f1ee264e5 Mon Sep 17 00:00:00 2001 From: Lubos Date: Mon, 29 Jan 2024 16:01:00 +0000 Subject: [PATCH 13/28] test(ci): match steps from CircleCI --- .github/workflows/unittest.yml | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 5b78eb8c3..88d0fde35 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -6,15 +6,32 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - name: Checkout + uses: actions/checkout@v4.1.1 + + - name: Setup Node environment + uses: actions/setup-node@v4.0.1 with: node-version: 20 + - name: Cache Modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: "**/node_modules" key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }} - - run: npm install - - run: npm run test + + - name: Install dependencies + run: npm install + + - name: Build library + run: npm run release + + - name: Run unit tests + run: npm run test + + # - name: Run e2e tests + # run: npm run test:e2e + + # - name: Submit to Codecov + # run: npm run codecov From 4174eb76c5d17fca078b916b9bd7ab634a729408 Mon Sep 17 00:00:00 2001 From: Lubos Date: Mon, 29 Jan 2024 16:42:40 +0000 Subject: [PATCH 14/28] chore(model): add another test case --- src/openApi/v3/parser/getModelComposition.ts | 4 +- test/__snapshots__/index.spec.ts.snap | 66 ++++++++++++++++++-- test/spec/v3.json | 46 ++++++++++++-- 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/src/openApi/v3/parser/getModelComposition.ts b/src/openApi/v3/parser/getModelComposition.ts index 2c27d1815..afbd34087 100644 --- a/src/openApi/v3/parser/getModelComposition.ts +++ b/src/openApi/v3/parser/getModelComposition.ts @@ -30,9 +30,11 @@ export const getModelComposition = ( .filter(model => { const hasProperties = model.properties.length; const hasEnums = model.enums.length; + const hasLink = typeof model.link !== 'undefined' && model.link !== null; + console.log(model.name, model) const isObject = model.type === 'any'; const isDictionary = model.export === 'dictionary'; - const isEmpty = isObject && !hasProperties && !hasEnums; + const isEmpty = isObject && !hasProperties && !hasEnums && !hasLink; return !isEmpty || isDictionary; }) .forEach(model => { diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index bc9d216b3..281363bd5 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -3710,6 +3710,7 @@ export type { CompositionWithAllOfAndNullable } from './models/CompositionWithAl export type { CompositionWithAnyOf } from './models/CompositionWithAnyOf'; export type { CompositionWithAnyOfAndNullable } from './models/CompositionWithAnyOfAndNullable'; export type { CompositionWithAnyOfAnonymous } from './models/CompositionWithAnyOfAnonymous'; +export type { CompositionWithNestedAnyAndTypeNull } from './models/CompositionWithNestedAnyAndTypeNull'; export type { CompositionWithNestedAnyOfAndNull } from './models/CompositionWithNestedAnyOfAndNull'; export type { CompositionWithOneOf } from './models/CompositionWithOneOf'; export type { CompositionWithOneOfAndComplexArrayDictionary } from './models/CompositionWithOneOfAndComplexArrayDictionary'; @@ -3786,6 +3787,7 @@ export { $CompositionWithAllOfAndNullable } from './schemas/$CompositionWithAllO export { $CompositionWithAnyOf } from './schemas/$CompositionWithAnyOf'; export { $CompositionWithAnyOfAndNullable } from './schemas/$CompositionWithAnyOfAndNullable'; export { $CompositionWithAnyOfAnonymous } from './schemas/$CompositionWithAnyOfAnonymous'; +export { $CompositionWithNestedAnyAndTypeNull } from './schemas/$CompositionWithNestedAnyAndTypeNull'; export { $CompositionWithNestedAnyOfAndNull } from './schemas/$CompositionWithNestedAnyOfAndNull'; export { $CompositionWithOneOf } from './schemas/$CompositionWithOneOf'; export { $CompositionWithOneOfAndComplexArrayDictionary } from './schemas/$CompositionWithOneOfAndComplexArrayDictionary'; @@ -3894,11 +3896,14 @@ exports[`v3 should generate: test/generated/v3/models/AnyOfArrays.ts 1`] = ` /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +/** + * This is a simple array with any of properties + */ export type AnyOfArrays = { results?: Array<({ - onePro?: boolean; + foo?: string; } | { - anotherProp?: boolean; + bar?: string; })>; }; @@ -4167,6 +4172,23 @@ export type CompositionWithAnyOfAnonymous = { " `; +exports[`v3 should generate: test/generated/v3/models/CompositionWithNestedAnyAndTypeNull.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ModelWithArray } from './ModelWithArray'; +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)); +}; + +" +`; + exports[`v3 should generate: test/generated/v3/models/CompositionWithNestedAnyOfAndNull.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -5110,19 +5132,20 @@ exports[`v3 should generate: test/generated/v3/schemas/$AnyOfArrays.ts 1`] = ` /* tslint:disable */ /* eslint-disable */ export const $AnyOfArrays = { + description: \`This is a simple array with any of properties\`, properties: { results: { type: 'any-of', contains: [{ properties: { - onePro: { - type: 'boolean', + foo: { + type: 'string', }, }, }, { properties: { - anotherProp: { - type: 'boolean', + bar: { + type: 'string', }, }, }], @@ -5464,6 +5487,37 @@ export const $CompositionWithAnyOfAnonymous = { " `; +exports[`v3 should generate: test/generated/v3/schemas/$CompositionWithNestedAnyAndTypeNull.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CompositionWithNestedAnyAndTypeNull = { + description: \`This is a model with nested 'any of' property with a type null\`, + properties: { + propA: { + type: 'any-of', + contains: [{ + type: 'any-of', + contains: [{ + type: 'ModelWithDictionary', + }, { + type: 'null', + }], + }, { + type: 'any-of', + contains: [{ + type: 'ModelWithArray', + }, { + type: 'null', + }], + }], + }, + }, +} as const; +" +`; + exports[`v3 should generate: test/generated/v3/schemas/$CompositionWithNestedAnyOfAndNull.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 2528e3120..95c98243d 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -2095,6 +2095,43 @@ } } }, + "CompositionWithNestedAnyAndTypeNull": { + "description": "This is a model with nested 'any of' property with a type null", + "type": "object", + "properties": { + "propA": { + "type": "object", + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelWithDictionary" + }, + { + "type": "null" + } + ] + }, + "type": "array" + }, + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelWithArray" + }, + { + "type": "null" + } + ] + }, + "type": "array" + } + ] + } + } + }, "Enum1": { "enum": [ "Bird", @@ -2304,6 +2341,7 @@ } }, "AnyOfArrays": { + "description": "This is a simple array with any of properties", "type": "object", "properties": { "results": { @@ -2312,16 +2350,16 @@ { "type": "object", "properties": { - "oneProp": { - "type": "boolean" + "foo": { + "type": "string" } } }, { "type": "object", "properties": { - "anotherProp": { - "type": "boolean" + "bar": { + "type": "string" } } } From b0a2db3b3230b7dddc94e959d151c713d17d378c Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 29 Jan 2024 11:58:40 -0500 Subject: [PATCH 15/28] Change package name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f75f4fcf8..dcd40c7a2 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@canoapbc/openapi-typescript-codegen", + "name": "@nicolas-chaulet/openapi-typescript-codegen", "version": "0.27.2", "description": "Library that generates Typescript clients based on the OpenAPI specification.", "author": "Ferdi Koomen", From 8a4fad3f0865f92200630d68f33ab12ba62c3e57 Mon Sep 17 00:00:00 2001 From: Lubos Date: Tue, 30 Jan 2024 03:54:39 +0000 Subject: [PATCH 16/28] fix(client): support regexp to select models to export --- README.md | 2 +- bin/index.js | 14 ++++++++------ bin/index.spec.js | 4 +++- src/index.ts | 2 +- src/utils/writeClient.ts | 7 ++++++- src/utils/writeClientIndex.ts | 2 +- types/index.d.ts | 2 +- 7 files changed, 21 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 63f268332..b0d3ad873 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ $ openapi --help --useUnionTypes Use union types instead of enums --exportCore Write core files to disk (default: true) --exportServices Write services to disk [true, false, regexp] (default: true) - --exportModels Write models to disk (default: true) + --exportModels Write models to disk [true, false, regexp] (default: true) --exportSchemas Write schemas to disk (default: false) --indent Indentation options [4, 2, tab] (default: "4") --postfixServices Service name postfix (default: "Service") diff --git a/bin/index.js b/bin/index.js index edb7879f1..36664e351 100755 --- a/bin/index.js +++ b/bin/index.js @@ -29,13 +29,15 @@ const params = program const OpenAPI = require(path.resolve(__dirname, '../dist/index.js')); -if (OpenAPI) { - let exportServices; +const parseBooleanOrString = value => { try { - exportServices = JSON.parse(params.exportServices) === true; + return JSON.parse(value) === true; } catch (error) { - exportServices = params.exportServices; + return value; } +}; + +if (OpenAPI) { OpenAPI.generate({ input: params.input, output: params.output, @@ -44,8 +46,8 @@ if (OpenAPI) { useOptions: params.useOptions, useUnionTypes: params.useUnionTypes, exportCore: JSON.parse(params.exportCore) === true, - exportServices, - exportModels: JSON.parse(params.exportModels) === true, + exportServices: parseBooleanOrString(params.exportServices), + exportModels: parseBooleanOrString(params.exportModels), exportSchemas: JSON.parse(params.exportSchemas) === true, indent: params.indent, postfixServices: params.postfixServices, diff --git a/bin/index.spec.js b/bin/index.spec.js index e9dadabef..ff1cff75a 100755 --- a/bin/index.spec.js +++ b/bin/index.spec.js @@ -43,7 +43,7 @@ describe('bin', () => { expect(result.stderr.toString()).toBe(''); }); - it('it should support regexp in exportSchemas', async () => { + it('it should support regexp params', async () => { const result = crossSpawn.sync('node', [ './bin/index.js', '--input', @@ -52,6 +52,8 @@ describe('bin', () => { './test/generated/bin', '--exportServices', '^(Simple|Types)', + '--exportModels', + '^(Simple|Types)', ]); expect(result.stdout.toString()).toBe(''); expect(result.stderr.toString()).toBe(''); diff --git a/src/index.ts b/src/index.ts index 3763ed67d..749a2cfb2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ export type Options = { useUnionTypes?: boolean; exportCore?: boolean; exportServices?: boolean | string; - exportModels?: boolean; + exportModels?: boolean | string; exportSchemas?: boolean; indent?: Indent; postfixServices?: string; diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index 2f55b0e05..6cc491ecd 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -42,7 +42,7 @@ export const writeClient = async ( useUnionTypes: boolean, exportCore: boolean, exportServices: boolean | string, - exportModels: boolean, + exportModels: boolean | string, exportSchemas: boolean, indent: Indent, postfixServices: string, @@ -65,6 +65,11 @@ export const writeClient = async ( client.services = client.services.filter(service => regexp.test(service.name)); } + if (typeof exportModels === 'string') { + const regexp = new RegExp(exportModels); + client.models = client.models.filter(model => regexp.test(model.name)); + } + if (exportCore) { await rmdir(outputPathCore); await mkdir(outputPathCore); diff --git a/src/utils/writeClientIndex.ts b/src/utils/writeClientIndex.ts index 72723c0f1..5f0ced589 100644 --- a/src/utils/writeClientIndex.ts +++ b/src/utils/writeClientIndex.ts @@ -30,7 +30,7 @@ export const writeClientIndex = async ( useUnionTypes: boolean, exportCore: boolean, exportServices: boolean | string, - exportModels: boolean, + exportModels: boolean | string, exportSchemas: boolean, postfixServices: string, postfixModels: string, diff --git a/types/index.d.ts b/types/index.d.ts index f347d74bc..127ec7af9 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -21,7 +21,7 @@ export type Options = { useUnionTypes?: boolean; exportCore?: boolean; exportServices?: boolean | string; - exportModels?: boolean; + exportModels?: boolean | string; exportSchemas?: boolean; indent?: Indent | '4' | '2' | 'tab'; postfixServices?: string; From f35bb1275b916d7b9dbba2a22fb3be59d016d9f1 Mon Sep 17 00:00:00 2001 From: Lubos Date: Thu, 1 Feb 2024 23:46:31 +0800 Subject: [PATCH 17/28] fix(any-of): handle more cases --- src/openApi/v3/parser/getModel.ts | 5 +- src/openApi/v3/parser/getModelComposition.ts | 12 +- test/__snapshots__/index.spec.ts.snap | 77 ++++++++++--- test/spec/v3.json | 109 +++++++++++-------- 4 files changed, 134 insertions(+), 69 deletions(-) diff --git a/src/openApi/v3/parser/getModel.ts b/src/openApi/v3/parser/getModel.ts index ad55d0fc7..b48d70344 100644 --- a/src/openApi/v3/parser/getModel.ts +++ b/src/openApi/v3/parser/getModel.ts @@ -13,7 +13,8 @@ export const getModel = ( openApi: OpenApi, definition: OpenApiSchema, isDefinition: boolean = false, - name: string = '' + name: string = '', + parentDefinition: OpenApiSchema | null = null ): Model => { const model: Model = { name, @@ -82,7 +83,7 @@ export const getModel = ( model.imports.push(...arrayItems.imports); model.default = getModelDefault(definition, model); return model; - } else if (definition.items.anyOf) { + } else if (definition.items.anyOf && parentDefinition) { return getModel(openApi, definition.items); } else { const arrayItems = getModel(openApi, definition.items); diff --git a/src/openApi/v3/parser/getModelComposition.ts b/src/openApi/v3/parser/getModelComposition.ts index afbd34087..41dc4d321 100644 --- a/src/openApi/v3/parser/getModelComposition.ts +++ b/src/openApi/v3/parser/getModelComposition.ts @@ -26,17 +26,7 @@ export const getModelComposition = ( const properties: Model[] = []; definitions - .map(definition => getModel(openApi, definition)) - .filter(model => { - const hasProperties = model.properties.length; - const hasEnums = model.enums.length; - const hasLink = typeof model.link !== 'undefined' && model.link !== null; - console.log(model.name, model) - const isObject = model.type === 'any'; - const isDictionary = model.export === 'dictionary'; - const isEmpty = isObject && !hasProperties && !hasEnums && !hasLink; - return !isEmpty || isDictionary; - }) + .map(def => getModel(openApi, def, undefined, undefined, definition)) .forEach(model => { composition.imports.push(...model.imports); composition.enums.push(...model.enums); diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index 281363bd5..d86e8a7ac 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -3692,6 +3692,7 @@ export type { OpenAPIConfig } from './core/OpenAPI'; export type { _default } from './models/_default'; export type { AnyOfAnyAndNull } from './models/AnyOfAnyAndNull'; export type { AnyOfArrays } from './models/AnyOfArrays'; +export type { ArrayWithAnyOfProperties } from './models/ArrayWithAnyOfProperties'; export type { ArrayWithArray } from './models/ArrayWithArray'; export type { ArrayWithBooleans } from './models/ArrayWithBooleans'; export type { ArrayWithNumbers } from './models/ArrayWithNumbers'; @@ -3769,6 +3770,7 @@ export type { SimpleStringWithPattern } from './models/SimpleStringWithPattern'; export { $_default } from './schemas/$_default'; export { $AnyOfAnyAndNull } from './schemas/$AnyOfAnyAndNull'; export { $AnyOfArrays } from './schemas/$AnyOfArrays'; +export { $ArrayWithAnyOfProperties } from './schemas/$ArrayWithAnyOfProperties'; export { $ArrayWithArray } from './schemas/$ArrayWithArray'; export { $ArrayWithBooleans } from './schemas/$ArrayWithBooleans'; export { $ArrayWithNumbers } from './schemas/$ArrayWithNumbers'; @@ -3885,7 +3887,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); }; " @@ -3910,6 +3912,22 @@ export type AnyOfArrays = { " `; +exports[`v3 should generate: test/generated/v3/models/ArrayWithAnyOfProperties.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * This is a simple array with any of properties + */ +export type ArrayWithAnyOfProperties = Array<({ + foo?: string; +} | { + bar?: string; +})>; +" +`; + exports[`v3 should generate: test/generated/v3/models/ArrayWithArray.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -5118,6 +5136,9 @@ export const $AnyOfAnyAndNull = { data: { type: 'any-of', contains: [{ + properties: { + }, + }, { type: 'null', }], }, @@ -5135,26 +5156,56 @@ export const $AnyOfArrays = { description: \`This is a simple array with any of properties\`, properties: { results: { - type: 'any-of', - contains: [{ - properties: { - foo: { - type: 'string', + type: 'array', + contains: { + type: 'any-of', + contains: [{ + properties: { + foo: { + type: 'string', + }, }, - }, - }, { - properties: { - bar: { - type: 'string', + }, { + properties: { + bar: { + type: 'string', + }, }, - }, - }], + }], + }, }, }, } as const; " `; +exports[`v3 should generate: test/generated/v3/schemas/$ArrayWithAnyOfProperties.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ArrayWithAnyOfProperties = { + type: 'array', + contains: { + type: 'any-of', + contains: [{ + properties: { + foo: { + type: 'string', + }, + }, + }, { + properties: { + bar: { + type: 'string', + }, + }, + }], + }, +} as const; +" +`; + exports[`v3 should generate: test/generated/v3/schemas/$ArrayWithArray.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 95c98243d..c901a6526 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -1683,6 +1683,72 @@ } } }, + "ArrayWithAnyOfProperties": { + "description": "This is a simple array with any of properties", + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + }, + { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + ] + } + }, + "AnyOfAnyAndNull": { + "type": "object", + "properties": { + "data": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + } + } + }, + "AnyOfArrays": { + "description": "This is a simple array with any of properties", + "type": "object", + "properties": { + "results": { + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + }, + { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + ] + }, + "type": "array" + } + } + }, "DictionaryWithString": { "description": "This is a string dictionary", "type": "object", @@ -2327,49 +2393,6 @@ } } }, - "AnyOfAnyAndNull": { - "type": "object", - "properties": { - "data": { - "anyOf": [ - {}, - { - "type": "null" - } - ] - } - } - }, - "AnyOfArrays": { - "description": "This is a simple array with any of properties", - "type": "object", - "properties": { - "results": { - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "foo": { - "type": "string" - } - } - }, - { - "type": "object", - "properties": { - "bar": { - "type": "string" - } - } - } - ] - }, - "type": "array", - "title": "Results" - } - } - }, "CompositionBaseModel": { "description": "This is a base model with two simple optional properties", "type": "object", From 059558e8644d5f06cdf7617b2d1800369d70ed85 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 3 Feb 2024 06:36:24 +0800 Subject: [PATCH 18/28] fix(config): support autoformat option flag --- bin/index.js | 2 ++ bin/index.spec.js | 14 ++++++++++++++ src/index.ts | 3 +++ src/utils/writeClient.spec.ts | 1 + src/utils/writeClient.ts | 13 +++++++++++++ test/index.js | 1 + test/index.spec.ts | 2 ++ types/index.d.ts | 1 + 8 files changed, 37 insertions(+) diff --git a/bin/index.js b/bin/index.js index 36664e351..ecc915aa8 100755 --- a/bin/index.js +++ b/bin/index.js @@ -16,6 +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('--exportCore ', 'Write core files to disk', true) .option('--exportServices ', 'Write services to disk', true) .option('--exportModels ', 'Write models to disk', true) @@ -45,6 +46,7 @@ if (OpenAPI) { clientName: params.name, useOptions: params.useOptions, useUnionTypes: params.useUnionTypes, + autoformat: JSON.parse(params.autoformat) === true, exportCore: JSON.parse(params.exportCore) === true, exportServices: parseBooleanOrString(params.exportServices), exportModels: parseBooleanOrString(params.exportModels), diff --git a/bin/index.spec.js b/bin/index.spec.js index ff1cff75a..5d6d86aec 100755 --- a/bin/index.spec.js +++ b/bin/index.spec.js @@ -59,6 +59,20 @@ describe('bin', () => { expect(result.stderr.toString()).toBe(''); }); + it('should autoformat with Prettier', async () => { + const result = crossSpawn.sync('node', [ + './bin/index.js', + '--input', + './test/spec/v3.json', + '--output', + './test/generated/bin', + '--autoformat', + 'true', + ]); + expect(result.stdout.toString()).toBe(''); + expect(result.stderr.toString()).toBe(''); + }); + it('it should throw error without params', async () => { const result = crossSpawn.sync('node', ['./bin/index.js']); expect(result.stdout.toString()).toBe(''); diff --git a/src/index.ts b/src/index.ts index 749a2cfb2..f6b9b27b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ export type Options = { clientName?: string; useOptions?: boolean; useUnionTypes?: boolean; + autoformat?: boolean; exportCore?: boolean; exportServices?: boolean | string; exportModels?: boolean | string; @@ -57,6 +58,7 @@ export const generate = async ({ clientName, useOptions = false, useUnionTypes = false, + autoformat = false, exportCore = true, exportServices = true, exportModels = true, @@ -100,6 +102,7 @@ export const generate = async ({ httpClient, useOptions, useUnionTypes, + autoformat, exportCore, exportServices, exportModels, diff --git a/src/utils/writeClient.spec.ts b/src/utils/writeClient.spec.ts index 3c06a95a5..2bfc3e284 100644 --- a/src/utils/writeClient.spec.ts +++ b/src/utils/writeClient.spec.ts @@ -43,6 +43,7 @@ describe('writeClient', () => { HttpClient.FETCH, false, false, + false, true, true, true, diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index 6cc491ecd..06b1e0b72 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -1,3 +1,5 @@ +import { spawnSync } from 'child_process'; +import { createRequire } from 'module'; import { resolve } from 'path'; import type { Client } from '../client/interfaces/Client'; @@ -40,6 +42,7 @@ export const writeClient = async ( httpClient: HttpClient, useOptions: boolean, useUnionTypes: boolean, + autoformat: boolean, exportCore: boolean, exportServices: boolean | string, exportModels: boolean | string, @@ -125,4 +128,14 @@ export const writeClient = async ( clientName ); } + + if (autoformat) { + const pathPackageJson = resolve(process.cwd(), 'package.json'); + const require = createRequire('/'); + const json = require(pathPackageJson); + const usesPrettier = [json.dependencies, json.devDependencies].some(deps => Boolean(deps.prettier)); + if (usesPrettier) { + spawnSync('prettier', ['--ignore-unknown', '--write', output]); + } + } }; diff --git a/test/index.js b/test/index.js index 6d276c412..f60c9b047 100644 --- a/test/index.js +++ b/test/index.js @@ -10,6 +10,7 @@ const generate = async (input, output) => { httpClient: OpenAPI.HttpClient.FETCH, useOptions: true, useUnionTypes: false, + autoformat: false, exportCore: true, exportSchemas: true, exportModels: true, diff --git a/test/index.spec.ts b/test/index.spec.ts index 78a0197d5..118a293ad 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -11,6 +11,7 @@ describe('v2', () => { httpClient: HttpClient.FETCH, useOptions: false, useUnionTypes: false, + autoformat: false, exportCore: true, exportSchemas: true, exportModels: true, @@ -32,6 +33,7 @@ describe('v3', () => { httpClient: HttpClient.FETCH, useOptions: false, useUnionTypes: false, + autoformat: false, exportCore: true, exportSchemas: true, exportModels: true, diff --git a/types/index.d.ts b/types/index.d.ts index 127ec7af9..4b4b2beda 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -19,6 +19,7 @@ export type Options = { clientName?: string; useOptions?: boolean; useUnionTypes?: boolean; + autoformat?: boolean; exportCore?: boolean; exportServices?: boolean | string; exportModels?: boolean | string; From c33a9c3bbe463c3224c89d6436164184411fa3a8 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 3 Feb 2024 07:06:38 +0800 Subject: [PATCH 19/28] 0.27.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 79ee08ebd..3a4ae48b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@canoapbc/openapi-typescript-codegen", - "version": "0.27.2", + "version": "0.27.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@canoapbc/openapi-typescript-codegen", - "version": "0.27.2", + "version": "0.27.3", "license": "MIT", "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", diff --git a/package.json b/package.json index 14985fe6e..a01011b3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nicolas-chaulet/openapi-typescript-codegen", - "version": "0.27.2", + "version": "0.27.3", "description": "Library that generates Typescript clients based on the OpenAPI specification.", "author": "Ferdi Koomen", "homepage": "https://github.com/CanoaPBC/openapi-typescript-codegen", From 8f75b6a2a179e34349a6a536e0a4f0008c3bf471 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 3 Feb 2024 07:06:40 +0800 Subject: [PATCH 20/28] 0.27.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a4ae48b2..a79fa204d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@canoapbc/openapi-typescript-codegen", - "version": "0.27.3", + "version": "0.27.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@canoapbc/openapi-typescript-codegen", - "version": "0.27.3", + "version": "0.27.4", "license": "MIT", "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", diff --git a/package.json b/package.json index a01011b3f..4adad9587 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nicolas-chaulet/openapi-typescript-codegen", - "version": "0.27.3", + "version": "0.27.4", "description": "Library that generates Typescript clients based on the OpenAPI specification.", "author": "Ferdi Koomen", "homepage": "https://github.com/CanoaPBC/openapi-typescript-codegen", From 20dac0468fa07abc35348ae086c8d2fcb029b633 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 3 Feb 2024 07:19:53 +0800 Subject: [PATCH 21/28] chore(deps): update package-lock.json --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index eadcda562..49ffdebab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@canoapbc/openapi-typescript-codegen", + "name": "@nicolas-chaulet/openapi-typescript-codegen", "version": "0.27.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@canoapbc/openapi-typescript-codegen", + "name": "@nicolas-chaulet/openapi-typescript-codegen", "version": "0.27.4", "license": "MIT", "dependencies": { From c1d8979c626e44b413b6bee37e7c4b77ad0e324e Mon Sep 17 00:00:00 2001 From: Lubos Date: Sun, 4 Feb 2024 18:40:36 +0800 Subject: [PATCH 22/28] 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" } } } From fc55ad2dfa0357d55a1eac3124746d3e600eee5c Mon Sep 17 00:00:00 2001 From: Lubos Date: Sun, 4 Feb 2024 23:08:28 +0800 Subject: [PATCH 23/28] fix(parser): type additional properties with properties --- src/openApi/v3/parser/getModel.ts | 47 ++++----------------- src/openApi/v3/parser/getModelProperties.ts | 39 +++++++++++++++++ test/__snapshots__/index.spec.ts.snap | 42 ++++++++++++++++++ test/spec/v3.json | 11 +++++ 4 files changed, 101 insertions(+), 38 deletions(-) diff --git a/src/openApi/v3/parser/getModel.ts b/src/openApi/v3/parser/getModel.ts index 0393cc999..8c79d8397 100644 --- a/src/openApi/v3/parser/getModel.ts +++ b/src/openApi/v3/parser/getModel.ts @@ -6,7 +6,7 @@ import { extendEnum } from './extendEnum'; import { getEnum } from './getEnum'; import { findModelComposition, getModelComposition } from './getModelComposition'; import { getModelDefault } from './getModelDefault'; -import { getModelProperties } from './getModelProperties'; +import { getAdditionalPropertiesModel, getModelProperties } from './getModelProperties'; import { getType } from './getType'; export const getModel = ( @@ -103,33 +103,6 @@ export const getModel = ( return model; } - if ( - definition.type === 'object' && - (typeof definition.additionalProperties === 'object' || definition.additionalProperties === true) - ) { - const ap = typeof definition.additionalProperties === 'object' ? definition.additionalProperties : {}; - if (ap.$ref) { - const additionalProperties = getType(ap.$ref); - model.export = 'dictionary'; - model.type = additionalProperties.type; - model.base = additionalProperties.base; - model.template = additionalProperties.template; - model.imports.push(...additionalProperties.imports); - model.default = getModelDefault(definition, model); - return model; - } else { - const additionalProperties = getModel(openApi, ap); - model.export = 'dictionary'; - model.type = additionalProperties.type; - model.base = additionalProperties.base; - model.template = additionalProperties.template; - model.link = additionalProperties; - model.imports.push(...additionalProperties.imports); - model.default = getModelDefault(definition, model); - return model; - } - } - const foundComposition = findModelComposition(definition); if (foundComposition) { const composition = getModelComposition({ @@ -158,18 +131,16 @@ export const getModel = ( model.enums.push(modelProperty); } }); - return model; - } else { - const additionalProperties = getModel(openApi, {}); - model.export = 'dictionary'; - model.type = additionalProperties.type; - model.base = additionalProperties.base; - model.template = additionalProperties.template; - model.link = additionalProperties; - model.imports.push(...additionalProperties.imports); - model.default = getModelDefault(definition, model); + + if (definition.additionalProperties === true) { + const modelProperty = getAdditionalPropertiesModel(openApi, definition, getModel, model); + model.properties.push(modelProperty); + } + return model; } + + return getAdditionalPropertiesModel(openApi, definition, getModel, model); } if (definition.const !== undefined) { diff --git a/src/openApi/v3/parser/getModelProperties.ts b/src/openApi/v3/parser/getModelProperties.ts index 6e25ca833..e054bf9ed 100644 --- a/src/openApi/v3/parser/getModelProperties.ts +++ b/src/openApi/v3/parser/getModelProperties.ts @@ -5,11 +5,50 @@ import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; import { escapeName } from './escapeName'; import type { getModel } from './getModel'; +import { getModelDefault } from './getModelDefault'; import { getType } from './getType'; // Fix for circular dependency export type GetModelFn = typeof getModel; +export const getAdditionalPropertiesModel = ( + openApi: OpenApi, + definition: OpenApiSchema, + getModel: GetModelFn, + model: Model +): Model => { + const ap = typeof definition.additionalProperties === 'object' ? definition.additionalProperties : {}; + const apModel = getModel(openApi, ap); + + if (definition.additionalProperties === true && definition.properties) { + apModel.default = getModelDefault(definition, model); + apModel.export = 'generic'; + apModel.isRequired = true; + apModel.name = '[key: string]'; + return apModel; + } + + if (ap.$ref) { + const apType = getType(ap.$ref); + model.base = apType.base; + model.default = getModelDefault(definition, model); + model.export = 'dictionary'; + model.imports.push(...apType.imports); + model.template = apType.template; + model.type = apType.type; + return model; + } + + model.base = apModel.base; + model.default = getModelDefault(definition, model); + model.export = 'dictionary'; + model.imports.push(...apModel.imports); + model.link = apModel; + model.template = apModel.template; + model.type = apModel.type; + return model; +}; + export const getModelProperties = ( openApi: OpenApi, definition: OpenApiSchema, diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index 10e585282..a2b043972 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -3740,6 +3740,7 @@ export type { ModelCircle } from './models/ModelCircle'; export type { ModelSquare } from './models/ModelSquare'; export type { ModelThatExtends } from './models/ModelThatExtends'; export type { ModelThatExtendsExtends } from './models/ModelThatExtendsExtends'; +export type { ModelWithAdditionalPropertiesEqTrue } from './models/ModelWithAdditionalPropertiesEqTrue'; export type { ModelWithArray } from './models/ModelWithArray'; export type { ModelWithBoolean } from './models/ModelWithBoolean'; export type { ModelWithCircularReference } from './models/ModelWithCircularReference'; @@ -3819,6 +3820,7 @@ export { $ModelCircle } from './schemas/$ModelCircle'; export { $ModelSquare } from './schemas/$ModelSquare'; export { $ModelThatExtends } from './schemas/$ModelThatExtends'; export { $ModelThatExtendsExtends } from './schemas/$ModelThatExtendsExtends'; +export { $ModelWithAdditionalPropertiesEqTrue } from './schemas/$ModelWithAdditionalPropertiesEqTrue'; export { $ModelWithArray } from './schemas/$ModelWithArray'; export { $ModelWithBoolean } from './schemas/$ModelWithBoolean'; export { $ModelWithCircularReference } from './schemas/$ModelWithCircularReference'; @@ -4650,6 +4652,25 @@ export type ModelThatExtendsExtends = (ModelWithString & ModelThatExtends & { " `; +exports[`v3 should generate: test/generated/v3/models/ModelWithAdditionalPropertiesEqTrue.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * This is a model with one property and additionalProperties: true + */ +export type ModelWithAdditionalPropertiesEqTrue = { + /** + * This is a simple string property + */ + prop?: string; + [key: string]: any; +}; + +" +`; + exports[`v3 should generate: test/generated/v3/models/ModelWithArray.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -6143,6 +6164,27 @@ export const $ModelThatExtendsExtends = { " `; +exports[`v3 should generate: test/generated/v3/schemas/$ModelWithAdditionalPropertiesEqTrue.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ModelWithAdditionalPropertiesEqTrue = { + description: \`This is a model with one property and additionalProperties: true\`, + properties: { + prop: { + type: 'string', + description: \`This is a simple string property\`, + }, + [key: string]: { + type: 'any', + isRequired: true, + }, + }, +} as const; +" +`; + exports[`v3 should generate: test/generated/v3/schemas/$ModelWithArray.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 70376f1ab..f1bb32e34 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -2737,6 +2737,17 @@ } } }, + "ModelWithAdditionalPropertiesEqTrue": { + "description": "This is a model with one property and additionalProperties: true", + "type": "object", + "properties": { + "prop": { + "description": "This is a simple string property", + "type": "string" + } + }, + "additionalProperties": true + }, "NestedAnyOfArraysNullable": { "properties": { "nullableArray": { From 80f0dbf7274e8e8becf2d1ed9dd967ae39f834dd Mon Sep 17 00:00:00 2001 From: Lubos Date: Sun, 4 Feb 2024 23:23:06 +0800 Subject: [PATCH 24/28] 0.27.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49ffdebab..2dccfb58c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@nicolas-chaulet/openapi-typescript-codegen", - "version": "0.27.4", + "version": "0.27.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nicolas-chaulet/openapi-typescript-codegen", - "version": "0.27.4", + "version": "0.27.5", "license": "MIT", "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", diff --git a/package.json b/package.json index a63cef195..c078f787d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nicolas-chaulet/openapi-typescript-codegen", - "version": "0.27.4", + "version": "0.27.5", "description": "Library that generates Typescript clients based on the OpenAPI specification.", "author": "Ferdi Koomen", "homepage": "https://github.com/CanoaPBC/openapi-typescript-codegen", From d9daebda7fa2cfd93726e46299c71a25b657e1cb Mon Sep 17 00:00:00 2001 From: Lubos Date: Sun, 4 Feb 2024 23:44:17 +0800 Subject: [PATCH 25/28] fix(api): allow overriding request body name with x-body-name key --- src/openApi/v3/interfaces/OpenApiRequestBody.d.ts | 5 +++-- src/openApi/v3/parser/getOperationRequestBody.ts | 4 ++-- src/utils/writeClient.ts | 1 - test/__snapshots__/index.spec.ts.snap | 6 +++--- test/spec/v3.json | 1 + 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/openApi/v3/interfaces/OpenApiRequestBody.d.ts b/src/openApi/v3/interfaces/OpenApiRequestBody.d.ts index 1a687b8b9..fc323dba1 100644 --- a/src/openApi/v3/interfaces/OpenApiRequestBody.d.ts +++ b/src/openApi/v3/interfaces/OpenApiRequestBody.d.ts @@ -6,8 +6,9 @@ import type { OpenApiReference } from './OpenApiReference'; * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#requestBodyObject */ export interface OpenApiRequestBody extends OpenApiReference { - description?: string; + 'x-body-name'?: string; content: Dictionary; - required?: boolean; + description?: string; nullable?: boolean; + required?: boolean; } diff --git a/src/openApi/v3/parser/getOperationRequestBody.ts b/src/openApi/v3/parser/getOperationRequestBody.ts index 9f9cca241..b27ab1284 100644 --- a/src/openApi/v3/parser/getOperationRequestBody.ts +++ b/src/openApi/v3/parser/getOperationRequestBody.ts @@ -10,8 +10,8 @@ export const getOperationRequestBody = (openApi: OpenApi, body: OpenApiRequestBo const requestBody: OperationParameter = { in: 'body', export: 'interface', - prop: 'requestBody', - name: 'requestBody', + prop: body['x-body-name'] ?? 'requestBody', + name: body['x-body-name'] ?? 'requestBody', type: 'any', base: 'any', template: null, diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index 06b1e0b72..c1c06ba39 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -28,7 +28,6 @@ import { writeClientServices } from './writeClientServices'; * @param exportServices Generate services * @param exportModels Generate models * @param exportSchemas Generate schemas - * @param exportSchemas Generate schemas * @param indent Indentation options (4, 2 or tab) * @param postfixServices Service name postfix * @param postfixModels Model name postfix diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index a2b043972..91bb40356 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -7583,12 +7583,12 @@ import { request as __request } from '../core/request'; export class RequestBodyService { /** * @param parameter This is a reusable parameter - * @param requestBody A reusable request body + * @param foo A reusable request body * @throws ApiError */ public static postApiRequestBody( parameter?: string, - requestBody?: ModelWithString, + foo?: ModelWithString, ): CancelablePromise { return __request(OpenAPI, { method: 'POST', @@ -7596,7 +7596,7 @@ export class RequestBodyService { query: { 'parameter': parameter, }, - body: requestBody, + body: foo, mediaType: 'application/json', }); } diff --git a/test/spec/v3.json b/test/spec/v3.json index f1bb32e34..5dd4eccf9 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -1493,6 +1493,7 @@ "components": { "requestBodies": { "SimpleRequestBody": { + "x-body-name": "foo", "description": "A reusable request body", "required": false, "content": { From c14c1c5e0002965dee23a944147b0c8640b14b64 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sun, 4 Feb 2024 23:49:31 +0800 Subject: [PATCH 26/28] 0.27.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2dccfb58c..432003e30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@nicolas-chaulet/openapi-typescript-codegen", - "version": "0.27.5", + "version": "0.27.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nicolas-chaulet/openapi-typescript-codegen", - "version": "0.27.5", + "version": "0.27.6", "license": "MIT", "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", diff --git a/package.json b/package.json index c078f787d..ae5c16dd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nicolas-chaulet/openapi-typescript-codegen", - "version": "0.27.5", + "version": "0.27.6", "description": "Library that generates Typescript clients based on the OpenAPI specification.", "author": "Ferdi Koomen", "homepage": "https://github.com/CanoaPBC/openapi-typescript-codegen", From a20511130ad5eeb4aead88df4e0e92ff4c4fcef0 Mon Sep 17 00:00:00 2001 From: Jostein Stuhaug Date: Wed, 14 Feb 2024 10:56:28 +0100 Subject: [PATCH 27/28] Support non-ascii (unicode) enum and type names. This replaces regexp patterns that only worked with ascii characters with more proper matching that supports unicode identifiers in typescript/javascript. The platform must support "unicode-aware mode" (the u flag) for this to work. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode --- src/openApi/v2/parser/escapeName.ts | 4 +- src/openApi/v2/parser/getEnum.ts | 7 +- src/openApi/v2/parser/getType.ts | 5 +- src/openApi/v3/parser/escapeName.ts | 4 +- src/openApi/v3/parser/getEnum.ts | 7 +- src/openApi/v3/parser/getType.ts | 5 +- src/utils/sanitizeEnumName.spec.ts | 11 +++ src/utils/sanitizeEnumName.ts | 18 ++++ src/utils/sanitizeTypeName.spec.ts | 10 ++ src/utils/sanitizeTypeName.ts | 16 +++ src/utils/validTypescriptIdentifierRegex.ts | 4 + test/__snapshots__/index.spec.ts.snap | 104 ++++++++++++++++++++ test/spec/v2.json | 10 +- test/spec/v3.json | 10 +- 14 files changed, 193 insertions(+), 22 deletions(-) create mode 100644 src/utils/sanitizeEnumName.spec.ts create mode 100644 src/utils/sanitizeEnumName.ts create mode 100644 src/utils/sanitizeTypeName.spec.ts create mode 100644 src/utils/sanitizeTypeName.ts create mode 100644 src/utils/validTypescriptIdentifierRegex.ts diff --git a/src/openApi/v2/parser/escapeName.ts b/src/openApi/v2/parser/escapeName.ts index 9d6816c10..1fb196c63 100644 --- a/src/openApi/v2/parser/escapeName.ts +++ b/src/openApi/v2/parser/escapeName.ts @@ -1,6 +1,8 @@ +import validTypescriptIdentifierRegex from '../../../utils/validTypescriptIdentifierRegex'; + export const escapeName = (value: string): string => { if (value || value === '') { - const validName = /^[a-zA-Z_$][\w$]+$/g.test(value); + const validName = validTypescriptIdentifierRegex.test(value); if (!validName) { return `'${value}'`; } diff --git a/src/openApi/v2/parser/getEnum.ts b/src/openApi/v2/parser/getEnum.ts index 64c7ca8b5..eb671f679 100644 --- a/src/openApi/v2/parser/getEnum.ts +++ b/src/openApi/v2/parser/getEnum.ts @@ -1,4 +1,5 @@ import type { Enum } from '../../../client/interfaces/Enum'; +import sanitizeEnumName from '../../../utils/sanitizeEnumName'; export const getEnum = (values?: (string | number)[]): Enum[] => { if (Array.isArray(values)) { @@ -19,11 +20,7 @@ export const getEnum = (values?: (string | number)[]): Enum[] => { }; } return { - name: String(value) - .replace(/\W+/g, '_') - .replace(/^(\d+)/g, '_$1') - .replace(/([a-z])([A-Z]+)/g, '$1_$2') - .toUpperCase(), + name: sanitizeEnumName(String(value)), value: `'${value.replace(/'/g, "\\'")}'`, type: 'string', description: null, diff --git a/src/openApi/v2/parser/getType.ts b/src/openApi/v2/parser/getType.ts index 6caa1e015..594fa6106 100644 --- a/src/openApi/v2/parser/getType.ts +++ b/src/openApi/v2/parser/getType.ts @@ -1,10 +1,9 @@ import type { Type } from '../../../client/interfaces/Type'; +import sanitizeTypeName from '../../../utils/sanitizeTypeName'; import { getMappedType } from './getMappedType'; import { stripNamespace } from './stripNamespace'; -const encode = (value: string): string => { - return value.replace(/^[^a-zA-Z_$]+/g, '').replace(/[^\w$]+/g, '_'); -}; +const encode = (value: string): string => sanitizeTypeName(value); /** * Parse any string value into a type object. diff --git a/src/openApi/v3/parser/escapeName.ts b/src/openApi/v3/parser/escapeName.ts index 9d6816c10..1fb196c63 100644 --- a/src/openApi/v3/parser/escapeName.ts +++ b/src/openApi/v3/parser/escapeName.ts @@ -1,6 +1,8 @@ +import validTypescriptIdentifierRegex from '../../../utils/validTypescriptIdentifierRegex'; + export const escapeName = (value: string): string => { if (value || value === '') { - const validName = /^[a-zA-Z_$][\w$]+$/g.test(value); + const validName = validTypescriptIdentifierRegex.test(value); if (!validName) { return `'${value}'`; } diff --git a/src/openApi/v3/parser/getEnum.ts b/src/openApi/v3/parser/getEnum.ts index 64c7ca8b5..eb671f679 100644 --- a/src/openApi/v3/parser/getEnum.ts +++ b/src/openApi/v3/parser/getEnum.ts @@ -1,4 +1,5 @@ import type { Enum } from '../../../client/interfaces/Enum'; +import sanitizeEnumName from '../../../utils/sanitizeEnumName'; export const getEnum = (values?: (string | number)[]): Enum[] => { if (Array.isArray(values)) { @@ -19,11 +20,7 @@ export const getEnum = (values?: (string | number)[]): Enum[] => { }; } return { - name: String(value) - .replace(/\W+/g, '_') - .replace(/^(\d+)/g, '_$1') - .replace(/([a-z])([A-Z]+)/g, '$1_$2') - .toUpperCase(), + name: sanitizeEnumName(String(value)), value: `'${value.replace(/'/g, "\\'")}'`, type: 'string', description: null, diff --git a/src/openApi/v3/parser/getType.ts b/src/openApi/v3/parser/getType.ts index e8ef4733d..4cb82d367 100644 --- a/src/openApi/v3/parser/getType.ts +++ b/src/openApi/v3/parser/getType.ts @@ -1,11 +1,10 @@ import type { Type } from '../../../client/interfaces/Type'; import { isDefined } from '../../../utils/isDefined'; +import sanitizeTypeName from '../../../utils/sanitizeTypeName'; import { getMappedType } from './getMappedType'; import { stripNamespace } from './stripNamespace'; -const encode = (value: string): string => { - return value.replace(/^[^a-zA-Z_$]+/g, '').replace(/[^\w$]+/g, '_'); -}; +const encode = (value: string): string => sanitizeTypeName(value); /** * Parse any string value into a type object. diff --git a/src/utils/sanitizeEnumName.spec.ts b/src/utils/sanitizeEnumName.spec.ts new file mode 100644 index 000000000..36db89743 --- /dev/null +++ b/src/utils/sanitizeEnumName.spec.ts @@ -0,0 +1,11 @@ +import sanitizeEnumName from './sanitizeEnumName'; + +describe('sanitizeEnumName', () => { + it('should replace illegal characters', () => { + expect(sanitizeEnumName('abc')).toEqual('ABC'); + expect(sanitizeEnumName('æbc')).toEqual('ÆBC'); + expect(sanitizeEnumName('æb.c')).toEqual('ÆB_C'); + expect(sanitizeEnumName('1æb.c')).toEqual('_1ÆB_C'); + expect(sanitizeEnumName("'quoted'")).toEqual('_QUOTED_'); + }); +}); diff --git a/src/utils/sanitizeEnumName.ts b/src/utils/sanitizeEnumName.ts new file mode 100644 index 000000000..1f1a6080c --- /dev/null +++ b/src/utils/sanitizeEnumName.ts @@ -0,0 +1,18 @@ +/** + * Sanitizes names of enums, so they are valid typescript identifiers of a certain form. + * + * 1: Replace all characters not legal as part of identifier with '_' + * 2: Add '_' prefix if first character of enum name has character not legal for start of identifier + * 3: Add '_' where the string transitions from lowercase to uppercase + * 4: Transform the whole string to uppercase + * + * Javascript identifier regexp pattern retrieved from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers + */ +const sanitizeEnumName = (name: string) => + name + .replace(/[^$\u200c\u200d\p{ID_Continue}]/gu, '_') + .replace(/^([^$_\p{ID_Start}])/u, '_$1') + .replace(/(\p{Lowercase})(\p{Uppercase}+)/gu, '$1_$2') + .toUpperCase(); + +export default sanitizeEnumName; diff --git a/src/utils/sanitizeTypeName.spec.ts b/src/utils/sanitizeTypeName.spec.ts new file mode 100644 index 000000000..d01c52cb7 --- /dev/null +++ b/src/utils/sanitizeTypeName.spec.ts @@ -0,0 +1,10 @@ +import sanitizeTypeName from './sanitizeTypeName'; + +describe('sanitizeTypeName', () => { + it('should remove/replace illegal characters', () => { + expect(sanitizeTypeName('abc')).toEqual('abc'); + expect(sanitizeTypeName('æbc')).toEqual('æbc'); + expect(sanitizeTypeName('æb.c')).toEqual('æb_c'); + expect(sanitizeTypeName('1æb.c')).toEqual('æb_c'); + }); +}); diff --git a/src/utils/sanitizeTypeName.ts b/src/utils/sanitizeTypeName.ts new file mode 100644 index 000000000..3c0a2079c --- /dev/null +++ b/src/utils/sanitizeTypeName.ts @@ -0,0 +1,16 @@ +/** + * Sanitizes names of types, so they are valid typescript identifiers of a certain form. + * + * 1: Remove any leading characters that are illegal as starting character of a typescript identifier. + * 2: Replace illegal characters in remaining part of type name with underscore (_). + * + * Step 1 should perhaps instead also replace illegal characters with underscore, or prefix with it, like sanitizeEnumName + * does. The way this is now one could perhaps end up removing all characters, if all are illegal start characters. It + * would be sort of a breaking change to do so, though, previously generated code might change then. + * + * Javascript identifier regexp pattern retrieved from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers + */ +const sanitizeTypeName = (name: string) => + name.replace(/^[^$_\p{ID_Start}]+/u, '').replace(/[^$\u200c\u200d\p{ID_Continue}]/gu, '_'); + +export default sanitizeTypeName; diff --git a/src/utils/validTypescriptIdentifierRegex.ts b/src/utils/validTypescriptIdentifierRegex.ts new file mode 100644 index 000000000..80501679b --- /dev/null +++ b/src/utils/validTypescriptIdentifierRegex.ts @@ -0,0 +1,4 @@ +// Javascript identifier regexp pattern retrieved from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers +const validTypescriptIdentifierRegex = /^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u; + +export default validTypescriptIdentifierRegex; diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index e1c221495..ae5a82912 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -614,6 +614,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 { NonAsciiStringæøåÆØÅöôêÊ } from './models/NonAsciiStringæøåÆØÅöôêÊ'; export type { SimpleBoolean } from './models/SimpleBoolean'; export type { SimpleFile } from './models/SimpleFile'; export type { SimpleInteger } from './models/SimpleInteger'; @@ -663,6 +664,7 @@ export { $ModelWithPattern } from './schemas/$ModelWithPattern'; export { $ModelWithProperties } from './schemas/$ModelWithProperties'; export { $ModelWithReference } from './schemas/$ModelWithReference'; export { $ModelWithString } from './schemas/$ModelWithString'; +export { $NonAsciiStringæøåÆØÅöôêÊ } from './schemas/$NonAsciiStringæøåÆØÅöôêÊ'; export { $SimpleBoolean } from './schemas/$SimpleBoolean'; export { $SimpleFile } from './schemas/$SimpleFile'; export { $SimpleInteger } from './schemas/$SimpleInteger'; @@ -1009,6 +1011,7 @@ export enum EnumWithStrings { ERROR = 'Error', _SINGLE_QUOTE_ = '\\'Single Quote\\'', _DOUBLE_QUOTES_ = '"Double Quotes"', + NON_ASCII__ØÆÅÔÖ_ØÆÅÔÖ = 'Non-ascii: øæåôöØÆÅÔÖ', } " `; @@ -1178,6 +1181,7 @@ export namespace ModelWithEnum { SUCCESS = 'Success', WARNING = 'Warning', ERROR = 'Error', + ØÆÅ = 'ØÆÅ', } /** * These are the HTTP error code enums @@ -1388,6 +1392,30 @@ export type ModelWithString = { " `; +exports[`v2 should generate: test/generated/v2/models/NonAsciiStringæøåÆØÅöôêÊ.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ) + */ +export type NonAsciiStringæøåÆØÅöôêÊ = string; +" +`; + +exports[`v2 should generate: test/generated/v2/models/NonAsciiStringæøåÆØÅöôêÊ字符串.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ字符串) + */ +export type NonAsciiStringæøåÆØÅöôêÊ字符串 = string; +" +`; + exports[`v2 should generate: test/generated/v2/models/SimpleBoolean.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -2256,6 +2284,30 @@ export const $ModelWithString = { " `; +exports[`v2 should generate: test/generated/v2/schemas/$NonAsciiStringæøåÆØÅöôêÊ.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $NonAsciiStringæøåÆØÅöôêÊ = { + type: 'string', + description: \`A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ)\`, +} as const; +" +`; + +exports[`v2 should generate: test/generated/v2/schemas/$NonAsciiStringæøåÆØÅöôêÊ字符串.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $NonAsciiStringæøåÆØÅöôêÊ字符串 = { + type: 'string', + description: \`A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ字符串)\`, +} as const; +" +`; + exports[`v2 should generate: test/generated/v2/schemas/$SimpleBoolean.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -3728,6 +3780,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 { NonAsciiStringæøåÆØÅöôêÊ } from './models/NonAsciiStringæøåÆØÅöôêÊ'; export type { Pageable } from './models/Pageable'; export type { SimpleBoolean } from './models/SimpleBoolean'; export type { SimpleFile } from './models/SimpleFile'; @@ -3798,6 +3851,7 @@ export { $ModelWithPattern } from './schemas/$ModelWithPattern'; export { $ModelWithProperties } from './schemas/$ModelWithProperties'; export { $ModelWithReference } from './schemas/$ModelWithReference'; export { $ModelWithString } from './schemas/$ModelWithString'; +export { $NonAsciiStringæøåÆØÅöôêÊ } from './schemas/$NonAsciiStringæøåÆØÅöôêÊ'; export { $Pageable } from './schemas/$Pageable'; export { $SimpleBoolean } from './schemas/$SimpleBoolean'; export { $SimpleFile } from './schemas/$SimpleFile'; @@ -4385,6 +4439,7 @@ export enum EnumWithStrings { ERROR = 'Error', _SINGLE_QUOTE_ = '\\'Single Quote\\'', _DOUBLE_QUOTES_ = '"Double Quotes"', + NON_ASCII__ØÆÅÔÖ_ØÆÅÔÖ = 'Non-ascii: øæåôöØÆÅÔÖ', } " `; @@ -4638,6 +4693,7 @@ export namespace ModelWithEnum { SUCCESS = 'Success', WARNING = 'Warning', ERROR = 'Error', + ØÆÅ = 'ØÆÅ', } /** * These are the HTTP error code enums @@ -4857,6 +4913,30 @@ export type ModelWithString = { " `; +exports[`v3 should generate: test/generated/v3/models/NonAsciiStringæøåÆØÅöôêÊ.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ) + */ +export type NonAsciiStringæøåÆØÅöôêÊ = string; +" +`; + +exports[`v3 should generate: test/generated/v3/models/NonAsciiStringæøåÆØÅöôêÊ字符串.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ字符串) + */ +export type NonAsciiStringæøåÆØÅöôêÊ字符串 = string; +" +`; + exports[`v3 should generate: test/generated/v3/models/Pageable.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -6247,6 +6327,30 @@ export const $ModelWithString = { " `; +exports[`v3 should generate: test/generated/v3/schemas/$NonAsciiStringæøåÆØÅöôêÊ.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $NonAsciiStringæøåÆØÅöôêÊ = { + type: 'string', + description: \`A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ)\`, +} as const; +" +`; + +exports[`v3 should generate: test/generated/v3/schemas/$NonAsciiStringæøåÆØÅöôêÊ字符串.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $NonAsciiStringæøåÆØÅöôêÊ字符串 = { + type: 'string', + description: \`A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ字符串)\`, +} 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/v2.json b/test/spec/v2.json index e8eb19b51..1f1728a06 100644 --- a/test/spec/v2.json +++ b/test/spec/v2.json @@ -944,6 +944,10 @@ "description": "This is a simple string", "type": "string" }, + "NonAsciiStringæøåÆØÅöôêÊ字符串": { + "description": "A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ字符串)", + "type": "string" + }, "SimpleFile": { "description": "This is a simple file", "type": "file" @@ -965,7 +969,8 @@ "Warning", "Error", "'Single Quote'", - "\"Double Quotes\"" + "\"Double Quotes\"", + "Non-ascii: øæåôöØÆÅÔÖ字符串" ] }, "EnumWithNumbers": { @@ -1174,7 +1179,8 @@ "enum": [ "Success", "Warning", - "Error" + "Error", + "ØÆÅ字符串" ] }, "statusCode": { diff --git a/test/spec/v3.json b/test/spec/v3.json index cb590d0b7..b58b24559 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -1539,6 +1539,10 @@ "description": "This is a simple string", "type": "string" }, + "NonAsciiStringæøåÆØÅöôêÊ字符串": { + "description": "A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ字符串)", + "type": "string" + }, "SimpleFile": { "description": "This is a simple file", "type": "file" @@ -1561,7 +1565,8 @@ "Warning", "Error", "'Single Quote'", - "\"Double Quotes\"" + "\"Double Quotes\"", + "Non-ascii: øæåôöØÆÅÔÖ字符串" ] }, "EnumWithNumbers": { @@ -1781,7 +1786,8 @@ "enum": [ "Success", "Warning", - "Error" + "Error", + "ØÆÅ字符串" ] }, "statusCode": { From 308a180451cefb6b5811243c448c769d013328ef Mon Sep 17 00:00:00 2001 From: Jostein Stuhaug Date: Wed, 14 Feb 2024 12:45:49 +0100 Subject: [PATCH 28/28] Update test snapshot. So that it matches the desired test result. --- test/__snapshots__/index.spec.ts.snap | 64 ++++----------------------- 1 file changed, 8 insertions(+), 56 deletions(-) diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index ae5a82912..e0bf03c13 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -614,7 +614,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 { NonAsciiStringæøåÆØÅöôêÊ } from './models/NonAsciiStringæøåÆØÅöôêÊ'; +export type { NonAsciiStringæøåÆØÅöôêÊ字符串 } from './models/NonAsciiStringæøåÆØÅöôêÊ字符串'; export type { SimpleBoolean } from './models/SimpleBoolean'; export type { SimpleFile } from './models/SimpleFile'; export type { SimpleInteger } from './models/SimpleInteger'; @@ -664,7 +664,7 @@ export { $ModelWithPattern } from './schemas/$ModelWithPattern'; export { $ModelWithProperties } from './schemas/$ModelWithProperties'; export { $ModelWithReference } from './schemas/$ModelWithReference'; export { $ModelWithString } from './schemas/$ModelWithString'; -export { $NonAsciiStringæøåÆØÅöôêÊ } from './schemas/$NonAsciiStringæøåÆØÅöôêÊ'; +export { $NonAsciiStringæøåÆØÅöôêÊ字符串 } from './schemas/$NonAsciiStringæøåÆØÅöôêÊ字符串'; export { $SimpleBoolean } from './schemas/$SimpleBoolean'; export { $SimpleFile } from './schemas/$SimpleFile'; export { $SimpleInteger } from './schemas/$SimpleInteger'; @@ -1011,7 +1011,7 @@ export enum EnumWithStrings { ERROR = 'Error', _SINGLE_QUOTE_ = '\\'Single Quote\\'', _DOUBLE_QUOTES_ = '"Double Quotes"', - NON_ASCII__ØÆÅÔÖ_ØÆÅÔÖ = 'Non-ascii: øæåôöØÆÅÔÖ', + NON_ASCII__ØÆÅÔÖ_ØÆÅÔÖ字符串 = 'Non-ascii: øæåôöØÆÅÔÖ字符串', } " `; @@ -1181,7 +1181,7 @@ export namespace ModelWithEnum { SUCCESS = 'Success', WARNING = 'Warning', ERROR = 'Error', - ØÆÅ = 'ØÆÅ', + ØÆÅ字符串 = 'ØÆÅ字符串', } /** * These are the HTTP error code enums @@ -1392,18 +1392,6 @@ export type ModelWithString = { " `; -exports[`v2 should generate: test/generated/v2/models/NonAsciiStringæøåÆØÅöôêÊ.ts 1`] = ` -"/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ) - */ -export type NonAsciiStringæøåÆØÅöôêÊ = string; -" -`; - exports[`v2 should generate: test/generated/v2/models/NonAsciiStringæøåÆØÅöôêÊ字符串.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -2284,18 +2272,6 @@ export const $ModelWithString = { " `; -exports[`v2 should generate: test/generated/v2/schemas/$NonAsciiStringæøåÆØÅöôêÊ.ts 1`] = ` -"/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $NonAsciiStringæøåÆØÅöôêÊ = { - type: 'string', - description: \`A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ)\`, -} as const; -" -`; - exports[`v2 should generate: test/generated/v2/schemas/$NonAsciiStringæøåÆØÅöôêÊ字符串.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -3780,7 +3756,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 { NonAsciiStringæøåÆØÅöôêÊ } from './models/NonAsciiStringæøåÆØÅöôêÊ'; +export type { NonAsciiStringæøåÆØÅöôêÊ字符串 } from './models/NonAsciiStringæøåÆØÅöôêÊ字符串'; export type { Pageable } from './models/Pageable'; export type { SimpleBoolean } from './models/SimpleBoolean'; export type { SimpleFile } from './models/SimpleFile'; @@ -3851,7 +3827,7 @@ export { $ModelWithPattern } from './schemas/$ModelWithPattern'; export { $ModelWithProperties } from './schemas/$ModelWithProperties'; export { $ModelWithReference } from './schemas/$ModelWithReference'; export { $ModelWithString } from './schemas/$ModelWithString'; -export { $NonAsciiStringæøåÆØÅöôêÊ } from './schemas/$NonAsciiStringæøåÆØÅöôêÊ'; +export { $NonAsciiStringæøåÆØÅöôêÊ字符串 } from './schemas/$NonAsciiStringæøåÆØÅöôêÊ字符串'; export { $Pageable } from './schemas/$Pageable'; export { $SimpleBoolean } from './schemas/$SimpleBoolean'; export { $SimpleFile } from './schemas/$SimpleFile'; @@ -4439,7 +4415,7 @@ export enum EnumWithStrings { ERROR = 'Error', _SINGLE_QUOTE_ = '\\'Single Quote\\'', _DOUBLE_QUOTES_ = '"Double Quotes"', - NON_ASCII__ØÆÅÔÖ_ØÆÅÔÖ = 'Non-ascii: øæåôöØÆÅÔÖ', + NON_ASCII__ØÆÅÔÖ_ØÆÅÔÖ字符串 = 'Non-ascii: øæåôöØÆÅÔÖ字符串', } " `; @@ -4693,7 +4669,7 @@ export namespace ModelWithEnum { SUCCESS = 'Success', WARNING = 'Warning', ERROR = 'Error', - ØÆÅ = 'ØÆÅ', + ØÆÅ字符串 = 'ØÆÅ字符串', } /** * These are the HTTP error code enums @@ -4913,18 +4889,6 @@ export type ModelWithString = { " `; -exports[`v3 should generate: test/generated/v3/models/NonAsciiStringæøåÆØÅöôêÊ.ts 1`] = ` -"/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ) - */ -export type NonAsciiStringæøåÆØÅöôêÊ = string; -" -`; - exports[`v3 should generate: test/generated/v3/models/NonAsciiStringæøåÆØÅöôêÊ字符串.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -6327,18 +6291,6 @@ export const $ModelWithString = { " `; -exports[`v3 should generate: test/generated/v3/schemas/$NonAsciiStringæøåÆØÅöôêÊ.ts 1`] = ` -"/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export const $NonAsciiStringæøåÆØÅöôêÊ = { - type: 'string', - description: \`A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ)\`, -} as const; -" -`; - exports[`v3 should generate: test/generated/v3/schemas/$NonAsciiStringæøåÆØÅöôêÊ字符串.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */