From 6d21daf6cf72c774bc06df88b3559ae9d1575bf0 Mon Sep 17 00:00:00 2001 From: WoH Date: Wed, 14 Aug 2019 10:51:59 +0200 Subject: [PATCH] ObjectTypeLiteral support --- src/metadataGeneration/tsoa.ts | 8 ++++- src/metadataGeneration/typeResolver.ts | 17 ++++++++++- src/swagger/specGenerator.ts | 20 +++++++++++++ tests/fixtures/controllers/getController.ts | 3 ++ tests/fixtures/inversify/managedService.ts | 3 ++ tests/fixtures/services/modelService.ts | 3 ++ tests/fixtures/testModel.ts | 18 +++++++---- ...dynamic-controllers-express-server.spec.ts | 3 ++ tests/integration/express-server.spec.ts | 3 ++ tests/integration/hapi-server.spec.ts | 3 ++ tests/integration/inversify-server.spec.ts | 3 ++ .../koa-server-no-additional-allowed.spec.ts | 3 ++ tests/integration/koa-server.spec.ts | 3 ++ .../definitionsGeneration/definitions.spec.ts | 28 +++++++++++++++++ tests/unit/swagger/schemaDetails3.spec.ts | 30 +++++++++++++++++++ 15 files changed, 140 insertions(+), 8 deletions(-) diff --git a/src/metadataGeneration/tsoa.ts b/src/metadataGeneration/tsoa.ts index a9fbb8c60..07ee7ac2c 100644 --- a/src/metadataGeneration/tsoa.ts +++ b/src/metadataGeneration/tsoa.ts @@ -87,12 +87,13 @@ export namespace Tsoa { | 'any' | 'refEnum' | 'refObject' + | 'objectLiteral' | 'union' | 'intersection'; export type RefTypeLiteral = 'refObject' | 'refEnum'; - export type PrimitiveTypeLiteral = Exclude; + export type PrimitiveTypeLiteral = Exclude; export interface Type { dataType: TypeStringLiteral; @@ -107,6 +108,11 @@ export namespace Tsoa { enums: string[]; } + export interface ObjectLiteralType extends Type { + dataType: 'objectLiteral'; + properties: Property[]; + } + export interface ArrayType extends Type { dataType: 'array'; elementType: Type; diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts index 73d1226dc..f38e3b506 100644 --- a/src/metadataGeneration/typeResolver.ts +++ b/src/metadataGeneration/typeResolver.ts @@ -90,7 +90,22 @@ export class TypeResolver { } if (this.typeNode.kind === ts.SyntaxKind.TypeLiteral) { - return { dataType: 'any' } as Tsoa.Type; + const properties = (this.typeNode as ts.TypeLiteralNode).members + .filter(member => member.kind === ts.SyntaxKind.PropertySignature) + .reduce((res, propertySignature: ts.PropertySignature) => { + const type = new TypeResolver(propertySignature.type as ts.TypeNode, this.current, this.typeNode).resolve(); + + return [ + { + name: (propertySignature.name as ts.Identifier).text, + required: !propertySignature.questionToken, + type, + } as Tsoa.Property, + ...res, + ]; + }, []); + + return { dataType: 'objectLiteral', properties } as Tsoa.ObjectLiteralType; } if (this.typeNode.kind === ts.SyntaxKind.ObjectKeyword) { diff --git a/src/swagger/specGenerator.ts b/src/swagger/specGenerator.ts index 9fbfdbe62..9484d1705 100644 --- a/src/swagger/specGenerator.ts +++ b/src/swagger/specGenerator.ts @@ -79,6 +79,8 @@ export abstract class SpecGenerator { return this.getSwaggerTypeForUnionType(type as Tsoa.UnionType); } else if (type.dataType === 'intersection') { return this.getSwaggerTypeForIntersectionType(type as Tsoa.IntersectionType); + } else if (type.dataType === 'objectLiteral') { + return this.getSwaggerTypeForObjectLiteral(type as Tsoa.ObjectLiteralType); } else { return assertNever(type.dataType); } @@ -87,6 +89,24 @@ export abstract class SpecGenerator { protected abstract getSwaggerTypeForUnionType(type: Tsoa.UnionType); protected abstract getSwaggerTypeForIntersectionType(type: Tsoa.IntersectionType); + public getSwaggerTypeForObjectLiteral(objectLiteral: Tsoa.ObjectLiteralType): Swagger.Schema { + const properties = objectLiteral.properties.reduce((acc, property: Tsoa.Property) => { + return { + [property.name]: this.getSwaggerType(property.type), + ...acc, + }; + }, {}); + + const required = objectLiteral.properties.filter(prop => prop.required).map(prop => prop.name); + + // An empty list required: [] is not valid. + // If all properties are optional, do not specify the required keyword. + return { + properties, + ...(required && required.length && { required }), + type: 'object', + }; + } protected getSwaggerTypeForReferenceType(referenceType: Tsoa.ReferenceType): Swagger.BaseSchema { return { diff --git a/tests/fixtures/controllers/getController.ts b/tests/fixtures/controllers/getController.ts index 85f092611..dd38ab385 100644 --- a/tests/fixtures/controllers/getController.ts +++ b/tests/fixtures/controllers/getController.ts @@ -24,6 +24,9 @@ export class GetTestController extends Controller { modelsArray: new Array(), numberArray: [1, 2, 3], numberValue: 1, + objLiteral: { + name: 'a string', + }, object: { a: 'a', }, diff --git a/tests/fixtures/inversify/managedService.ts b/tests/fixtures/inversify/managedService.ts index 77ffb99fa..fcd13ad01 100644 --- a/tests/fixtures/inversify/managedService.ts +++ b/tests/fixtures/inversify/managedService.ts @@ -16,6 +16,9 @@ export class ManagedService { modelsArray: new Array(), numberArray: [1, 2, 3], numberValue: 1, + objLiteral: { + name: 'hello', + }, object: { a: 'a', }, diff --git a/tests/fixtures/services/modelService.ts b/tests/fixtures/services/modelService.ts index 2ae18c16a..3cb1eae8f 100644 --- a/tests/fixtures/services/modelService.ts +++ b/tests/fixtures/services/modelService.ts @@ -14,6 +14,9 @@ export class ModelService { modelsArray: new Array(), numberArray: [1, 2, 3], numberValue: 1, + objLiteral: { + name: 'hello', + }, object: { a: 'a', }, diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts index 9c9c0c93d..0d826a228 100644 --- a/tests/fixtures/testModel.ts +++ b/tests/fixtures/testModel.ts @@ -75,6 +75,18 @@ export interface TestModel extends Model { genericNestedArrayKeyword2?: GenericRequest>; genericNestedArrayCharacter2?: GenericRequest; mixedUnion?: string | TypeAliasModel1; + + objLiteral: { + name: string; + nested?: { + bool: boolean; + optional?: number; + allOptional: { + one?: string; + two?: string; + }; + }; + }; } export interface TypeAliasModel1 { @@ -333,12 +345,6 @@ export class TestClassModel extends TestClassBaseModel { stringProperty: string; protected protectedStringProperty: string; - public static typeLiterals = { - booleanTypeLiteral: { $type: Boolean }, - numberTypeLiteral: { $type: Number }, - stringTypeLiteral: { $type: String }, - }; - /** * @param publicConstructorVar This is a description for publicConstructorVar */ diff --git a/tests/integration/dynamic-controllers-express-server.spec.ts b/tests/integration/dynamic-controllers-express-server.spec.ts index 84657a0cd..eb77b3726 100644 --- a/tests/integration/dynamic-controllers-express-server.spec.ts +++ b/tests/integration/dynamic-controllers-express-server.spec.ts @@ -937,6 +937,9 @@ describe('Express Server', () => { }, numberArray: [1, 2], numberValue: 5, + objLiteral: { + name: 'hello', + }, object: { foo: 'bar' }, objectArray: [{ foo1: 'bar1' }, { foo2: 'bar2' }], optionalString: 'test1234', diff --git a/tests/integration/express-server.spec.ts b/tests/integration/express-server.spec.ts index e3219e853..3601bdf3d 100644 --- a/tests/integration/express-server.spec.ts +++ b/tests/integration/express-server.spec.ts @@ -937,6 +937,9 @@ describe('Express Server', () => { }, numberArray: [1, 2], numberValue: 5, + objLiteral: { + name: 'hello', + }, object: { foo: 'bar' }, objectArray: [{ foo1: 'bar1' }, { foo2: 'bar2' }], optionalString: 'test1234', diff --git a/tests/integration/hapi-server.spec.ts b/tests/integration/hapi-server.spec.ts index 25e409e99..9b99bc07d 100644 --- a/tests/integration/hapi-server.spec.ts +++ b/tests/integration/hapi-server.spec.ts @@ -866,6 +866,9 @@ describe('Hapi Server', () => { }, numberArray: [1, 2], numberValue: 5, + objLiteral: { + name: 'hello', + }, object: { foo: 'bar' }, objectArray: [{ foo1: 'bar1' }, { foo2: 'bar2' }], optionalString: 'test1234', diff --git a/tests/integration/inversify-server.spec.ts b/tests/integration/inversify-server.spec.ts index 3360559ad..38b0f5b8b 100644 --- a/tests/integration/inversify-server.spec.ts +++ b/tests/integration/inversify-server.spec.ts @@ -41,6 +41,9 @@ describe('Inversify Express Server', () => { }, numberArray: [1, 2, 3], numberValue: 1, + objLiteral: { + name: 'hello', + }, object: { a: 'a', }, diff --git a/tests/integration/koa-server-no-additional-allowed.spec.ts b/tests/integration/koa-server-no-additional-allowed.spec.ts index 4aeecde58..43771871a 100644 --- a/tests/integration/koa-server-no-additional-allowed.spec.ts +++ b/tests/integration/koa-server-no-additional-allowed.spec.ts @@ -355,6 +355,9 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { modelsArray: [{ email: 'test@test.com', id: 1 }], numberArray: [1, 2], numberValue: 5, + objLiteral: { + name: 'hello', + }, object: { foo: 'bar' }, objectArray: [{ foo1: 'bar1' }, { foo2: 'bar2' }], optionalString: 'test1234', diff --git a/tests/integration/koa-server.spec.ts b/tests/integration/koa-server.spec.ts index 9ca9a309b..5947df7b0 100644 --- a/tests/integration/koa-server.spec.ts +++ b/tests/integration/koa-server.spec.ts @@ -851,6 +851,9 @@ describe('Koa Server', () => { modelsArray: [{ email: 'test@test.com', id: 1 }], numberArray: [1, 2], numberValue: 5, + objLiteral: { + name: 'hello', + }, object: { foo: 'bar' }, objectArray: [{ foo1: 'bar1' }, { foo2: 'bar2' }], optionalString: 'test1234', diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts index 7cedc6c71..20c7b4b88 100644 --- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts +++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts @@ -422,6 +422,29 @@ describe('Definition generation', () => { expect(propertySchema.type).to.eq('object', `for property ${propertyName}`); expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); }, + objLiteral: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.include({ + properties: { + name: { + type: 'string', + }, + nested: { + properties: { + allOptional: { + properties: { one: { type: 'string' }, two: { type: 'string' } }, + type: 'object', + }, + bool: { type: 'boolean' }, + optional: { format: 'double', type: 'number' }, + }, + required: ['allOptional', 'bool'], + type: 'object', + }, + }, + required: ['name'], + type: 'object', + }); + }, }; Object.keys(assertionsPerProperty).forEach(aPropertyName => { @@ -438,6 +461,11 @@ describe('Definition generation', () => { Object.keys(definition.properties!).length, `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`, ); + + expect(Object.keys(assertionsPerProperty)).to.length( + Object.keys(definition.properties!).length, + `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`, + ); }); }); diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts index f481cde01..2e85b885e 100644 --- a/tests/unit/swagger/schemaDetails3.spec.ts +++ b/tests/unit/swagger/schemaDetails3.spec.ts @@ -214,6 +214,29 @@ describe('Definition generation for OpenAPI 3.0.0', () => { expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`); expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); }, + objLiteral: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.include({ + properties: { + name: { + type: 'string', + }, + nested: { + properties: { + allOptional: { + properties: { one: { type: 'string' }, two: { type: 'string' } }, + type: 'object', + }, + bool: { type: 'boolean' }, + optional: { format: 'double', type: 'number' }, + }, + required: ['allOptional', 'bool'], + type: 'object', + }, + }, + required: ['name'], + type: 'object', + }); + }, object: (propertyName, propertySchema) => { expect(propertySchema.type).to.eq('object', `for property ${propertyName}`); if (currentSpec.specName === 'specWithNoImplicitExtras') { @@ -481,6 +504,13 @@ describe('Definition generation for OpenAPI 3.0.0', () => { `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`, ); }); + + it('should have only created schemas for properties on the TypeScript interface', () => { + expect(Object.keys(assertionsPerProperty)).to.length( + Object.keys(testModel.properties!).length, + `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`, + ); + }); }); }); });