feat: Improve support for allOf, oneOf, and anyOf

This commit is contained in:
Christian Budde Christensen 2020-11-10 16:38:59 +01:00
parent 2d8aa162cd
commit a63db3774d
No known key found for this signature in database
GPG Key ID: 475D82D19F0E4FDB
10 changed files with 360 additions and 90 deletions

View File

@ -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;

View File

@ -125,70 +125,42 @@ export function getModel(openApi: OpenApi, definition: OpenApiSchema, isDefiniti
return model;
}
}
// TODO:
// Add correct support for oneOf
// https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/
if (definition.anyOf?.length && !definition.properties) {
const compositionTypes = definition.anyOf.filter(type => type.$ref).map(type => getType(type.$ref));
const composition = compositionTypes
.map(type => type.type)
.sort()
.join(' | ');
model.export = 'generic';
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?.length && !definition.properties) {
const compositionTypes = definition.oneOf.filter(type => type.$ref).map(type => getType(type.$ref));
const composition = compositionTypes
.map(type => type.type)
.sort()
.join(' | ');
model.export = 'generic';
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 = 'any';
model.base = 'any';
model.default = getModelDefault(definition, model);
if (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;
}

View File

@ -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}}

View File

@ -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}}
}

View File

@ -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}}

View File

@ -0,0 +1 @@
({{#each properties}} & {{>type}}{{/each}}){{>isNullable}}

View File

@ -0,0 +1 @@
({{#each properties}} | {{>type}}{{/each}}){{>isNullable}}

View File

@ -51,6 +51,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';
@ -61,7 +62,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 {
@ -121,6 +124,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));
@ -128,6 +132,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

View File

@ -2657,7 +2657,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';
@ -2671,6 +2673,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';
@ -2703,7 +2706,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';
@ -2717,6 +2722,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';
@ -2983,11 +2989,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`] = `
@ -3001,9 +3006,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;
}
"
`;
@ -3022,7 +3046,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;
}
"
`;
@ -3285,7 +3329,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;
}
"
`;
@ -3699,18 +3763,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',
},
},
}, ]
};"
`;
@ -3719,18 +3786,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,
},
},
};"
@ -3744,7 +3843,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,
},
},
};"
@ -3991,7 +4129,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,
},
},
};"
@ -4261,7 +4438,7 @@ export class ComplexService {
readonly type: 'Monkey' | 'Horse' | 'Bird',
listOfModels?: Array<ModelWithString> | null,
listOfStrings?: Array<string> | null,
parameters: ModelWithArray | ModelWithDictionary | ModelWithEnum | ModelWithString,
parameters: ( | ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary),
readonly user?: {
readonly id?: number,
readonly name?: string | null,

View File

@ -1565,6 +1565,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",