UNPKG

webapi-ng2

Version:

ASP.NET Core Web API client generator for Angular 2

762 lines (588 loc) 23.9 kB
import { GeneratorConfig } from '../../generator-config'; import { Attribute, AttributeParameter, Controller, EnumValue, Operation, OperationParameter, Property, Schema, SchemaType, Specification } from '../../specification'; export class AngularGenerator { private _script: string; public generate(specification: Specification, config: GeneratorConfig): string { // if (apiDescription.definitions != undefined) { // this.mapDefinitions(apiDescription.definitions); // } let script = this.getHeader() + '\n\r' + this.getImports() + '\n\r' + this.getOptions(config) + '\n\r' + this.getBaseClass(config) + '\n\r'; for (let controller of specification.controllers) { script += '\n\r' + this.getController(controller, config); } if (specification.schema != undefined) { const enumTypes: Schema[] = []; for (let schemaName in specification.schema) { let schema = specification.schema[schemaName]; script += '\n\r' + this.getSchema(schema); if (schema.type == SchemaType.Enumeration) { enumTypes.push(schema); } } if (enumTypes.length > 0) { script += '\n\r' + this.getEnumService(enumTypes); } } return script; } private getHeader(): string { let result = ` // // This file is autogenerated. // See http://github.com/bmitchenko/webapi-ng2 for details. // // tslint:disable:max-line-length `; return result; } private getImports(): string { let result = ` import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpEvent, HttpResponse, HttpErrorResponse, HttpRequest, HttpHeaders } from '@angular/common/http'; import { map, filter, catchError } from 'rxjs/operators'; import { Observable, throwError } from 'rxjs'; `; return result; } private getOptions(config: GeneratorConfig): string { let result = ` export class HttpError extends Error { detail?: string; error?: any; status: number; title?: string; traceId?: string; type?: string; url: string; constructor(message?: string) { super(message); } } @Injectable() export class ` + config.outputClass + `Options { public basePath = ''; public loginUrl: string; public dateSerialization: 'local' | 'utc' = 'utc'; }`; return result; } private getEnumService(enumTypes: Schema[]): string { const enumMetadata: string[] = []; for (const enumType of enumTypes) { let displayName: string | undefined; if (enumType.attributes != undefined) { displayName = this.getDisplayName(enumType.attributes); } if (displayName == undefined) { displayName = enumType.name; } let values: string[] = []; if (enumType.values != undefined) { for (const enumValue of enumType.values) { let valueDisplayName: string | undefined; if (enumValue.attributes != undefined) { valueDisplayName = this.getDisplayName(enumValue.attributes); } if (valueDisplayName == undefined) { valueDisplayName = enumValue.name; } values.push(`{ value: ${enumType.name}.${enumValue.name}, displayName: '${valueDisplayName}' }`); } } enumMetadata.push(` { type: ${enumType.name}, displayName: '${displayName}', values: [ ${values.join(',\r')} ] }`); } let result = ` export interface EnumValue<T> { displayName: string; value: T; } interface EnumMetadata<T> { displayName: string; type: any; values: EnumValue<T>[]; } export const ENUM_METADATA: EnumMetadata<any>[] = [ ${enumMetadata.join(',')} ]; @Injectable() export class EnumService { public getDisplayName(enumType: any): string { const metadata = this.findEnum<any>(enumType); return metadata.displayName; } public getValueDisplayName(enumType: any, enumValue: number): string { const metadata = this.findEnum<any>(enumType); const exactValue = metadata.values.find(x => x.value === enumValue); if (exactValue != undefined) { return exactValue.displayName; } const result = metadata.values // tslint:disable-next-line:no-bitwise .filter(x => x.value !== 0 && (x.value & enumValue) === x.value) .map(x => x.displayName); return result.join(', '); } public getValues<T>(enumType: any): EnumValue<T>[] { const metadata = this.findEnum<any>(enumType); return metadata.values; } private findEnum<T>(enumType: any): EnumMetadata<T> { const metadata = ENUM_METADATA.find(x => x.type === enumType); if (metadata == undefined) { throw Error('Metadata for enum type not found.'); } return metadata; } }`; return result; } private getDisplayName(attributes: Attribute[]): string | undefined { const displayAttribute = attributes.find(x => x.name == 'DisplayName'); if (displayAttribute != undefined) { if (displayAttribute.parameters != undefined) { const nameParameter = displayAttribute.parameters.find(x => x.name == 'displayName'); if (nameParameter != undefined) { return nameParameter.value; } } } const descriptionAttribute = attributes.find(x => x.name == 'Description'); if (descriptionAttribute != undefined) { if (descriptionAttribute.parameters != undefined) { const nameParameter = descriptionAttribute.parameters.find(x => x.name == 'description'); if (nameParameter != undefined) { return nameParameter.value; } } } const displayNameAttribute = attributes.find(x => x.name == 'Display'); if (displayNameAttribute != undefined) { if (displayNameAttribute.parameters != undefined) { const nameParameter = displayNameAttribute.parameters.find(x => x.name == 'Name'); if (nameParameter != undefined) { return nameParameter.value; } } } return undefined; } private getBaseClass(config: GeneratorConfig): string { let result = ` @Injectable() export abstract class ` + config.outputClass + `Base { private dateFormat = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*/; constructor(public http: HttpClient, public options: ` + config.outputClass + `Options) { this.reviver = this.reviver.bind(this); } ${this.getRequestMethod(config)} private extractBody(response: HttpResponse<any>): any { let body = response.body; if (typeof body === 'string') { if (this.isJsonResponse(response)) { try { body = this.parseJson(body); } catch (e) { } } } return body; } private extractError(response: HttpErrorResponse): Error { const error = new HttpError(response.message); error.status = response.status; error.url = response.url; const contentType = response.headers.get('content-type'); if (contentType?.includes('application/problem+json')) { const problemDetail = JSON.parse(response.error); if (problemDetail.detail != null) { error.detail = problemDetail.detail; } if (problemDetail.error != null || problemDetail.errors != null) { error.error = problemDetail.error ?? problemDetail.errors; } if (problemDetail.title != null) { error.title = problemDetail.title; } if (problemDetail.traceId != null) { error.traceId = problemDetail.traceId; } if (problemDetail.type != null) { error.type = problemDetail.type; } } else { if (typeof response.error === 'string') { error.title = response.error; } else { if (this.isJsonResponse(response)) { error.error = JSON.parse(response.error); } else { error.error = response.error; } } } return error; } private serializeBody(body?: any) { if (body == undefined) { return body; } if (typeof body !== 'object') { return body; } return JSON.stringify(body, (key, value) => { if (typeof value === 'string' && this.dateFormat.test(value)) { return this.serializeDate(new Date(value)); } return value; }); } private serializeDate(d: Date): string { let result: string; const n = x => x < 10 ? '0' + x.toString() : x.toString(); if (this.options.dateSerialization === 'local') { result = d.getFullYear() + '-' + n(d.getMonth() + 1) + '-' + n(d.getDate()) + 'T' + n(d.getHours()) + ':' + n(d.getMinutes()) + ':' + n(d.getSeconds()); } else { result = d.toJSON(); } return result; } private isJsonResponse(response: HttpResponse<any> | HttpErrorResponse): boolean { const contentType = response.headers.get('content-type'); if (contentType && contentType.indexOf('application/json') !== -1) { return true; } return false; } private parseJson(text: string): any { return JSON.parse(text, this.reviver); } private reviver(key: any, value: any) { if (typeof value === 'string' && this.dateFormat.test(value)) { return this.parseDate(value); } return value; } private parseDate(s: string): Date { const a = s.split(/[^0-9]/) as any; const d = new Date(a[0], a[1] - 1, a[2], a[3], a[4], a[5]); return d; } private addSearchParam(params: HttpParams, name: string, value: any): HttpParams { if (value instanceof Array) { value.forEach((v, i) => { params = this.addSearchParam(params, ` + "`${name}[${i}]`" + `, v); }); } else { if (value instanceof Date) { params = params.append(name, this.serializeDate(value)); } else { if (value instanceof Object) { Object.getOwnPropertyNames(value).forEach((propertyName) => { params = this.addSearchParam(params, ` + "`${name}.${propertyName}`" + `, value[propertyName]); }); } else { if (value != undefined) { params = params.append(name, value); } } } } return params; } }`; return result; } private getRequestMethod(config: GeneratorConfig): string { let returnType = config.usePromises ? 'Promise<T>' : 'Observable<T>'; let request = ` const result = this.http.request(request) .pipe( filter((event: HttpEvent<any>) => event instanceof HttpResponse), map((event: HttpResponse<any>) => { return this.extractBody(event); }), catchError((error: HttpErrorResponse) => { return throwError(this.extractError(error)); }) );`; let result = ` protected request<T>(path: string, method: string, urlParams?: any, body?: any): ${returnType} { let url = path; let params = new HttpParams(); if (urlParams !== undefined) { Object.getOwnPropertyNames(urlParams).forEach((paramName) => { if (url.indexOf(` + "`{${paramName}}`" + `) !== -1) { url = url.replace(` + "`{${paramName}}`" + `, urlParams[paramName]); } else { params = this.addSearchParam(params, paramName, urlParams[paramName]); } }); } body = this.serializeBody(body); const request = new HttpRequest<any>(method, this.options.basePath + url, body, { headers: new HttpHeaders({ 'Content-Type': 'application/json' }), params: params, responseType: 'text' }); ${request} return ${config.usePromises ? 'result.toPromise()' : 'result'}; }`; return result; } private getController(controller: Controller, config: GeneratorConfig): string { let operations: string[] = []; if (controller.operations != undefined) { for (let operation of controller.operations) { operations.push(this.getOperation(operation, config)); } } let className = controller.name; const suffix = config.suffix == undefined ? 'Service' : config.suffix; className += suffix; let result = ` @Injectable() export class ${className} extends ` + config.outputClass + `Base { ${operations.join('')} }`; return result; } private getOperation(operation: Operation, config: GeneratorConfig): string { let path = operation.path; let method = operation.method; let urlParams = ''; let bodyParam: string | undefined = undefined; let name = this.camelCase(operation.name); let returnTypeArgument = this.mapType(operation.responseType); let returnType = config.usePromises ? `Promise<${returnTypeArgument}>` : `Observable<${returnTypeArgument}>`; let parameters: string[] = []; if (operation.parameters != undefined) { for (let parameter of operation.parameters) { let source = parameter.in.toLowerCase(); if (source == 'body') { bodyParam = parameter.name; } if (source == 'query' || source == 'path') { if (urlParams.length > 0) { urlParams += ', '; } urlParams += `${parameter.name}: ${parameter.name}`; } parameters.push(this.getOperationParameter(parameter)); } } let requestParameters = `'${path}', '${method}'`; if (urlParams.length > 0 || bodyParam != undefined) { requestParameters += `, { ${urlParams} }`; } if (bodyParam != undefined) { requestParameters += `, ${bodyParam}`; } let operationMethod = ` public ${name}(${parameters.join()}): ${returnType} { return this.request<${returnTypeArgument}>(${requestParameters}); } `; if (operation.summary) { operationMethod = `/** ${operation.summary} */ ${operationMethod}`; } return operationMethod; } private getOperationParameter(operationParameter: OperationParameter): string { let parameter = operationParameter.name; if (!operationParameter.required && operationParameter.default == undefined) { parameter += '?'; } let parameterType = this.mapType(operationParameter.type); parameter += `: ${parameterType}`; if (!operationParameter.required) { let defaultValue = operationParameter.default; if (defaultValue != undefined) { if (parameterType == 'string') { defaultValue = `'${defaultValue}'`; } parameter += ` = ${defaultValue}`; } } return parameter; } private getSchema(schema: Schema): string { if (schema.type === SchemaType.Enumeration) { return this.getEnum(schema); } return this.getInterface(schema); } private getEnum(schema: Schema): string { let values: string[] = []; if (schema.values != undefined) { for (let enumValue of schema.values) { if (enumValue.value != undefined) { values.push(`${enumValue.name} = ${enumValue.value}`); } else { values.push(enumValue.name); } } } let result = ` export enum ${schema.name} { ${values.join(', \n')} }`; return result; } private getInterface(schema: Schema): string { let base = ''; let properties: string[] = []; if (schema.extends != undefined) { base = `extends ${schema.extends.map(x => this.mapType(x)).join(', ')}`; } if (schema.properties != undefined) { for (let property of schema.properties) { let propertyName = this.camelCase(property.name); let propertyType = this.mapType(property.type); if (property.nullable) { propertyName += '?'; } properties.push(`${propertyName}: ${propertyType};`); } } let result = ` export interface ${schema.name} ${base} { ${properties.join('\n')} }`; return result; } private mapType(coreType: string): string { // collection; if (coreType.endsWith('[]')) { let collectionType = this.mapType(coreType.substr(0, coreType.length - 2)); return `${collectionType}[]`; } // generic type; if (coreType.endsWith('>')) { let genericType = this.parseGenericType(coreType); let genericArguments = genericType.arguments .map(x => this.mapType(x)) .join(', '); return `${genericType.name}<${genericArguments}>`; } // primitive; switch (coreType.toLowerCase()) { case 'guid': case 'string': return 'string'; case 'datetime': return 'Date'; case 'bool': case 'boolean': return 'boolean'; case 'byte': case 'short': case 'int': case 'integer': case 'int16': case 'int32': case 'int64': case 'float': case 'decimal': case 'double': case 'single': return 'number'; case 'void': return 'void'; case 'object': return 'any'; default: break; } // interface; return coreType; } private parseGenericType(typeName: string): { name: string, arguments: string[] } { let open = typeName.indexOf('<'); let genericName = typeName.substr(0, open); let genericArguments: string[] = []; let splitters let level = 0; let buf = ''; for (let i = open + 1; i < typeName.length - 1; i++) { let c = typeName[i]; if (c == '<') { level++; } else if (c == '>') { level--; } else if (c == ',' && level == 0) { genericArguments.push(buf.trim()); buf = ''; } if (c != ',' || level > 0) { buf += c; } } genericArguments.push(buf.trim()); return { arguments: genericArguments, name: genericName }; } private camelCase(text: string): string { return text.substr(0, 1).toLowerCase() + text.substr(1); } } // private getApi(apiDescription: ApiDescription, config: GeneratorConfig): string { // let fields: string[] = []; // let properties: string[] = []; // if (apiDescription.controllers != undefined) { // apiDescription.controllers.forEach((controller) => { // var className = controller.name + 'Controller'; // var fieldName = '_' + this.camelCase(controller.name); // fields.push(`private ${fieldName}: ${className};`); // var property = `public get ${this.camelCase(controller.name)}(): ${className} { // if (this.${fieldName} == undefined) { // this.${fieldName} = new ${className}(this._connection); // } // return this.${fieldName}; // }`; // properties.push(property); // }); // } // return ` // @Injectable() // export class ${config.outputClass} { // private _connection: ApiConnection; // ${fields.join('\n')} // constructor (http: Http) { // this._connection = new ApiConnection(http, ''); // } // public get basePath(): string { // return this._connection.basePath; // } // public set basePath(basePath: string) { // this._connection.basePath = basePath; // } // ${properties.join('\n')} // }`; // }