Skip to content

Commit

Permalink
Merge branch '912_cli_nullable' of https://github.com/tmtron/swagger
Browse files Browse the repository at this point in the history
…into tmtron-912_cli_nullable
  • Loading branch information
kamilmysliwiec committed Aug 20, 2020
2 parents 912c215 + 0038627 commit 766956e
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 40 deletions.
123 changes: 83 additions & 40 deletions lib/plugin/visitors/model-class.visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,12 @@ export class ModelClassVisitor extends AbstractFileVisitor {
hostFilename = ''
): ts.ObjectLiteralExpression {
const isRequired = !node.questionToken;

let properties = [
...existingProperties,
!hasPropertyKey('required', existingProperties) &&
ts.createPropertyAssignment('required', ts.createLiteral(isRequired)),
this.createTypePropertyAssignment(
node,
...this.createTypePropertyAssignments(
node.type,
typeChecker,
existingProperties,
hostFilename
Expand All @@ -151,64 +150,108 @@ export class ModelClassVisitor extends AbstractFileVisitor {
return objectLiteral;
}

createTypePropertyAssignment(
node: ts.PropertyDeclaration | ts.PropertySignature,
/**
* The function returns an array with 0, 1 or 2 PropertyAssignments.
* Possible keys:
* - 'type'
* - 'nullable'
*/
private createTypePropertyAssignments(
node: ts.TypeNode,
typeChecker: ts.TypeChecker,
existingProperties: ts.NodeArray<ts.PropertyAssignment>,
hostFilename: string
) {
): ts.PropertyAssignment[] {
const key = 'type';
if (hasPropertyKey(key, existingProperties)) {
return undefined;
}
const type = typeChecker.getTypeAtLocation(node);
if (!type) {
return undefined;
return [];
}
if (node.type && ts.isTypeLiteralNode(node.type)) {
const propertyAssignments = Array.from(node.type.members || []).map(
(member) => {
const literalExpr = this.createDecoratorObjectLiteralExpr(
member as ts.PropertySignature,
if (node) {
if (ts.isTypeLiteralNode(node)) {
const propertyAssignments = Array.from(node.members || []).map(
(member) => {
const literalExpr = this.createDecoratorObjectLiteralExpr(
member as ts.PropertySignature,
typeChecker,
existingProperties,
{},
hostFilename
);
return ts.createPropertyAssignment(
ts.createIdentifier(member.name.getText()),
literalExpr
);
}
);
return [
ts.createPropertyAssignment(
key,
ts.createArrowFunction(
undefined,
undefined,
[],
undefined,
undefined,
ts.createParen(ts.createObjectLiteral(propertyAssignments))
)
)
];
} else if (ts.isUnionTypeNode(node)) {
const nullableType = node.types.find(
(type) => type.kind === ts.SyntaxKind.NullKeyword
);
const isNullable = !!nullableType;
const remainingTypes = node.types.filter(
(item) => item !== nullableType
);
/**
* when we have more than 1 type left, we could use oneOf
*/
if (remainingTypes.length === 1) {
const remainingTypesProperties = this.createTypePropertyAssignments(
remainingTypes[0],
typeChecker,
existingProperties,
{},
hostFilename
);
return ts.createPropertyAssignment(
ts.createIdentifier(member.name.getText()),
literalExpr

const resultArray = new Array<ts.PropertyAssignment>(
...remainingTypesProperties
);
if (isNullable) {
const nullablePropertyAssignment = ts.createPropertyAssignment(
'nullable',
ts.createTrue()
);
resultArray.push(nullablePropertyAssignment);
}
return resultArray;
}
);
return ts.createPropertyAssignment(
}
}

const type = typeChecker.getTypeAtLocation(node);
if (!type) {
return [];
}
let typeReference = getTypeReferenceAsString(type, typeChecker);
if (!typeReference) {
return [];
}
typeReference = replaceImportPath(typeReference, hostFilename);
return [
ts.createPropertyAssignment(
key,
ts.createArrowFunction(
undefined,
undefined,
[],
undefined,
undefined,
ts.createParen(ts.createObjectLiteral(propertyAssignments))
ts.createIdentifier(typeReference)
)
);
}
let typeReference = getTypeReferenceAsString(type, typeChecker);
if (!typeReference) {
return undefined;
}
typeReference = replaceImportPath(typeReference, hostFilename);
return ts.createPropertyAssignment(
key,
ts.createArrowFunction(
undefined,
undefined,
[],
undefined,
undefined,
ts.createIdentifier(typeReference)
)
);
];
}

createEnumPropertyAssignment(
Expand Down
21 changes: 21 additions & 0 deletions test/plugin/fixtures/nullable.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const nullableDtoText = `
export class NullableDto {
@ApiProperty()
stringValue: string | null;
@ApiProperty()
stringArr: string[] | null;
}
`;

export const nullableDtoTextTranspiled = `export class NullableDto {
static _OPENAPI_METADATA_FACTORY() {
return { stringValue: { required: true, type: () => String, nullable: true }, stringArr: { required: true, type: () => [String], nullable: true } };
}
}
__decorate([
ApiProperty()
], NullableDto.prototype, "stringValue", void 0);
__decorate([
ApiProperty()
], NullableDto.prototype, "stringArr", void 0);
`;
25 changes: 25 additions & 0 deletions test/plugin/model-class-visitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,33 @@ import {
es5CreateCatDtoText,
es5CreateCatDtoTextTranspiled
} from './fixtures/es5-class.dto';
import {
nullableDtoText,
nullableDtoTextTranspiled
} from './fixtures/nullable.dto';

describe('API model properties', () => {
it('should understand nullable', () => {
const options: ts.CompilerOptions = {
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
newLine: ts.NewLineKind.LineFeed,
noEmitHelpers: true,
strict: true
};
const filename = 'nullable.dto.ts';
const fakeProgram = ts.createProgram([filename], options);

const result = ts.transpileModule(nullableDtoText, {
compilerOptions: options,
fileName: filename,
transformers: {
before: [before({ classValidatorShim: true }, fakeProgram)]
}
});
expect(result.outputText).toEqual(nullableDtoTextTranspiled);
});

it('should add the metadata factory when no decorators exist', () => {
const options: ts.CompilerOptions = {
module: ts.ModuleKind.ESNext,
Expand Down

0 comments on commit 766956e

Please sign in to comment.