diff --git a/src/interfaces/metadata.ts b/src/interfaces/metadata.ts new file mode 100644 index 000000000..156e304b1 --- /dev/null +++ b/src/interfaces/metadata.ts @@ -0,0 +1,20 @@ +export interface PropertyMetadata { + format?: string; + title?: string; + description?: string; + default?: any; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: boolean; + minimum?: number; + exclusiveMinimum?: boolean; + maxLength?: number; + minLength?: number; + pattern?: string; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + maxProperties?: number; + minProperties?: number; + type?: string; +} diff --git a/src/metadataGeneration/metadataGenerator.ts b/src/metadataGeneration/metadataGenerator.ts index c57e76e0a..e0750b5b1 100644 --- a/src/metadataGeneration/metadataGenerator.ts +++ b/src/metadataGeneration/metadataGenerator.ts @@ -1,5 +1,6 @@ import * as ts from 'typescript'; import { ControllerGenerator } from './controllerGenerator'; +import {PropertyMetadata} from '../interfaces/metadata'; export class MetadataGenerator { public static current: MetadataGenerator; @@ -121,6 +122,7 @@ export interface Property { name: string; type: Type; required: boolean; + metadata?: PropertyMetadata; } export interface ArrayType { diff --git a/src/metadataGeneration/resolveType.ts b/src/metadataGeneration/resolveType.ts index ec3334143..e0149b33d 100644 --- a/src/metadataGeneration/resolveType.ts +++ b/src/metadataGeneration/resolveType.ts @@ -1,5 +1,7 @@ import * as ts from 'typescript'; -import { MetadataGenerator, Type, ReferenceType, Property } from './metadataGenerator'; +import { MetadataGenerator, Type, ReferenceType, Property, ArrayType } from './metadataGenerator'; +import { ValidationGenerator } from './validationGenerator'; +import * as _ from 'lodash'; const syntaxKindMap: { [kind: number]: string } = {}; syntaxKindMap[ts.SyntaxKind.NumberKeyword] = 'number'; @@ -9,6 +11,7 @@ syntaxKindMap[ts.SyntaxKind.VoidKeyword] = 'void'; const localReferenceTypeCache: { [typeName: string]: ReferenceType } = {}; const inProgressTypes: { [typeName: string]: boolean } = {}; +const validationGenerator = new ValidationGenerator(); type UsableDeclaration = ts.InterfaceDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration; export function ResolveType(typeNode: ts.TypeNode): Type { @@ -40,28 +43,40 @@ export function ResolveType(typeNode: ts.TypeNode): Type { return ResolveType(typeReference); } - const referenceType = generateReferenceType(typeReference.typeName.text); + let referenceType: ReferenceType; + + if (typeReference.typeArguments && typeReference.typeArguments.length === 1) { + let typeT: ts.TypeNode[] = typeReference.typeArguments as ts.TypeNode[]; + referenceType = generateReferenceType(typeReference.typeName.text, typeT); + } else { + referenceType = generateReferenceType(typeReference.typeName.text); + } + MetadataGenerator.current.AddReferenceType(referenceType); return referenceType; } -function generateReferenceType(typeName: string): ReferenceType { +function generateReferenceType(typeName: string, genericTypes?: ts.TypeNode[]): ReferenceType { try { - const existingType = localReferenceTypeCache[typeName]; + + const typeNameWithGenerics = getTypeName(typeName, genericTypes); + const existingType = localReferenceTypeCache[typeNameWithGenerics]; if (existingType) { return existingType; } - if (inProgressTypes[typeName]) { - return createCircularDependencyResolver(typeName); + if (inProgressTypes[typeNameWithGenerics]) { + return createCircularDependencyResolver(typeNameWithGenerics); } - inProgressTypes[typeName] = true; + inProgressTypes[typeNameWithGenerics] = true; const modelTypeDeclaration = getModelTypeDeclaration(typeName); - const properties = getModelTypeProperties(modelTypeDeclaration); + + + const properties = getModelTypeProperties(modelTypeDeclaration, genericTypes); const referenceType: ReferenceType = { description: getModelDescription(modelTypeDeclaration), - name: typeName, + name: typeNameWithGenerics, properties: properties }; if (modelTypeDeclaration.kind === ts.SyntaxKind.TypeAliasDeclaration) { @@ -75,14 +90,56 @@ function generateReferenceType(typeName: string): ReferenceType { const extendedProperties = getInheritedProperties(modelTypeDeclaration); referenceType.properties = referenceType.properties.concat(extendedProperties); - localReferenceTypeCache[typeName] = referenceType; + localReferenceTypeCache[typeNameWithGenerics] = referenceType; + return referenceType; } catch (err) { - console.error(`There was a problem resolving type of '${typeName}'.`); + console.error(`There was a problem resolving type of '${getTypeName(typeName, genericTypes)}'.`); throw err; } } +function getTypeName(typeName: string, genericTypes?: ts.TypeNode[]): string { + if (!genericTypes || !genericTypes.length) { + return typeName; + } + let names = genericTypes.map((t) => { + return getAnyTypeName(t); + }); + + return typeName + '<' + names.join(', ') + '>'; +} + +function getAnyTypeName(typeNode: ts.TypeNode): string { + const primitiveType = syntaxKindMap[typeNode.kind]; + if (primitiveType) { + return primitiveType; + } + + if (typeNode.kind === ts.SyntaxKind.ArrayType) { + const arrayType = typeNode as ts.ArrayTypeNode; + return getAnyTypeName(arrayType.elementType) + '[]'; + } + + if (typeNode.kind === ts.SyntaxKind.UnionType) { + return 'object'; + } + + if (typeNode.kind !== ts.SyntaxKind.TypeReference) { + throw new Error(`Unknown type: ${ts.SyntaxKind[typeNode.kind]}`); + } + + const typeReference = typeNode as ts.TypeReferenceNode; + try { + return (typeReference.typeName as ts.Identifier).text; + } catch (e) { + // idk what would hit this? probably needs more testing + console.error(e); + return typeNode.toString(); + } + +} + function createCircularDependencyResolver(typeName: string) { const referenceType = { description: '', @@ -127,22 +184,52 @@ function getModelTypeDeclaration(typeName: string) { return modelTypes[0]; } -function getModelTypeProperties(node: UsableDeclaration) { +function getModelTypeProperties(node: UsableDeclaration, genericTypes?: ts.TypeNode[]) { if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { const interfaceDeclaration = node as ts.InterfaceDeclaration; return interfaceDeclaration.members .filter(member => member.kind === ts.SyntaxKind.PropertySignature) .map((property: any) => { + const propertyDeclaration = property as ts.PropertyDeclaration; const identifier = propertyDeclaration.name as ts.Identifier; if (!propertyDeclaration.type) { throw new Error('No valid type found for property declaration.'); } + // Declare a variable that can be overridden if needed + let aType = propertyDeclaration.type; + + // aType.kind will always be a TypeReference when the property of Interface is of type T + if (aType.kind === ts.SyntaxKind.TypeReference && genericTypes && genericTypes.length && node.typeParameters) { + + // The type definitions are conviently located on the object which allow us to map -> to the genericTypes + let typeParams = _.map(node.typeParameters, (typeParam: ts.TypeParameterDeclaration) => { + return typeParam.name.text; + }); + + // I am not sure in what cases + const typeIdentifier = (aType as ts.TypeReferenceNode).typeName; + let typeIdentifierName: string; + + // typeIdentifier can either be a Identifier or a QualifiedName + if ((typeIdentifier as ts.Identifier).text) { + typeIdentifierName = (typeIdentifier as ts.Identifier).text; + } else { + typeIdentifierName = (typeIdentifier as ts.QualifiedName).right.text; + } + + // I could not produce a situation where this did not find it so its possible this check is irrelevant + const indexOfType = _.indexOf(typeParams, typeIdentifierName); + if (indexOfType >= 0) { + aType = genericTypes[indexOfType] as ts.TypeNode; + } + } + return { description: getNodeDescription(propertyDeclaration), name: identifier.text, required: !property.questionToken, - type: ResolveType(propertyDeclaration.type) + type: ResolveType(aType) }; }); } @@ -176,16 +263,57 @@ function getModelTypeProperties(node: UsableDeclaration) { const identifier = declaration.name as ts.Identifier; if (!declaration.type) { throw new Error('No valid type found for property declaration.'); } + const decorators = getPropertyInfo(declaration); + + const type = ResolveType(declaration.type); + let propType = ''; + if (typeof type === 'string') { + propType = type; + } else if ((type as ArrayType).elementType) { + propType = 'array'; + } + + let metadata; + if (decorators && propType) { + metadata = validationGenerator.getProperties(decorators, propType); + } return { description: getNodeDescription(declaration), + metadata: metadata, name: identifier.text, required: !declaration.questionToken, - type: ResolveType(declaration.type) + type: type }; }); } +function getPropertyInfo(prop: ts.PropertyDeclaration | ts.ParameterDeclaration) { + if (prop.decorators) { + console.error('I found a property with decorators!'); + return prop.decorators + .map(d => d.expression as ts.CallExpression) + .map(e => { + let decoratorName = (e.expression as ts.Identifier).text; + let args = e.arguments.map((arg) => { + if (arg) { + return (arg as ts.Identifier).text; + } + return undefined; + }) + .filter((val) => val !== undefined); + console.error('found decorator: ', decoratorName); + console.error('args: ', args); + return { + args: args, + name: decoratorName + }; + }); + } else { + return null; + } +} + function hasPublicModifier(node: ts.Node) { return !node.modifiers || node.modifiers.every(modifier => { return modifier.kind !== ts.SyntaxKind.ProtectedKeyword && modifier.kind !== ts.SyntaxKind.PrivateKeyword; diff --git a/src/metadataGeneration/validationGenerator.ts b/src/metadataGeneration/validationGenerator.ts new file mode 100644 index 000000000..947681f56 --- /dev/null +++ b/src/metadataGeneration/validationGenerator.ts @@ -0,0 +1,109 @@ +import {PropertyMetadata} from '../interfaces/metadata'; +import * as _ from 'lodash'; + +export interface DecorationConfig { + name: string; + args: any[]; +} + +interface ValidationConfig { + [type: string]: ValidationConfigDeclaration; +} + +interface ValidationFunction { + (...args: any[]): PropertyMetadata; +} + +interface ValidationConfigDeclaration { + [type: string]: PropertyMetadata | ValidationFunction; +} + +const internalMapping: ValidationConfig = { + array: { + arraymaxsize: (i) => { + return { + maxItems: parseInt(i, 10) + }; + }, + arrayminsize: (i) => { + return { + minItems: parseInt(i, 10) + }; + } + }, + number: { + isint: { + format: 'int64', + type: 'integer' + }, + isnegative: { + exclusiveMaximum: true, + maximum: 0, + }, + ispositive: { + exclusiveMinimum: true, + minimum: 0, + }, + max: (i) => { + return { + maximum: parseFloat(i) + }; + }, + min: (i) => { + return { + maximum: parseFloat(i) + }; + } + }, + string: { + fake: { + minimum: 0 + }, + fakeme: (i) => { + return { + maximum: parseInt(i, 10) + 99 + }; + }, + maxlength: (i) => { + return { + maxLength: parseInt(i, 10) + }; + }, + minlength: (i) => { + return { + minLength: parseInt(i, 10) + }; + } + } +}; + +export class ValidationGenerator { + private config: ValidationConfig; + constructor() { + this.config = internalMapping; + } + + public getProperties(decorators: DecorationConfig[], type: string) { + let configType = this.config[type]; + + if (!configType) { return {}; } + + let ret = {}; + + decorators.map((dec) => { + let decName = dec.name.toLowerCase(); + if (!configType[decName]) { + return; + } + let combine: Object; + if (typeof configType[decName] === 'function') { + combine = (configType[decName] as ValidationFunction).apply(null, dec.args); + } else { + combine = configType[decName] as PropertyMetadata; + } + _.assign(ret, combine); + }); + + return ret; + } +} diff --git a/src/routeGeneration/routeGenerator.ts b/src/routeGeneration/routeGenerator.ts index 1681457e6..fcf41b06e 100644 --- a/src/routeGeneration/routeGenerator.ts +++ b/src/routeGeneration/routeGenerator.ts @@ -78,7 +78,7 @@ export class RouteGenerator { }, {{/each}} }; - `.concat(middlewareTemplate)); + `.concat(middlewareTemplate), { noEscape: true }); const authenticationModule = this.options.authenticationModule ? this.getRelativeImportPath(this.options.authenticationModule) : undefined; const iocModule = this.options.iocModule ? this.getRelativeImportPath(this.options.iocModule) : undefined; diff --git a/src/swagger/specGenerator.ts b/src/swagger/specGenerator.ts index 6feb20b47..156459667 100644 --- a/src/swagger/specGenerator.ts +++ b/src/swagger/specGenerator.ts @@ -3,6 +3,7 @@ import { Metadata, Type, ArrayType, ReferenceType, PrimitiveType, Property, Meth import { Swagger } from './swagger'; import * as fs from 'fs'; import * as mkdirp from 'mkdirp'; +import * as _ from 'lodash'; export class SpecGenerator { constructor(private readonly metadata: Metadata, private readonly config: SwaggerConfig) { } @@ -176,6 +177,9 @@ export class SpecGenerator { if (!swaggerType.$ref) { swaggerType.description = property.description; } + if (property.metadata) { + _.assign(swaggerType, property.metadata); + } swaggerProperties[property.name] = swaggerType; }); diff --git a/src/swagger/swagger.ts b/src/swagger/swagger.ts index ffd46e196..bb7d3e3f5 100644 --- a/src/swagger/swagger.ts +++ b/src/swagger/swagger.ts @@ -1,3 +1,5 @@ +import {PropertyMetadata} from '../interfaces/metadata'; + export namespace Swagger { export interface Info { title?: string; @@ -107,26 +109,8 @@ export namespace Swagger { examples?: { [exampleName: string]: Example }; } - export interface BaseSchema { - format?: string; - title?: string; - description?: string; - default?: string | boolean | number | Object; - multipleOf?: number; - maximum?: number; - exclusiveMaximum?: number; - minimum?: number; - exclusiveMinimum?: number; - maxLength?: number; - minLength?: number; - pattern?: string; - maxItems?: number; - minItems?: number; - uniqueItems?: boolean; - maxProperties?: number; - minProperties?: number; + export interface BaseSchema extends PropertyMetadata { enum?: [string | boolean | number | Object]; - type?: string; items?: Schema | [Schema]; } diff --git a/tests/fixtures/controllers/getController.ts b/tests/fixtures/controllers/getController.ts index 9a2eb9b77..df443b3c3 100644 --- a/tests/fixtures/controllers/getController.ts +++ b/tests/fixtures/controllers/getController.ts @@ -4,7 +4,7 @@ import { Get } from '../../../src/decorators/methods'; import { Controller } from '../../../src/interfaces/controller'; import { ModelService } from '../services/modelService'; import { Route } from '../../../src/decorators/route'; -import { TestModel, TestSubModel, TestClassModel } from '../testModel'; +import { GenericModel, TestModel, TestSubModel, TestClassModel } from '../testModel'; import { Tags } from '../../../src/decorators/tags'; @Route('GetTest') @@ -122,6 +122,36 @@ export class GetTestController extends Controller { public async getBuffer(@Query() buffer: Buffer): Promise { return new Buffer('testbuffer'); } + + @Get('GenericModel') + public async getGenericModel(): Promise> { + return { + result: new ModelService().getModel() + }; + } + + @Get('GenericModelArray') + public async getGenericModelArray(): Promise> { + return { + result: [ + new ModelService().getModel() + ] + }; + } + + @Get('GenericPrimitive') + public async getGenericPrimitive(): Promise> { + return { + result: new ModelService().getModel().stringValue + }; + } + + @Get('GenericPrimitiveArray') + public async getGenericPrimitiveArray(): Promise> { + return { + result: new ModelService().getModel().stringArray + }; + } } export interface ErrorResponse { diff --git a/tests/fixtures/controllers/postController.ts b/tests/fixtures/controllers/postController.ts index c15b7d982..34a34e314 100644 --- a/tests/fixtures/controllers/postController.ts +++ b/tests/fixtures/controllers/postController.ts @@ -1,7 +1,7 @@ import { Route } from '../../../src/decorators/route'; import { Body, Query } from '../../../src/decorators/parameter'; import { Post, Patch } from '../../../src/decorators/methods'; -import { TestModel, TestClassModel } from '../testModel'; +import { GenericRequest, TestModel, TestClassModel } from '../testModel'; import { ModelService } from '../services/modelService'; @Route('PostTest') @@ -48,4 +48,9 @@ export class PostTestController { public async postWithBodyAndQueryParams(@Body() model: TestModel, @Query() query: string): Promise { return new ModelService().getModel(); } + + @Post('GenericBody') + public async getGenericRequest(@Body() genericReq: GenericRequest): Promise { + return genericReq.value; + } } diff --git a/tests/fixtures/express/routes.ts b/tests/fixtures/express/routes.ts index cc9f7e05d..a171f8de8 100644 --- a/tests/fixtures/express/routes.ts +++ b/tests/fixtures/express/routes.ts @@ -37,13 +37,30 @@ const models: any = { 'publicStringProperty': { typeName: 'string', required: true }, 'optionalPublicStringProperty': { typeName: 'string', required: false }, 'stringProperty': { typeName: 'string', required: true }, + 'testDecorator': { typeName: 'string', required: false }, 'publicConstructorVar': { typeName: 'string', required: true }, 'optionalPublicConstructorVar': { typeName: 'string', required: false }, 'id': { typeName: 'number', required: true }, }, + 'GenericRequest': { + 'name': { typeName: 'string', required: true }, + 'value': { typeName: 'TestModel', required: true }, + }, 'Result': { 'value': { typeName: 'object', required: true }, }, + 'GenericModel': { + 'result': { typeName: 'TestModel', required: true }, + }, + 'GenericModel': { + 'result': { typeName: 'array', required: true, arrayType: 'TestModel' }, + }, + 'GenericModel': { + 'result': { typeName: 'string', required: true }, + }, + 'GenericModel': { + 'result': { typeName: 'array', required: true, arrayType: 'string' }, + }, 'ErrorResponseModel': { 'status': { typeName: 'number', required: true }, 'message': { typeName: 'string', required: true }, @@ -315,6 +332,29 @@ export function RegisterRoutes(app: any) { } promiseHandler(promise, statusCode, response, next); }); + app.post('/v1/PostTest/GenericBody', + function(request: any, response: any, next: any) { + const args = { + 'genericReq': { name: 'genericReq', typeName: 'GenericRequest', required: true, in: 'body', }, + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request); + } catch (err) { + return next(err); + } + + const controller = new PostTestController(); + + + const promise = controller.getGenericRequest.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + promiseHandler(promise, statusCode, response, next); + }); app.patch('/v1/PatchTest', function(request: any, response: any, next: any) { const args = { @@ -679,6 +719,94 @@ export function RegisterRoutes(app: any) { } promiseHandler(promise, statusCode, response, next); }); + app.get('/v1/GetTest/GenericModel', + function(request: any, response: any, next: any) { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request); + } catch (err) { + return next(err); + } + + const controller = new GetTestController(); + + + const promise = controller.getGenericModel.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + promiseHandler(promise, statusCode, response, next); + }); + app.get('/v1/GetTest/GenericModelArray', + function(request: any, response: any, next: any) { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request); + } catch (err) { + return next(err); + } + + const controller = new GetTestController(); + + + const promise = controller.getGenericModelArray.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + promiseHandler(promise, statusCode, response, next); + }); + app.get('/v1/GetTest/GenericPrimitive', + function(request: any, response: any, next: any) { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request); + } catch (err) { + return next(err); + } + + const controller = new GetTestController(); + + + const promise = controller.getGenericPrimitive.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + promiseHandler(promise, statusCode, response, next); + }); + app.get('/v1/GetTest/GenericPrimitiveArray', + function(request: any, response: any, next: any) { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request); + } catch (err) { + return next(err); + } + + const controller = new GetTestController(); + + + const promise = controller.getGenericPrimitiveArray.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + promiseHandler(promise, statusCode, response, next); + }); app.delete('/v1/DeleteTest', function(request: any, response: any, next: any) { const args = { diff --git a/tests/fixtures/hapi/routes.ts b/tests/fixtures/hapi/routes.ts index b0bb070a1..b5e684d21 100644 --- a/tests/fixtures/hapi/routes.ts +++ b/tests/fixtures/hapi/routes.ts @@ -37,13 +37,30 @@ const models: any = { 'publicStringProperty': { typeName: 'string', required: true }, 'optionalPublicStringProperty': { typeName: 'string', required: false }, 'stringProperty': { typeName: 'string', required: true }, + 'testDecorator': { typeName: 'string', required: false }, 'publicConstructorVar': { typeName: 'string', required: true }, 'optionalPublicConstructorVar': { typeName: 'string', required: false }, 'id': { typeName: 'number', required: true }, }, + 'GenericRequest': { + 'name': { typeName: 'string', required: true }, + 'value': { typeName: 'TestModel', required: true }, + }, 'Result': { 'value': { typeName: 'object', required: true }, }, + 'GenericModel': { + 'result': { typeName: 'TestModel', required: true }, + }, + 'GenericModel': { + 'result': { typeName: 'array', required: true, arrayType: 'TestModel' }, + }, + 'GenericModel': { + 'result': { typeName: 'string', required: true }, + }, + 'GenericModel': { + 'result': { typeName: 'array', required: true, arrayType: 'string' }, + }, 'ErrorResponseModel': { 'status': { typeName: 'number', required: true }, 'message': { typeName: 'string', required: true }, @@ -360,6 +377,33 @@ export function RegisterRoutes(server: hapi.Server) { } } }); + server.route({ + method: 'post', + path: '/v1/PostTest/GenericBody', + config: { + handler: (request: any, reply) => { + const args = { + 'genericReq': { name: 'genericReq', typeName: 'GenericRequest', required: true, in: 'body', }, + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request); + } catch (err) { + return reply(err).code(err.status || 500); + } + + const controller = new PostTestController(); + + const promise = controller.getGenericRequest.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + return promiseHandler(promise, statusCode, request, reply); + } + } + }); server.route({ method: 'patch', path: '/v1/PatchTest', @@ -788,6 +832,110 @@ export function RegisterRoutes(server: hapi.Server) { } } }); + server.route({ + method: 'get', + path: '/v1/GetTest/GenericModel', + config: { + handler: (request: any, reply) => { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request); + } catch (err) { + return reply(err).code(err.status || 500); + } + + const controller = new GetTestController(); + + const promise = controller.getGenericModel.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + return promiseHandler(promise, statusCode, request, reply); + } + } + }); + server.route({ + method: 'get', + path: '/v1/GetTest/GenericModelArray', + config: { + handler: (request: any, reply) => { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request); + } catch (err) { + return reply(err).code(err.status || 500); + } + + const controller = new GetTestController(); + + const promise = controller.getGenericModelArray.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + return promiseHandler(promise, statusCode, request, reply); + } + } + }); + server.route({ + method: 'get', + path: '/v1/GetTest/GenericPrimitive', + config: { + handler: (request: any, reply) => { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request); + } catch (err) { + return reply(err).code(err.status || 500); + } + + const controller = new GetTestController(); + + const promise = controller.getGenericPrimitive.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + return promiseHandler(promise, statusCode, request, reply); + } + } + }); + server.route({ + method: 'get', + path: '/v1/GetTest/GenericPrimitiveArray', + config: { + handler: (request: any, reply) => { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request); + } catch (err) { + return reply(err).code(err.status || 500); + } + + const controller = new GetTestController(); + + const promise = controller.getGenericPrimitiveArray.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + return promiseHandler(promise, statusCode, request, reply); + } + } + }); server.route({ method: 'delete', path: '/v1/DeleteTest', @@ -1113,8 +1261,8 @@ export function RegisterRoutes(server: hapi.Server) { pre: [ { method: authenticateMiddleware('api_key' - ) -} + ) + } ], handler: (request: any, reply) => { const args = { @@ -1414,8 +1562,8 @@ export function RegisterRoutes(server: hapi.Server) { pre: [ { method: authenticateMiddleware('api_key' - ) -} + ) + } ], handler: (request: any, reply) => { const args = { @@ -1451,8 +1599,8 @@ export function RegisterRoutes(server: hapi.Server) { 'write:pets', 'read:pets' ] - ) - } + ) +} ], handler: (request: any, reply) => { const args = { diff --git a/tests/fixtures/koa/routes.ts b/tests/fixtures/koa/routes.ts index ef10b35b1..fda3d8741 100644 --- a/tests/fixtures/koa/routes.ts +++ b/tests/fixtures/koa/routes.ts @@ -37,13 +37,30 @@ const models: any = { 'publicStringProperty': { typeName: 'string', required: true }, 'optionalPublicStringProperty': { typeName: 'string', required: false }, 'stringProperty': { typeName: 'string', required: true }, + 'testDecorator': { typeName: 'string', required: false }, 'publicConstructorVar': { typeName: 'string', required: true }, 'optionalPublicConstructorVar': { typeName: 'string', required: false }, 'id': { typeName: 'number', required: true }, }, + 'GenericRequest': { + 'name': { typeName: 'string', required: true }, + 'value': { typeName: 'TestModel', required: true }, + }, 'Result': { 'value': { typeName: 'object', required: true }, }, + 'GenericModel': { + 'result': { typeName: 'TestModel', required: true }, + }, + 'GenericModel': { + 'result': { typeName: 'array', required: true, arrayType: 'TestModel' }, + }, + 'GenericModel': { + 'result': { typeName: 'string', required: true }, + }, + 'GenericModel': { + 'result': { typeName: 'array', required: true, arrayType: 'string' }, + }, 'ErrorResponseModel': { 'status': { typeName: 'number', required: true }, 'message': { typeName: 'string', required: true }, @@ -336,6 +353,31 @@ export function RegisterRoutes(router: KoaRouter) { statusCode = (controller as Controller).getStatus(); } + return promiseHandler(promise, statusCode, context, next); + }); + router.post('/v1/PostTest/GenericBody', + async (context, next) => { + const args = { + 'genericReq': { name: 'genericReq', typeName: 'GenericRequest', required: true, in: 'body', }, + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, context); + } catch (error) { + context.status = error.status || 500; + context.body = error; + return next(); + } + + const controller = new PostTestController(); + + const promise = controller.getGenericRequest.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + return promiseHandler(promise, statusCode, context, next); }); router.patch('/v1/PatchTest', @@ -732,6 +774,102 @@ export function RegisterRoutes(router: KoaRouter) { statusCode = (controller as Controller).getStatus(); } + return promiseHandler(promise, statusCode, context, next); + }); + router.get('/v1/GetTest/GenericModel', + async (context, next) => { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, context); + } catch (error) { + context.status = error.status || 500; + context.body = error; + return next(); + } + + const controller = new GetTestController(); + + const promise = controller.getGenericModel.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + + return promiseHandler(promise, statusCode, context, next); + }); + router.get('/v1/GetTest/GenericModelArray', + async (context, next) => { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, context); + } catch (error) { + context.status = error.status || 500; + context.body = error; + return next(); + } + + const controller = new GetTestController(); + + const promise = controller.getGenericModelArray.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + + return promiseHandler(promise, statusCode, context, next); + }); + router.get('/v1/GetTest/GenericPrimitive', + async (context, next) => { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, context); + } catch (error) { + context.status = error.status || 500; + context.body = error; + return next(); + } + + const controller = new GetTestController(); + + const promise = controller.getGenericPrimitive.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + + return promiseHandler(promise, statusCode, context, next); + }); + router.get('/v1/GetTest/GenericPrimitiveArray', + async (context, next) => { + const args = { + }; + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, context); + } catch (error) { + context.status = error.status || 500; + context.body = error; + return next(); + } + + const controller = new GetTestController(); + + const promise = controller.getGenericPrimitiveArray.apply(controller, validatedArgs); + let statusCode = undefined; + if (controller instanceof Controller) { + statusCode = (controller as Controller).getStatus(); + } + return promiseHandler(promise, statusCode, context, next); }); router.delete('/v1/DeleteTest', diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts index 54e496b97..5c2787004 100644 --- a/tests/fixtures/testModel.ts +++ b/tests/fixtures/testModel.ts @@ -1,3 +1,10 @@ +let fake = function fake(...values: string[]): any { + return () => { return; }; +}; +let fakeMe = function fake(a: number, ...values: string[]): any { + return () => { return; }; +}; + /** * This is a description of a model */ @@ -72,6 +79,9 @@ export class TestClassModel extends TestClassBaseModel { stringProperty: string; protected protectedStringProperty: string; + @fake() + @fakeMe(1) + public testDecorator?: string; /** * @param publicConstructorVar This is a description for publicConstructorVar */ @@ -83,3 +93,12 @@ export class TestClassModel extends TestClassBaseModel { super(); } } + +export interface GenericModel { + result: T; +} + +export interface GenericRequest { + name: string; + value: T; +} diff --git a/tests/integration/express-server.spec.ts b/tests/integration/express-server.spec.ts index fc8b0d691..e9237eafe 100644 --- a/tests/integration/express-server.spec.ts +++ b/tests/integration/express-server.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import { app } from '../fixtures/express/server'; -import { TestModel, TestClassModel, UserResponseModel, ParameterTestModel } from '../fixtures/testModel'; +import { GenericModel, GenericRequest, TestModel, TestClassModel, UserResponseModel, ParameterTestModel } from '../fixtures/testModel'; import * as chai from 'chai'; import * as request from 'supertest'; import { base64image } from '../fixtures/base64image'; @@ -265,6 +265,46 @@ describe('Express Server', () => { expect(model.human).to.equal(true); }); }); + + it('can get request with generic type', () => { + return verifyGetRequest(basePath + '/GetTest/GenericModel', (err, res) => { + const model = res.body as GenericModel; + expect(model.result.id).to.equal(1); + }); + }); + + it('can get request with generic array', () => { + return verifyGetRequest(basePath + '/GetTest/GenericModelArray', (err, res) => { + const model = res.body as GenericModel; + expect(model.result[0].id).to.equal(1); + }); + }); + + it('can get request with generic primative type', () => { + return verifyGetRequest(basePath + '/GetTest/GenericPrimitive', (err, res) => { + const model = res.body as GenericModel; + expect(model.result).to.equal('a string'); + }); + }); + + it('can get request with generic primative array', () => { + return verifyGetRequest(basePath + '/GetTest/GenericPrimitiveArray', (err, res) => { + const model = res.body as GenericModel; + expect(model.result[0]).to.equal('string one'); + }); + }); + + it('can post request with a generic body', () => { + + const data: GenericRequest = { + name: 'something', + value: getFakeModel() + }; + return verifyPostRequest(basePath + '/PostTest/GenericBody', data, (err, res) => { + const model = res.body as TestModel; + expect(model.id).to.equal(1); + }); + }); }); function verifyGetRequest(path: string, verifyResponse: (err: any, res: request.Response) => any, expectedStatus?: number) { diff --git a/tests/integration/hapi-server.spec.ts b/tests/integration/hapi-server.spec.ts index ba2b7e4b6..1cb332f9a 100644 --- a/tests/integration/hapi-server.spec.ts +++ b/tests/integration/hapi-server.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import { server } from '../fixtures/hapi/server'; -import { TestModel, TestClassModel, Model, ParameterTestModel } from '../fixtures/testModel'; +import { GenericModel, GenericRequest, TestModel, TestClassModel, Model, ParameterTestModel } from '../fixtures/testModel'; import * as chai from 'chai'; import * as request from 'supertest'; @@ -252,6 +252,46 @@ describe('Hapi Server', () => { expect(model.human).to.equal(true); }); }); + + it('can get request with generic type', () => { + return verifyGetRequest(basePath + '/GetTest/GenericModel', (err, res) => { + const model = res.body as GenericModel; + expect(model.result.id).to.equal(1); + }); + }); + + it('can get request with generic array', () => { + return verifyGetRequest(basePath + '/GetTest/GenericModelArray', (err, res) => { + const model = res.body as GenericModel; + expect(model.result[0].id).to.equal(1); + }); + }); + + it('can get request with generic primative type', () => { + return verifyGetRequest(basePath + '/GetTest/GenericPrimitive', (err, res) => { + const model = res.body as GenericModel; + expect(model.result).to.equal('a string'); + }); + }); + + it('can get request with generic primative array', () => { + return verifyGetRequest(basePath + '/GetTest/GenericPrimitiveArray', (err, res) => { + const model = res.body as GenericModel; + expect(model.result[0]).to.equal('string one'); + }); + }); + + it('can post request with a generic body', () => { + + const data: GenericRequest = { + name: 'something', + value: getFakeModel() + }; + return verifyPostRequest(basePath + '/PostTest/GenericBody', data, (err, res) => { + const model = res.body as TestModel; + expect(model.id).to.equal(1); + }); + }); }); function verifyGetRequest(path: string, verifyResponse: (err: any, res: request.Response) => any, expectedStatus?: number) { diff --git a/tests/integration/koa-server.spec.ts b/tests/integration/koa-server.spec.ts index dafc12cea..abb25e418 100644 --- a/tests/integration/koa-server.spec.ts +++ b/tests/integration/koa-server.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import { server } from '../fixtures/koa/server'; -import { TestModel, TestClassModel, Model, ParameterTestModel } from '../fixtures/testModel'; +import { GenericModel, GenericRequest, TestModel, TestClassModel, Model, ParameterTestModel } from '../fixtures/testModel'; import * as chai from 'chai'; import * as request from 'supertest'; @@ -230,6 +230,46 @@ describe('Koa Server', () => { expect(model.human).to.equal(true); }); }); + + it('can get request with generic type', () => { + return verifyGetRequest(basePath + '/GetTest/GenericModel', (err, res) => { + const model = res.body as GenericModel; + expect(model.result.id).to.equal(1); + }); + }); + + it('can get request with generic array', () => { + return verifyGetRequest(basePath + '/GetTest/GenericModelArray', (err, res) => { + const model = res.body as GenericModel; + expect(model.result[0].id).to.equal(1); + }); + }); + + it('can get request with generic primative type', () => { + return verifyGetRequest(basePath + '/GetTest/GenericPrimitive', (err, res) => { + const model = res.body as GenericModel; + expect(model.result).to.equal('a string'); + }); + }); + + it('can get request with generic primative array', () => { + return verifyGetRequest(basePath + '/GetTest/GenericPrimitiveArray', (err, res) => { + const model = res.body as GenericModel; + expect(model.result[0]).to.equal('string one'); + }); + }); + + it('can post request with a generic body', () => { + + const data: GenericRequest = { + name: 'something', + value: getFakeModel() + }; + return verifyPostRequest(basePath + '/PostTest/GenericBody', data, (err, res) => { + const model = res.body as TestModel; + expect(model.id).to.equal(1); + }); + }); }); diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts index a5e0bef33..69543a1d1 100644 --- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts +++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts @@ -1,5 +1,6 @@ import 'mocha'; import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator'; +import {Swagger} from '../../../../src/swagger/swagger'; import { SpecGenerator } from '../../../../src/swagger/specGenerator'; import { getDefaultOptions } from '../../../fixtures/defaultOptions'; import * as chai from 'chai'; @@ -167,4 +168,53 @@ describe('Definition generation', () => { expect(property.description).to.equal('This is a description for publicConstructorVar'); }); }); + + describe('Generic-based generation', () => { + it('should generate different definitions for a generic model', () => { + const definition = getValidatedDefinition('GenericModel').properties; + + if (!definition) { throw new Error(`There were no properties on model.`); } + + const property = definition['result']; + + expect(property).to.exist; + expect(property['$ref']).to.equal('#/definitions/TestModel'); + }); + it('should generate different definitions for a generic model array', () => { + const definition = getValidatedDefinition('GenericModel').properties; + + if (!definition) { throw new Error(`There were no properties on model.`); } + + const property = definition['result']; + + expect(property).to.exist; + expect(property.type).to.equal('array'); + + if (!property.items) { throw new Error(`There were no items on the property model.`); } + expect((property.items as Swagger.Schema)['$ref']).to.equal('#/definitions/TestModel'); + }); + it('should generate different definitions for a generic primitive', () => { + const definition = getValidatedDefinition('GenericModel').properties; + + if (!definition) { throw new Error(`There were no properties on model.`); } + + const property = definition['result']; + + expect(property).to.exist; + expect(property.type).to.equal('string'); + }); + it('should generate different definitions for a generic primitive array', () => { + const definition = getValidatedDefinition('GenericModel').properties; + + if (!definition) { throw new Error(`There were no properties on model.`); } + + const property = definition['result']; + + expect(property).to.exist; + expect(property.type).to.equal('array'); + + if (!property.items) { throw new Error(`There were no items on the property model.`); } + expect((property.items as Swagger.Schema)['type']).to.equal('string'); + }); + }); }); diff --git a/tests/unit/swagger/schemaDetails.spec.ts b/tests/unit/swagger/schemaDetails.spec.ts index 74ccc559f..1ba89b5ba 100644 --- a/tests/unit/swagger/schemaDetails.spec.ts +++ b/tests/unit/swagger/schemaDetails.spec.ts @@ -16,6 +16,8 @@ describe('Schema details generation', () => { if (!spec.info.version) { throw new Error('No spec info version.'); } if (!spec.host) { throw new Error('No host'); } + console.error(JSON.stringify(spec)); + it('should set API name if provided', () => expect(spec.info.title).to.equal(getDefaultOptions().name)); it('should set API description if provided', () => expect(spec.info.description).to.equal(getDefaultOptions().description)); it('should set API version if provided', () => expect(spec.info.version).to.equal(getDefaultOptions().version));