diff --git a/src/openApi/v2/parser/getOperationParameterName.ts b/src/openApi/v2/parser/getOperationParameterName.ts index 0954d7365..e21e83c80 100644 --- a/src/openApi/v2/parser/getOperationParameterName.ts +++ b/src/openApi/v2/parser/getOperationParameterName.ts @@ -1,7 +1,6 @@ import camelCase from 'camelcase'; -const reservedWords = - /^(arguments|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|eval|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)$/g; +const reservedWords = /^(arguments|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|eval|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)$/g; /** * Replaces any invalid characters from a parameter name. diff --git a/src/openApi/v3/parser/getModel.ts b/src/openApi/v3/parser/getModel.ts index 30efcfaca..99507ad8c 100644 --- a/src/openApi/v3/parser/getModel.ts +++ b/src/openApi/v3/parser/getModel.ts @@ -165,7 +165,7 @@ export function getModel( model.default = getModelDefault(definition, model); if (definition.properties) { - const modelProperties = getModelProperties(openApi, definition, getModel); + const modelProperties = getModelProperties(openApi, definition, getModel, model); modelProperties.forEach(modelProperty => { model.imports.push(...modelProperty.imports); model.enums.push(...modelProperty.enums); diff --git a/src/openApi/v3/parser/getModelProperties.ts b/src/openApi/v3/parser/getModelProperties.ts index 5d59b3e42..cad55ae93 100644 --- a/src/openApi/v3/parser/getModelProperties.ts +++ b/src/openApi/v3/parser/getModelProperties.ts @@ -1,4 +1,5 @@ import type { Model } from '../../../client/interfaces/Model'; +import { findOneOfParentDiscriminator, mapPropertyValue } from '../../../utils/discriminator'; import { getPattern } from '../../../utils/getPattern'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; @@ -10,77 +11,82 @@ import { getType } from './getType'; // Fix for circular dependency export type GetModelFn = typeof getModel; -export function getModelProperties(openApi: OpenApi, definition: OpenApiSchema, getModel: GetModelFn): Model[] { +export function getModelProperties( + openApi: OpenApi, + definition: OpenApiSchema, + getModel: GetModelFn, + parent?: Model +): Model[] { const models: Model[] = []; + const discriminator = findOneOfParentDiscriminator(openApi, parent); for (const propertyName in definition.properties) { if (definition.properties.hasOwnProperty(propertyName)) { const property = definition.properties[propertyName]; const propertyRequired = !!definition.required?.includes(propertyName); - if (property.$ref) { + const propertyValues = { + name: escapeName(propertyName), + description: getComment(property.description), + isDefinition: false, + isReadOnly: property.readOnly === true, + isRequired: propertyRequired, + format: property.format, + maximum: property.maximum, + exclusiveMaximum: property.exclusiveMaximum, + minimum: property.minimum, + exclusiveMinimum: property.exclusiveMinimum, + multipleOf: property.multipleOf, + maxLength: property.maxLength, + minLength: property.minLength, + maxItems: property.maxItems, + minItems: property.minItems, + uniqueItems: property.uniqueItems, + maxProperties: property.maxProperties, + minProperties: property.minProperties, + pattern: getPattern(property.pattern), + }; + if (parent && discriminator?.propertyName == propertyName) { + models.push({ + export: 'reference', + type: 'string', + base: `'${mapPropertyValue(discriminator, parent)}'`, + template: null, + isNullable: property.nullable === true, + link: null, + imports: [], + enum: [], + enums: [], + properties: [], + ...propertyValues, + }); + } else if (property.$ref) { const model = getType(property.$ref); models.push({ - name: escapeName(propertyName), export: 'reference', type: model.type, base: model.base, template: model.template, link: null, - description: getComment(property.description), - isDefinition: false, - isReadOnly: property.readOnly === true, - isRequired: propertyRequired, isNullable: model.isNullable || property.nullable === true, - format: property.format, - maximum: property.maximum, - exclusiveMaximum: property.exclusiveMaximum, - minimum: property.minimum, - exclusiveMinimum: property.exclusiveMinimum, - multipleOf: property.multipleOf, - maxLength: property.maxLength, - minLength: property.minLength, - maxItems: property.maxItems, - minItems: property.minItems, - uniqueItems: property.uniqueItems, - maxProperties: property.maxProperties, - minProperties: property.minProperties, - pattern: getPattern(property.pattern), imports: model.imports, enum: [], enums: [], properties: [], + ...propertyValues, }); } else { const model = getModel(openApi, property); models.push({ - name: escapeName(propertyName), export: model.export, type: model.type, base: model.base, template: model.template, link: model.link, - description: getComment(property.description), - isDefinition: false, - isReadOnly: property.readOnly === true, - isRequired: propertyRequired, isNullable: model.isNullable || property.nullable === true, - format: property.format, - maximum: property.maximum, - exclusiveMaximum: property.exclusiveMaximum, - minimum: property.minimum, - exclusiveMinimum: property.exclusiveMinimum, - multipleOf: property.multipleOf, - maxLength: property.maxLength, - minLength: property.minLength, - maxItems: property.maxItems, - minItems: property.minItems, - uniqueItems: property.uniqueItems, - maxProperties: property.maxProperties, - minProperties: property.minProperties, - pattern: getPattern(property.pattern), imports: model.imports, enum: model.enum, enums: model.enums, properties: model.properties, + ...propertyValues, }); } } diff --git a/src/openApi/v3/parser/getOperationParameterName.ts b/src/openApi/v3/parser/getOperationParameterName.ts index 0954d7365..e21e83c80 100644 --- a/src/openApi/v3/parser/getOperationParameterName.ts +++ b/src/openApi/v3/parser/getOperationParameterName.ts @@ -1,7 +1,6 @@ import camelCase from 'camelcase'; -const reservedWords = - /^(arguments|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|eval|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)$/g; +const reservedWords = /^(arguments|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|eval|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)$/g; /** * Replaces any invalid characters from a parameter name. diff --git a/src/templates/partials/schemaGeneric.hbs b/src/templates/partials/schemaGeneric.hbs index 7b0d96c9a..d8a9e5bcb 100644 --- a/src/templates/partials/schemaGeneric.hbs +++ b/src/templates/partials/schemaGeneric.hbs @@ -1,6 +1,6 @@ { {{#if type}} - type: '{{{base}}}', + type: '{{{type}}}', {{/if}} {{#if isReadOnly}} isReadOnly: {{{isReadOnly}}}, diff --git a/src/utils/discriminator.ts b/src/utils/discriminator.ts new file mode 100644 index 000000000..cf78d4702 --- /dev/null +++ b/src/utils/discriminator.ts @@ -0,0 +1,46 @@ +import { Model } from '../client/interfaces/Model'; +import { OpenApi } from '../openApi/v3/interfaces/OpenApi'; +import { OpenApiDiscriminator } from '../openApi/v3/interfaces/OpenApiDiscriminator'; +import { stripNamespace } from '../openApi/v3/parser/stripNamespace'; +import { Dictionary } from './types'; + +const inverseDictionary = (mapObj: Dictionary) => { + const m2: Dictionary = {}; + for (const key in mapObj) { + m2[mapObj[key]] = key; + } + return m2; +}; + +export function findOneOfParentDiscriminator(openApi: OpenApi, parent?: Model): OpenApiDiscriminator | undefined { + if (openApi.components) { + for (const definitionName in openApi.components.schemas) { + if (openApi.components.schemas.hasOwnProperty(definitionName)) { + const schema = openApi.components.schemas[definitionName]; + if (parent && schema.oneOf?.length && schema.discriminator) { + const isPartOf = + schema.oneOf + .map(definition => { + return definition.$ref && stripNamespace(definition.$ref) == parent.name; + }) + .filter(Boolean).length > 0; + if (isPartOf) { + return schema.discriminator; + } + } + } + } + } + return undefined; +} + +export function mapPropertyValue(discriminator: OpenApiDiscriminator, parent: Model): string { + if (discriminator.mapping) { + const mapping = inverseDictionary(discriminator.mapping); + const key = Object.keys(mapping).find(item => stripNamespace(item) == parent.name); + if (key && mapping[key]) { + return mapping[key]; + } + } + return parent.name; +} diff --git a/test/__snapshots__/index.spec.js.snap b/test/__snapshots__/index.spec.js.snap index 8d3251f89..13e5ac1c4 100644 --- a/test/__snapshots__/index.spec.js.snap +++ b/test/__snapshots__/index.spec.js.snap @@ -3169,6 +3169,7 @@ export type { CompositionWithAnyOfAnonymous } from './models/CompositionWithAnyO export type { CompositionWithOneOf } from './models/CompositionWithOneOf'; export type { CompositionWithOneOfAndNullable } from './models/CompositionWithOneOfAndNullable'; export type { CompositionWithOneOfAnonymous } from './models/CompositionWithOneOfAnonymous'; +export type { CompositionWithOneOfDiscriminator } from './models/CompositionWithOneOfDiscriminator'; export type { DictionaryWithArray } from './models/DictionaryWithArray'; export type { DictionaryWithDictionary } from './models/DictionaryWithDictionary'; export type { DictionaryWithProperties } from './models/DictionaryWithProperties'; @@ -3178,6 +3179,8 @@ export type { EnumFromDescription } from './models/EnumFromDescription'; export { EnumWithExtensions } from './models/EnumWithExtensions'; export { EnumWithNumbers } from './models/EnumWithNumbers'; export { EnumWithStrings } from './models/EnumWithStrings'; +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 { ModelWithArray } from './models/ModelWithArray'; @@ -3220,6 +3223,7 @@ export { $CompositionWithAnyOfAnonymous } from './schemas/$CompositionWithAnyOfA export { $CompositionWithOneOf } from './schemas/$CompositionWithOneOf'; export { $CompositionWithOneOfAndNullable } from './schemas/$CompositionWithOneOfAndNullable'; export { $CompositionWithOneOfAnonymous } from './schemas/$CompositionWithOneOfAnonymous'; +export { $CompositionWithOneOfDiscriminator } from './schemas/$CompositionWithOneOfDiscriminator'; export { $DictionaryWithArray } from './schemas/$DictionaryWithArray'; export { $DictionaryWithDictionary } from './schemas/$DictionaryWithDictionary'; export { $DictionaryWithProperties } from './schemas/$DictionaryWithProperties'; @@ -3229,6 +3233,8 @@ export { $EnumFromDescription } from './schemas/$EnumFromDescription'; export { $EnumWithExtensions } from './schemas/$EnumWithExtensions'; export { $EnumWithNumbers } from './schemas/$EnumWithNumbers'; export { $EnumWithStrings } from './schemas/$EnumWithStrings'; +export { $ModelCircle } from './schemas/$ModelCircle'; +export { $ModelSquare } from './schemas/$ModelSquare'; export { $ModelThatExtends } from './schemas/$ModelThatExtends'; export { $ModelThatExtendsExtends } from './schemas/$ModelThatExtendsExtends'; export { $ModelWithArray } from './schemas/$ModelWithArray'; @@ -3513,6 +3519,21 @@ export type CompositionWithOneOfAnonymous = { " `; +exports[`v3 should generate: ./test/generated/v3/models/CompositionWithOneOfDiscriminator.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ModelCircle } from './ModelCircle'; +import type { ModelSquare } from './ModelSquare'; + +/** + * This is a model with one property with a 'one of' relationship where the options are not $ref + */ +export type CompositionWithOneOfDiscriminator = (ModelCircle | ModelSquare); +" +`; + exports[`v3 should generate: ./test/generated/v3/models/DictionaryWithArray.ts 1`] = ` "/* istanbul ignore file */ /* tslint:disable */ @@ -3652,6 +3673,36 @@ export enum EnumWithStrings { }" `; +exports[`v3 should generate: ./test/generated/v3/models/ModelCircle.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Circle + */ +export type ModelCircle = { + kind: 'circle'; + radius?: number; +} +" +`; + +exports[`v3 should generate: ./test/generated/v3/models/ModelSquare.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Square + */ +export type ModelSquare = { + kind: 'square'; + sideLength?: number; +} +" +`; + exports[`v3 should generate: ./test/generated/v3/models/ModelThatExtends.ts 1`] = ` "/* istanbul ignore file */ /* tslint:disable */ @@ -4422,6 +4473,20 @@ export const $CompositionWithOneOfAnonymous = { } as const;" `; +exports[`v3 should generate: ./test/generated/v3/schemas/$CompositionWithOneOfDiscriminator.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CompositionWithOneOfDiscriminator = { + type: 'one-of', + contains: [{ + type: 'ModelCircle', + }, { + type: 'ModelSquare', + }], +} as const;" +`; + exports[`v3 should generate: ./test/generated/v3/schemas/$DictionaryWithArray.ts 1`] = ` "/* istanbul ignore file */ /* tslint:disable */ @@ -4531,6 +4596,40 @@ export const $EnumWithStrings = { } as const;" `; +exports[`v3 should generate: ./test/generated/v3/schemas/$ModelCircle.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ModelCircle = { + properties: { + kind: { + type: 'string', + isRequired: true, + }, + radius: { + type: 'number', + }, + }, +} as const;" +`; + +exports[`v3 should generate: ./test/generated/v3/schemas/$ModelSquare.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ModelSquare = { + properties: { + kind: { + type: 'string', + isRequired: true, + }, + sideLength: { + type: 'number', + }, + }, +} as const;" +`; + exports[`v3 should generate: ./test/generated/v3/schemas/$ModelThatExtends.ts 1`] = ` "/* istanbul ignore file */ /* tslint:disable */ diff --git a/test/spec/v3.json b/test/spec/v3.json index 892687f8c..8e7b9b029 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -1863,6 +1863,51 @@ } } }, + "ModelCircle": { + "description": "Circle", + "type": "object", + "required": ["kind"], + "properties": { + "kind": { + "type": "string" + }, + "radius": { + "type": "number" + } + } + }, + "ModelSquare": { + "description": "Square", + "type": "object", + "required": ["kind"], + "properties": { + "kind": { + "type": "string" + }, + "sideLength": { + "type": "number" + } + } + }, + "CompositionWithOneOfDiscriminator": { + "description": "This is a model with one property with a 'one of' relationship where the options are not $ref", + "type": "object", + "oneOf": [ + { + "$ref": "#/components/schemas/ModelCircle" + }, + { + "$ref": "#/components/schemas/ModelSquare" + } + ], + "discriminator": { + "propertyName": "kind", + "mapping": { + "circle": "#/components/schemas/ModelCircle", + "square": "#/components/schemas/ModelSquare" + } + } + }, "CompositionWithAnyOf": { "description": "This is a model with one property with a 'any of' relationship", "type": "object",