Skip to content

Adding oneOf with discriminator property support #741

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/openApi/v2/parser/getOperationParameterName.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/openApi/v3/parser/getModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
86 changes: 46 additions & 40 deletions src/openApi/v3/parser/getModelProperties.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
});
}
}
Expand Down
3 changes: 1 addition & 2 deletions src/openApi/v3/parser/getOperationParameterName.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/templates/partials/schemaGeneric.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
{{#if type}}
type: '{{{base}}}',
type: '{{{type}}}',
{{/if}}
{{#if isReadOnly}}
isReadOnly: {{{isReadOnly}}},
Expand Down
46 changes: 46 additions & 0 deletions src/utils/discriminator.ts
Original file line number Diff line number Diff line change
@@ -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<string>) => {
const m2: Dictionary<string> = {};
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;
}
99 changes: 99 additions & 0 deletions test/__snapshots__/index.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */
Expand Down
Loading