diff --git a/src/client/interfaces/Model.d.ts b/src/client/interfaces/Model.d.ts index 9d2da542..edf2b8d7 100644 --- a/src/client/interfaces/Model.d.ts +++ b/src/client/interfaces/Model.d.ts @@ -3,7 +3,7 @@ import type { Schema } from './Schema'; export interface Model extends Schema { name: string; - export: 'reference' | 'generic' | 'enum' | 'array' | 'dictionary' | 'interface'; + export: 'reference' | 'generic' | 'enum' | 'array' | 'dictionary' | 'interface' | 'one-of' | 'any-of' | 'all-of'; type: string; base: string; template: string | null; diff --git a/src/openApi/v3/parser/getModel.ts b/src/openApi/v3/parser/getModel.ts index e57553fa..6fbb1761 100644 --- a/src/openApi/v3/parser/getModel.ts +++ b/src/openApi/v3/parser/getModel.ts @@ -126,74 +126,42 @@ export function getModel(openApi: OpenApi, definition: OpenApiSchema, isDefiniti return model; } } - // TODO: - // Add correct support for oneOf, anyOf, allOf + // Add correct support for oneOf // https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/ - if (definition.anyOf && definition.anyOf.length && !definition.properties) { - model.export = 'generic'; - const compositionTypes = definition.anyOf.filter(type => type.$ref).map(type => getType(type.$ref)); - const composition = compositionTypes - .map(type => type.type) - .sort() - .join(' | '); - model.imports.push(...compositionTypes.map(type => type.base)); - model.type = composition; - model.base = composition; + if (definition.oneOf?.length || definition.anyOf?.length || definition.allOf?.length) { + let types: OpenApiSchema[] = []; + if (definition.oneOf?.length) { + model.export = 'one-of'; + types = definition.oneOf; + } else if (definition.anyOf?.length) { + model.export = 'any-of'; + types = definition.anyOf; + } else if (definition.allOf?.length) { + model.export = 'all-of'; + types = definition.allOf; + } + const compositionTypes = types.map(model => getModel(openApi, model)); + model.properties = compositionTypes; + model.imports.push(...compositionTypes.reduce((acc: string[], type) => acc.concat(type.imports), [])); + model.enums.push(...compositionTypes.reduce((acc: Model[], type) => acc.concat(type.enums), [])); return model; } - if (definition.oneOf && definition.oneOf.length && !definition.properties) { - model.export = 'generic'; - const compositionTypes = definition.oneOf.filter(type => type.$ref).map(type => getType(type.$ref)); - const composition = compositionTypes - .map(type => type.type) - .sort() - .join(' | '); - model.imports.push(...compositionTypes.map(type => type.base)); - model.type = composition; - model.base = composition; - return model; - } - - if (definition.type === 'object' || definition.allOf) { + if (definition.type === 'object') { model.export = 'interface'; model.type = PrimaryType.OBJECT; model.base = PrimaryType.OBJECT; model.default = getModelDefault(definition, model); - - if (definition.allOf && definition.allOf.length) { - definition.allOf.forEach(parent => { - if (parent.$ref) { - const parentRef = getType(parent.$ref); - model.extends.push(parentRef.base); - model.imports.push(parentRef.base); - } - if (parent.type === 'object' && parent.properties) { - const properties = getModelProperties(openApi, parent, getModel); - properties.forEach(property => { - model.properties.push(property); - model.imports.push(...property.imports); - if (property.export === 'enum') { - model.enums.push(property); - } - }); - } - }); - } - - if (definition.properties) { - const properties = getModelProperties(openApi, definition, getModel); - properties.forEach(property => { - model.properties.push(property); - model.imports.push(...property.imports); - if (property.export === 'enum') { - model.enums.push(property); - } - }); - } - + const properties = getModelProperties(openApi, definition, getModel); + properties.forEach(property => { + model.properties.push(property); + model.imports.push(...property.imports); + if (property.export === 'enum') { + model.enums.push(property); + } + }); return model; } diff --git a/src/templates/partials/schema.hbs b/src/templates/partials/schema.hbs index 68194de6..14b9156b 100644 --- a/src/templates/partials/schema.hbs +++ b/src/templates/partials/schema.hbs @@ -6,6 +6,12 @@ {{>schemaArray}} {{else equals export 'dictionary'}} {{>schemaDictionary}} +{{else equals export 'any-of'}} +{{>schemaComposition}} +{{else equals export 'all-of'}} +{{>schemaComposition}} +{{else equals export 'one-of'}} +{{>schemaComposition}} {{else}} {{>schemaGeneric}} {{/equals}} diff --git a/src/templates/partials/schemaComposition.hbs b/src/templates/partials/schemaComposition.hbs new file mode 100644 index 00000000..46953c1e --- /dev/null +++ b/src/templates/partials/schemaComposition.hbs @@ -0,0 +1,14 @@ +{ + type: '{{export}}', + + contains: [{{#each properties}}{{>schema}}, {{/each}}] +{{#if isReadOnly}} + isReadOnly: {{{isReadOnly}}}, +{{/if}} +{{#if isRequired}} + isRequired: {{{isRequired}}}, +{{/if}} +{{#if isNullable}} + isNullable: {{{isNullable}}}, +{{/if}} +} diff --git a/src/templates/partials/type.hbs b/src/templates/partials/type.hbs index fb17c467..b6b4834b 100644 --- a/src/templates/partials/type.hbs +++ b/src/templates/partials/type.hbs @@ -8,6 +8,12 @@ {{>typeArray}} {{else equals export 'dictionary'}} {{>typeDictionary}} +{{else equals export 'one-of'}} +{{>typeUnion}} +{{else equals export 'any-of'}} +{{>typeUnion}} +{{else equals export 'all-of'}} +{{>typeIntersection}} {{else}} {{>typeGeneric}} {{/equals}} diff --git a/src/templates/partials/typeIntersection.hbs b/src/templates/partials/typeIntersection.hbs new file mode 100644 index 00000000..c7cad9b9 --- /dev/null +++ b/src/templates/partials/typeIntersection.hbs @@ -0,0 +1 @@ +({{#each properties}} & {{>type}}{{/each}}){{>isNullable}} diff --git a/src/templates/partials/typeUnion.hbs b/src/templates/partials/typeUnion.hbs new file mode 100644 index 00000000..8eab5b5d --- /dev/null +++ b/src/templates/partials/typeUnion.hbs @@ -0,0 +1 @@ +({{#each properties}} | {{>type}}{{/each}}){{>isNullable}} diff --git a/src/utils/registerHandlebarTemplates.ts b/src/utils/registerHandlebarTemplates.ts index e555eb1b..fd451b1d 100644 --- a/src/utils/registerHandlebarTemplates.ts +++ b/src/utils/registerHandlebarTemplates.ts @@ -50,6 +50,7 @@ import partialParameters from '../templates/partials/parameters.hbs'; import partialResult from '../templates/partials/result.hbs'; import partialSchema from '../templates/partials/schema.hbs'; import partialSchemaArray from '../templates/partials/schemaArray.hbs'; +import partialSchemaComposition from '../templates/partials/schemaComposition.hbs'; import partialSchemaDictionary from '../templates/partials/schemaDictionary.hbs'; import partialSchemaEnum from '../templates/partials/schemaEnum.hbs'; import partialSchemaGeneric from '../templates/partials/schemaGeneric.hbs'; @@ -60,7 +61,9 @@ import partialTypeDictionary from '../templates/partials/typeDictionary.hbs'; import partialTypeEnum from '../templates/partials/typeEnum.hbs'; import partialTypeGeneric from '../templates/partials/typeGeneric.hbs'; import partialTypeInterface from '../templates/partials/typeInterface.hbs'; +import partialTypeIntersection from '../templates/partials/typeIntersection.hbs'; import partialTypeReference from '../templates/partials/typeReference.hbs'; +import partialTypeUnion from '../templates/partials/typeUnion.hbs'; import { registerHandlebarHelpers } from './registerHandlebarHelpers'; export interface Templates { @@ -120,6 +123,7 @@ export function registerHandlebarTemplates(): Templates { Handlebars.registerPartial('schemaEnum', Handlebars.template(partialSchemaEnum)); Handlebars.registerPartial('schemaGeneric', Handlebars.template(partialSchemaGeneric)); Handlebars.registerPartial('schemaInterface', Handlebars.template(partialSchemaInterface)); + Handlebars.registerPartial('schemaComposition', Handlebars.template(partialSchemaComposition)); Handlebars.registerPartial('type', Handlebars.template(partialType)); Handlebars.registerPartial('typeArray', Handlebars.template(partialTypeArray)); Handlebars.registerPartial('typeDictionary', Handlebars.template(partialTypeDictionary)); @@ -127,6 +131,8 @@ export function registerHandlebarTemplates(): Templates { Handlebars.registerPartial('typeGeneric', Handlebars.template(partialTypeGeneric)); Handlebars.registerPartial('typeInterface', Handlebars.template(partialTypeInterface)); Handlebars.registerPartial('typeReference', Handlebars.template(partialTypeReference)); + Handlebars.registerPartial('typeUnion', Handlebars.template(partialTypeUnion)); + Handlebars.registerPartial('typeIntersection', Handlebars.template(partialTypeIntersection)); Handlebars.registerPartial('base', Handlebars.template(partialBase)); // Generic functions used in 'request' file @see src/templates/core/request.hbs for more info diff --git a/test/__snapshots__/index.spec.js.snap b/test/__snapshots__/index.spec.js.snap index b03efe7c..afab0a21 100644 --- a/test/__snapshots__/index.spec.js.snap +++ b/test/__snapshots__/index.spec.js.snap @@ -2585,7 +2585,9 @@ export { EnumWithStrings } from './models/EnumWithStrings'; export type { ModelLink } from './models/ModelLink'; export type { ModelThatExtends } from './models/ModelThatExtends'; export type { ModelThatExtendsExtends } from './models/ModelThatExtendsExtends'; +export type { ModelWithAllOfAndNullable } from './models/ModelWithAllOfAndNullable'; export type { ModelWithAnyOf } from './models/ModelWithAnyOf'; +export type { ModelWithAnyOfAndNullable } from './models/ModelWithAnyOfAndNullable'; export type { ModelWithArray } from './models/ModelWithArray'; export type { ModelWithBoolean } from './models/ModelWithBoolean'; export type { ModelWithCircularReference } from './models/ModelWithCircularReference'; @@ -2599,6 +2601,7 @@ export type { ModelWithLink } from './models/ModelWithLink'; export type { ModelWithNestedEnums } from './models/ModelWithNestedEnums'; export type { ModelWithNestedProperties } from './models/ModelWithNestedProperties'; export type { ModelWithOneOf } from './models/ModelWithOneOf'; +export type { ModelWithOneOfAndNullable } from './models/ModelWithOneOfAndNullable'; export type { ModelWithOrderedProperties } from './models/ModelWithOrderedProperties'; export type { ModelWithPattern } from './models/ModelWithPattern'; export type { ModelWithProperties } from './models/ModelWithProperties'; @@ -2630,7 +2633,9 @@ export { $EnumWithStrings } from './schemas/$EnumWithStrings'; export { $ModelLink } from './schemas/$ModelLink'; export { $ModelThatExtends } from './schemas/$ModelThatExtends'; export { $ModelThatExtendsExtends } from './schemas/$ModelThatExtendsExtends'; +export { $ModelWithAllOfAndNullable } from './schemas/$ModelWithAllOfAndNullable'; export { $ModelWithAnyOf } from './schemas/$ModelWithAnyOf'; +export { $ModelWithAnyOfAndNullable } from './schemas/$ModelWithAnyOfAndNullable'; export { $ModelWithArray } from './schemas/$ModelWithArray'; export { $ModelWithBoolean } from './schemas/$ModelWithBoolean'; export { $ModelWithCircularReference } from './schemas/$ModelWithCircularReference'; @@ -2644,6 +2649,7 @@ export { $ModelWithLink } from './schemas/$ModelWithLink'; export { $ModelWithNestedEnums } from './schemas/$ModelWithNestedEnums'; export { $ModelWithNestedProperties } from './schemas/$ModelWithNestedProperties'; export { $ModelWithOneOf } from './schemas/$ModelWithOneOf'; +export { $ModelWithOneOfAndNullable } from './schemas/$ModelWithOneOfAndNullable'; export { $ModelWithOrderedProperties } from './schemas/$ModelWithOrderedProperties'; export { $ModelWithPattern } from './schemas/$ModelWithPattern'; export { $ModelWithProperties } from './schemas/$ModelWithProperties'; @@ -2899,11 +2905,10 @@ import type { ModelWithString } from './ModelWithString'; /** * This is a model that extends another model */ -export interface ModelThatExtends extends ModelWithString { - propExtendsA?: string; - propExtendsB?: ModelWithString; -} -" +export type ModelThatExtends = ( & ModelWithString & { + propExtendsA?: string, + propExtendsB?: ModelWithString, +});" `; exports[`v3 should generate: ./test/generated/v3/models/ModelThatExtendsExtends.ts 1`] = ` @@ -2917,9 +2922,28 @@ import type { ModelWithString } from './ModelWithString'; /** * This is a model that extends another model */ -export interface ModelThatExtendsExtends extends ModelWithString, ModelThatExtends { - propExtendsC?: string; - propExtendsD?: ModelWithString; +export type ModelThatExtendsExtends = ( & ModelWithString & ModelThatExtends & { + propExtendsC?: string, + propExtendsD?: ModelWithString, +});" +`; + +exports[`v3 should generate: ./test/generated/v3/models/ModelWithAllOfAndNullable.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ModelWithArray } from './ModelWithArray'; +import type { ModelWithDictionary } from './ModelWithDictionary'; +import type { ModelWithEnum } from './ModelWithEnum'; + +/** + * This is a model with one property with a 'all of' relationship + */ +export interface ModelWithAllOfAndNullable { + propA?: ( & { + boolean?: boolean, + } & ModelWithEnum & ModelWithArray & ModelWithDictionary) | null; } " `; @@ -2938,7 +2962,27 @@ import type { ModelWithString } from './ModelWithString'; * This is a model with one property with a 'any of' relationship */ export interface ModelWithAnyOf { - propA?: ModelWithArray | ModelWithDictionary | ModelWithEnum | ModelWithString; + propA?: ( | ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary); +} +" +`; + +exports[`v3 should generate: ./test/generated/v3/models/ModelWithAnyOfAndNullable.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ModelWithArray } from './ModelWithArray'; +import type { ModelWithDictionary } from './ModelWithDictionary'; +import type { ModelWithEnum } from './ModelWithEnum'; + +/** + * This is a model with one property with a 'any of' relationship + */ +export interface ModelWithAnyOfAndNullable { + propA?: ( | { + boolean?: boolean, + } | ModelWithEnum | ModelWithArray | ModelWithDictionary) | null; } " `; @@ -3201,7 +3245,27 @@ import type { ModelWithString } from './ModelWithString'; * This is a model with one property with a 'one of' relationship */ export interface ModelWithOneOf { - propA?: ModelWithArray | ModelWithDictionary | ModelWithEnum | ModelWithString; + propA?: ( | ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary); +} +" +`; + +exports[`v3 should generate: ./test/generated/v3/models/ModelWithOneOfAndNullable.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ModelWithArray } from './ModelWithArray'; +import type { ModelWithDictionary } from './ModelWithDictionary'; +import type { ModelWithEnum } from './ModelWithEnum'; + +/** + * This is a model with one property with a 'one of' relationship + */ +export interface ModelWithOneOfAndNullable { + propA?: ( | { + boolean?: boolean, + } | ModelWithEnum | ModelWithArray | ModelWithDictionary) | null; } " `; @@ -3605,18 +3669,21 @@ exports[`v3 should generate: ./test/generated/v3/schemas/$ModelThatExtends.ts 1` /* tslint:disable */ /* eslint-disable */ -import { $ModelWithString } from './$ModelWithString'; - export const $ModelThatExtends = { - properties: { - ...$ModelWithString.properties, - propExtendsA: { - type: 'string', + type: 'all-of', + + contains: [{ + type: 'ModelWithString', + }, { + properties: { + propExtendsA: { + type: 'string', + }, + propExtendsB: { + type: 'ModelWithString', + }, }, - propExtendsB: { - type: 'ModelWithString', - }, - }, + }, ] };" `; @@ -3625,18 +3692,50 @@ exports[`v3 should generate: ./test/generated/v3/schemas/$ModelThatExtendsExtend /* tslint:disable */ /* eslint-disable */ -import { $ModelWithString } from './$ModelWithString'; -import { $ModelThatExtends } from './$ModelThatExtends'; - export const $ModelThatExtendsExtends = { - properties: { - ...$ModelWithString.properties, - ...$ModelThatExtends.properties, - propExtendsC: { - type: 'string', + type: 'all-of', + + contains: [{ + type: 'ModelWithString', + }, { + type: 'ModelThatExtends', + }, { + properties: { + propExtendsC: { + type: 'string', + }, + propExtendsD: { + type: 'ModelWithString', + }, }, - propExtendsD: { - type: 'ModelWithString', + }, ] +};" +`; + +exports[`v3 should generate: ./test/generated/v3/schemas/$ModelWithAllOfAndNullable.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export const $ModelWithAllOfAndNullable = { + properties: { + propA: { + type: 'all-of', + + contains: [{ + properties: { + boolean: { + type: 'boolean', + }, + }, + }, { + type: 'ModelWithEnum', + }, { + type: 'ModelWithArray', + }, { + type: 'ModelWithDictionary', + }, ] + isNullable: true, }, }, };" @@ -3650,7 +3749,46 @@ exports[`v3 should generate: ./test/generated/v3/schemas/$ModelWithAnyOf.ts 1`] export const $ModelWithAnyOf = { properties: { propA: { - type: 'ModelWithArray | ModelWithDictionary | ModelWithEnum | ModelWithString', + type: 'any-of', + + contains: [{ + type: 'ModelWithString', + }, { + type: 'ModelWithEnum', + }, { + type: 'ModelWithArray', + }, { + type: 'ModelWithDictionary', + }, ] + }, + }, +};" +`; + +exports[`v3 should generate: ./test/generated/v3/schemas/$ModelWithAnyOfAndNullable.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export const $ModelWithAnyOfAndNullable = { + properties: { + propA: { + type: 'any-of', + + contains: [{ + properties: { + boolean: { + type: 'boolean', + }, + }, + }, { + type: 'ModelWithEnum', + }, { + type: 'ModelWithArray', + }, { + type: 'ModelWithDictionary', + }, ] + isNullable: true, }, }, };" @@ -3897,7 +4035,46 @@ exports[`v3 should generate: ./test/generated/v3/schemas/$ModelWithOneOf.ts 1`] export const $ModelWithOneOf = { properties: { propA: { - type: 'ModelWithArray | ModelWithDictionary | ModelWithEnum | ModelWithString', + type: 'one-of', + + contains: [{ + type: 'ModelWithString', + }, { + type: 'ModelWithEnum', + }, { + type: 'ModelWithArray', + }, { + type: 'ModelWithDictionary', + }, ] + }, + }, +};" +`; + +exports[`v3 should generate: ./test/generated/v3/schemas/$ModelWithOneOfAndNullable.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export const $ModelWithOneOfAndNullable = { + properties: { + propA: { + type: 'one-of', + + contains: [{ + properties: { + boolean: { + type: 'boolean', + }, + }, + }, { + type: 'ModelWithEnum', + }, { + type: 'ModelWithArray', + }, { + type: 'ModelWithDictionary', + }, ] + isNullable: true, }, }, };" @@ -4167,7 +4344,7 @@ export class ComplexService { readonly type: 'Monkey' | 'Horse' | 'Bird', listOfModels?: Array | null, listOfStrings?: Array | null, - parameters: ModelWithArray | ModelWithDictionary | ModelWithEnum | ModelWithString, + parameters: ( | ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary), readonly user?: { readonly id?: number, readonly name?: string | null, diff --git a/test/spec/v3.json b/test/spec/v3.json index daa47fd0..13a19d33 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -1561,6 +1561,93 @@ } } }, + "ModelWithOneOfAndNullable": { + "description": "This is a model with one property with a 'one of' relationship", + "type": "object", + "properties": { + "propA": { + "nullable": true, + "type": "object", + "oneOf": [ + { + "type": "object", + "properties": { + "boolean": { + "type": "boolean" + } + } + }, + { + "$ref": "#/components/schemas/ModelWithEnum" + }, + { + "$ref": "#/components/schemas/ModelWithArray" + }, + { + "$ref": "#/components/schemas/ModelWithDictionary" + } + ] + } + } + }, + "ModelWithAllOfAndNullable": { + "description": "This is a model with one property with a 'all of' relationship", + "type": "object", + "properties": { + "propA": { + "nullable": true, + "type": "object", + "allOf": [ + { + "type": "object", + "properties": { + "boolean": { + "type": "boolean" + } + } + }, + { + "$ref": "#/components/schemas/ModelWithEnum" + }, + { + "$ref": "#/components/schemas/ModelWithArray" + }, + { + "$ref": "#/components/schemas/ModelWithDictionary" + } + ] + } + } + }, + "ModelWithAnyOfAndNullable": { + "description": "This is a model with one property with a 'any of' relationship", + "type": "object", + "properties": { + "propA": { + "nullable": true, + "type": "object", + "anyOf": [ + { + "type": "object", + "properties": { + "boolean": { + "type": "boolean" + } + } + }, + { + "$ref": "#/components/schemas/ModelWithEnum" + }, + { + "$ref": "#/components/schemas/ModelWithArray" + }, + { + "$ref": "#/components/schemas/ModelWithDictionary" + } + ] + } + } + }, "ModelWithProperties": { "description": "This is a model with one nested property", "type": "object",