UNPKG

swagger-typescript-api-generator

Version:

Angular API client generator from swagger json

752 lines (616 loc) 31.2 kB
'use strict'; var fs = require('fs'); var Mustache = require('mustache'); //var beautify = require('js-beautify').js_beautify; //var Linter = require('tslint'); var _ = require('lodash'); var Generator = (function () { function Generator(swaggerfile, outputpath) { this._swaggerfile = swaggerfile; this._outputPath = outputpath; } Generator.prototype.Debug = false; Generator.prototype.initialize = function () { this.LogMessage('Reading Swagger file', this._swaggerfile); var swaggerfilecontent = fs.readFileSync(this._swaggerfile, 'UTF-8'); this.LogMessage('Parsing Swagger JSON'); this.swaggerParsed = JSON.parse(swaggerfilecontent); this.LogMessage('Reading Mustache templates'); this.templates = { 'api.module': fs.readFileSync(__dirname + "/../templates/angular2-api.module.mustache", 'utf-8'), 'api-module-config': fs.readFileSync(__dirname + "/../templates/angular2-api-module-config.mustache", 'utf-8'), 'class': fs.readFileSync(__dirname + "/../templates/angular2-service.mustache", 'utf-8'), 'enum': fs.readFileSync(__dirname + "/../templates/angular2-enum.mustache", 'utf-8'), 'model': fs.readFileSync(__dirname + "/../templates/angular2-model.mustache", 'utf-8'), 'models_index': fs.readFileSync(__dirname + "/../templates/angular2-index.mustache", 'utf-8') }; this.LogMessage('Creating Mustache viewModel'); this.viewModel = this.createMustacheViewModel(); this.initialized = true; }; Generator.prototype.generateAPIClient = function () { if (this.initialized !== true) this.initialize(); this.generateStaticFile('api.module'); this.generateStaticFile('api-module-config'); this.generateModels(); this.generateServices(); this.generateIndex('models', this.viewModel.definitions); this.generateIndex('services', this.viewModel.controllers); this.LogMessage('API client generated successfully'); }; Generator.prototype.generateStaticFile = function(name) { var that = this; if (this.initialized !== true) this.initialize(); that.LogMessage('Rendering template for '+name); var result = that.renderLintAndBeautify(that.templates[name], {}, that.templates); var outfile = that._outputPath + "/" + name + ".ts"; that.LogMessage('Creating output file', outfile); fs.writeFileSync(outfile, result, 'utf-8') }; Generator.prototype.generateModels = function () { var that = this; if (this.initialized !== true) this.initialize(); var outputdir = this._outputPath + '/models'; if (!fs.existsSync(outputdir)) fs.mkdirSync(outputdir); // generate API models _.forEach(this.viewModel.definitions, function (definition) { that.LogMessage('Rendering template for model: ', definition.name); var template = definition.isEnum ? that.templates.enum : that.templates.model; var result = that.renderLintAndBeautify(template, definition, that.templates); var outfile = outputdir + "/" + definition.filename + ".ts"; that.LogMessage('Creating output file', outfile); fs.writeFileSync(outfile, result, 'utf-8') }); }; Generator.prototype.generateServices = function () { var that = this; if (this.initialized !== true) this.initialize(); var outputdir = this._outputPath + '/services'; if (!fs.existsSync(outputdir)) fs.mkdirSync(outputdir); // generate API client classes _.forEach(this.viewModel.controllers, function (controller) { that.LogMessage('Rendering template for API: ', controller.name); var result = that.renderLintAndBeautify(that.templates.class, controller, that.templates); var outfile = outputdir + "/" + controller.filename + ".ts"; that.LogMessage('Creating output file', outfile); fs.writeFileSync(outfile, result, 'utf-8') }); }; Generator.prototype.generateIndex = function (path, files) { if (this.initialized !== true) this.initialize(); var outputdir = this._outputPath + '/' + path; if (!fs.existsSync(outputdir)) fs.mkdirSync(outputdir); this.LogMessage('Rendering '+path+' index'); var result = this.renderLintAndBeautify(this.templates.models_index, {files: files}, this.templates); var outfile = outputdir + "/index.ts"; this.LogMessage('Creating output file', outfile); fs.writeFileSync(outfile, result, 'utf-8') }; Generator.prototype.renderLintAndBeautify = function (tempalte, model) { // Render ***** var result = Mustache.render(tempalte, model); // Lint ***** // var ll = new Linter("noname", rendered, {}); // var lintResult = ll.lint(); // lintResult.errors.forEach(function (error) { // if (error.code[0] === 'E') // throw new Error(error.reason + ' in ' + error.evidence + ' (' + error.code + ')'); // }); // Beautify ***** // NOTE: this has been commented because of curly braces were added on newline after beaufity //result = beautify(result, { indent_size: 4, max_preserve_newlines: 2 }); return result; }; Generator.prototype.createMustacheViewModel = function () { var that = this; var swagger = this.swaggerParsed; var authorizedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; var data = { isNode: false, description: swagger.info.description, isSecure: swagger.securityDefinitions !== undefined, swagger: swagger, domain: (swagger.schemes && swagger.schemes.length > 0 && swagger.host && swagger.basePath) ? swagger.schemes[0] + '://' + swagger.host + swagger.basePath : '', methods: [], definitions: [], controllers: [] }; var enumDefinitions = []; _.forEach(swagger.paths, function (api, path) { var globalParams = []; var globalResponses = []; _.forEach(api, function (op, m) { if (m.toLowerCase() === 'parameters') { globalParams = op; } if (m.toLowerCase() === 'responses') { globalResponses = op; } }); _.forEach(api, function (op, m) { if (authorizedMethods.indexOf(m.toUpperCase()) === -1){ return; } // The description line is optional in the spec var summaryLines = []; if (op.description) { summaryLines = op.description.split('\n'); summaryLines.splice(summaryLines.length-1, 1); } var pathParamRegex = /\{(.*?)\}/g; var method = { path: path, backTickPath: path.replace(pathParamRegex, "$${$1PathSegment}"), tags: op.tags, methodName: op['x-swagger-js-method-name'] ? op['x-swagger-js-method-name'] : (op.summary ? op.summary : that.getPathToMethodName(m, path)), method: m.toUpperCase(), contentType: 'application/json', angular2httpMethod: m.toLowerCase(), isGET: m.toUpperCase() === 'GET', hasPayload: !_.includes(['GET','DELETE','HEAD'], m.toUpperCase()), summaryLines: summaryLines, isSecure: swagger.security !== undefined || op.security !== undefined, parameters: [], optionalParameters: [], allParameters: [], hasOptionalParameters: false, hasRequiredParameters: false, hasBodyParameter: false, hasRefType: false, responses: [], typescriptReturnType: "", typescriptReturnTypeName: "", hasJsonResponse: _.some(_.defaults([], swagger.produces, op.produces), function (response) { // TODO PREROBIT return response.indexOf('/json') != -1; }), refs: [], hasSort: false }; // Replace generic 'invoke' / 'handle' operations with method name from path if (method.methodName === 'invoke' || method.methodName === 'handle') { method.methodName = that.getPathToMethodName(m, path); } // Use operationId if method name is not unique with the given tags if (_.find(data.methods, {tags: method.tags, methodName: method.methodName})) { method.methodName = op.operationId; } var params = []; var responses = []; if (_.isArray(op.parameters)) params = op.parameters; if (op.responses != null) responses = op.responses; // Sort metadata if (op['x-sort']) { var xSort = op['x-sort']; method.sort = Object.keys(xSort).map(function (key) { return {key: key, value: xSort[key]}; }); method.hasSort = true; } params = params.concat(globalParams); //responses = params.concat(globalResponses); // Add path parameters if they are not part of the parameters definition var pathParamMatch; while ((pathParamMatch = pathParamRegex.exec(path)) != null) { var pathParamName = pathParamMatch[1]; if (!_.find(params, {name: pathParamName, in: 'path'})) { params.push({name: pathParamName, in: 'path', required: true, type: 'string'}); } } _.forEach(params, function (parameter) { // Ignore headers which are injected by proxies & app servers // eg: https://cloud.google.com/appengine/docs/go/requests#Go_Request_headers if (parameter['x-proxy-header'] && !data.isNode) return; // Ignore body parameters for GET method if (method.isGET && parameter.in === 'body') { return; } parameter.camelCaseName = that.camelCase(parameter.name); parameter.isArray = parameter.type === 'array' && _.has(parameter, 'items'); // Workaround for vendor extensions that are not supported on parameters by springfox yet var extensions; if (_.has(parameter, 'description') && parameter.description[0] === '{') { extensions = JSON.parse(parameter.description); parameter.description = ''; } if (_.has(parameter, 'schema')) { if (_.isString(parameter.schema.$ref)) { parameter.type = that.camelCase(that.getRefType(parameter.schema.$ref)); method.refs.push({name: parameter.type}); } else if (_.isString(parameter.schema.type)) { parameter.isArray = parameter.schema.type === 'array' && _.has(parameter.schema, 'items'); if (parameter.isArray) { parameter.type = parameter.schema.items.type; } else { parameter.type = parameter.schema.type; } if (_.has(parameter.schema, 'additionalProperties')) { if (_.isString(parameter.schema.additionalProperties.type)) { parameter.additionalPropertiesType = parameter.schema.additionalProperties.type; } } if (_.has(parameter.schema, 'items')) { parameter.items = parameter.schema.items; } if (parameter.schema.type === 'string' && parameter.schema.format === "byte") { parameter.type = 'ArrayBuffer'; } } } else if (_.has(extensions, 'x-ref-type')) { var refType = extensions['x-ref-type']; if(parameter.type === 'string') { parameter.type = 'StringRef<\''+refType+'\'>'; } else { parameter.type = 'Ref<\''+refType+'\'>'; } method.hasRefType = true; } else if (_.has(extensions, 'x-enum-type')) { parameter.type = extensions['x-enum-type']; method.refs.push({name: parameter.type}); var enumDefinition; if (parameter.enum !== undefined) { enumDefinition = that.createEnumDefinition(parameter.type, parameter.enum); } else if (parameter.type === 'array' && _.has(parameter.items, 'enum')) { enumDefinition = that.createEnumDefinition(parameter.type, parameter.items.enum); } if (enumDefinition) { enumDefinitions.push(enumDefinition); } } else if (parameter.isArray) { if (_.isString(parameter.items.type)) { parameter.type = parameter.items.type; } else if (_.isString(parameter.items.$ref)) { parameter.type = that.camelCase(that.getRefType(parameter.items.$ref)); method.refs.push({name: parameter.type}); } else { parameter.type = 'any'; } } if (parameter.isArray) { var arrayType; if (parameter.type === 'integer' || parameter.type === 'double') { arrayType = 'number'; } else { arrayType = parameter.type; } parameter.typescriptType = 'ReadonlyArray<' + arrayType + '>'; } else if (parameter.type === 'integer' || parameter.type === 'double') parameter.typescriptType = 'number'; else if (parameter.type === 'object' && parameter.additionalPropertiesType) parameter.typescriptType = '{ [key: string]: '+parameter.additionalPropertiesType+' }'; else parameter.typescriptType = parameter.type; if (!_.has(parameter, 'required')) { // Default is optional. See: https://swagger.io/docs/specification/describing-parameters/ parameter.isRequired = false; method.hasOptionalParameters = true; } else { parameter.isRequired = parameter.required; } if (_.has(parameter, 'default')) { parameter.isRequired = true; } if (parameter.enum && parameter.enum.length === 1) { parameter.isSingleton = true; parameter.singleton = parameter.enum[0]; } if (parameter.in === 'body') { parameter.isBodyParameter = true; if (parameter.type === 'ArrayBuffer') { method.contentType = 'application/octet-stream'; } method.hasBodyParameter = true; } else if (parameter.in === 'path') parameter.isPathParameter = true; else if (parameter.in === 'query') { parameter.isQueryParameter = true; if (parameter['x-name-pattern']) parameter.isPatternType = true; } else if (parameter.in === 'header') parameter.isHeaderParameter = true; else if (parameter.in === 'formData') parameter.isFormParameter = true; if(parameter.isRequired) { method.parameters.push(parameter); method.hasRequiredParameters = true; } else { method.optionalParameters.push(parameter); method.hasOptionalParameters = true; } }); if (method.parameters.length > 0 && !method.hasOptionalParameters) method.parameters[method.parameters.length - 1].last = true; if (method.optionalParameters.length > 0) method.optionalParameters[method.optionalParameters.length - 1].last = true; if(method.optionalParameters.length > 0 || method.parameters.length > 0) { method.allParameters = method.parameters.concat(method.optionalParameters); method.allParameters[method.allParameters.length - 1].last = true; } console.log(method.allParameters); _.forEach(responses, function (response) { var isArray = false; if (response.description !== 'OK') return; if (_.has(response, 'schema')) { if (_.isString(response.schema.$ref)) { response.type = that.camelCase(that.getRefType(response.schema.$ref)); method.refs.push({name: response.type}); } else if (_.has(response.schema, 'items')) { if (_.isString(response.schema.items.$ref)) { response.type = that.getRefType(response.schema.items.$ref); method.refs.push({name: response.type}); isArray = true; } else if (_.isString(response.schema.items.type)) { response.type = response.schema.items.type; isArray = true; } } else if (_.has(response.schema, 'type')) { response.type = response.schema.type; } } var innerType; if(!response.type) { innerType = 'HttpResponse<object>'; method.observeResponse = true; } else { if (response.type === 'integer' || response.type === 'double') { innerType = 'number'; } else if (response.type === 'file') { innerType = 'Blob'; method.responseType = 'blob'; } else { innerType = response.type; } method.observeResponse = false; } if(isArray) { innerType += '[]'; } method.typescriptReturnType = innerType; method.responses.push(response); }); // Use default response type if 200 response code is not defined if (!method.typescriptReturnType) { method.typescriptReturnType = 'HttpResponse<object>'; method.observeResponse = true; } if (method.responses.length > 0) method.responses[method.responses.length - 1].last = true; data.methods.push(method); }); }); // load all model data _.forEach(swagger.definitions, function (defin, defVal) { var defName = that.camelCase(defVal); var definition = { name: defName, filename: _.kebabCase(defName) + ".model", properties: [], refs: [], isEnum: false, hasRefType: false, hasNumberRefType: false, hasStringRefType: false, refGenericType: "", hasDecorators: false, decorators: [] }; _.forEach(defin.properties, function (propin, propVal) { var property = { name: propVal, isRequired: _.has(defin, 'required') && defin.required.indexOf(propVal) >= 0, isRef: _.has(propin, '$ref') || (propin.type === 'array' && _.has(propin.items, '$ref')), isRefType: _.has(propin, 'x-ref-type'), isArray: propin.type === 'array', type: null, typescriptType: null, isEnum: propin.enum !== undefined || (propin.type === 'array' && _.has(propin.items, 'enum')), enumValues: null, refType: null, refGenericType: "", minLength: propin.minLength, maxLength: propin.maxLength, minimum: propin.minimum, maximum: propin.maximum, pattern: propin.pattern !== undefined ? propin.pattern.replace(/\\/g, '\\\\').replace(/\'/g, '\\\'') : undefined }; if (property.isArray) { property.type = _.has(propin.items, '$ref') ? that.camelCase(propin.items["$ref"].replace("#/definitions/", "")) : propin.items.type; property.enumValues = propin.items.enum; } else { property.type = _.has(propin, '$ref') ? that.camelCase(propin["$ref"].replace("#/definitions/", "")) : propin.type; property.enumValues = propin.enum; } if (property.isRefType) { property.refType = propin['x-ref-type']; definition.hasRefType = true; if(property.type === 'string') { property.refGenericType = 'StringRef'; definition.hasStringRefType = true; } else { property.refGenericType = 'Ref'; definition.hasNumberRefType = true; } } if (property.type === 'integer' || property.type === 'double') property.typescriptType = 'number'; else if (property.type === 'string' && property.isEnum) { if (_.has(propin, 'x-enum-type')) { property.typescriptType = propin['x-enum-type']; property.type = propin['x-enum-type']; } else { // Use property name as enum type name property.typescriptType = that.firstLetterCapitalize(property.name); property.type = that.firstLetterCapitalize(property.name); } } else property.typescriptType = property.type; // create the definition for the enum type if(property.isEnum) { var enumDefinition = that.createEnumDefinition(property.type, property.enumValues); enumDefinitions.push(enumDefinition); } if (property.isRef || property.isEnum) { // Prevent import of the model itself if (property.type !== definition.name) { property.filename = _.kebabCase(property.type) + ".model"; definition.refs.push(property); } } if (property.isRequired) { definition.decorators.push('Required'); } if (property.minLength) { definition.decorators.push('MinLength'); } if (property.maxLength) { definition.decorators.push('MaxLength'); } if (property.minimum) { definition.decorators.push('Minimum'); } if (property.maximum) { definition.decorators.push('Maximum'); } if (property.pattern) { definition.decorators.push('Pattern'); } definition.properties.push(property); }); definition.properties.sort(function (a, b) { if (a.isRequired && !b.isRequired){ return -1; } else if (b.isRequired && !a.isRequired) { return 1; } return a.name.localeCompare(b.name); }); definition.refs = _.uniq(definition.refs, 'type'); definition.decorators = _.uniq(definition.decorators); definition.hasDecorators = definition.decorators.length > 0; data.definitions.push(definition); }); // make enum definitions unique var uniqueEnumDefinitions = _.uniq(enumDefinitions, 'name'); // add all enum definitons data.definitions = _.union(data.definitions, uniqueEnumDefinitions); if (data.definitions.length > 0) data.definitions[data.definitions.length - 1].last = true; // check, create tags & filter methods by tag _.forEach(data.swagger.tags, function (tag) { var name = tag.description.replace("Controller", "").replace("Resource", "").replace(/ /g, ""); var controller = { name: name + "Api", filename: _.kebabCase(name) + ".api", methods: [], refs: [], importHttpResponse: false, hasRefType: false, hasSort: false, }; _.forEach(data.methods, function (method) { if(method.tags && method.tags[0]) { if(method.tags[0] === tag.name) { controller.methods.push(method); controller.refs = _.uniq(controller.refs.concat(method.refs), 'name'); if (method.observeResponse) { controller.importHttpResponse = true; } if (method.hasRefType) { controller.hasRefType = true; } if (method.hasSort) { controller.hasSort = true; } } } }); if (controller.refs.length > 0) controller.refs[controller.refs.length - 1].last = true; data.controllers.push(controller); }); return data; }; Generator.prototype.getRefType = function (refString) { var segments = refString.split('/'); return segments.length === 3 ? segments[2] : segments[0]; }; Generator.prototype.getPathToMethodName = function (m, path) { if (path === '/' || path === '') return m; // clean url path for requests ending with '/' var cleanPath = path; if (cleanPath.indexOf('/', cleanPath.length - 1) !== -1) cleanPath = cleanPath.substring(0, cleanPath.length - 1); var segments = cleanPath.split('/').slice(1); segments = _.transform(segments, function (result, segment) { if (segment[0] === '{' && segment[segment.length - 1] === '}') segment = 'by' + segment[1].toUpperCase() + segment.substring(2, segment.length - 1); result.push(segment); }); var result = this.camelCase(segments.join('-')); return m.toLowerCase() + result[0].toUpperCase() + result.substring(1); }; Generator.prototype.camelCase = function (text) { if (!text) return text; if (text.indexOf('-') === -1 && text.indexOf('.') === -1) return text; var tokens = []; text.split('-').forEach(function (token, index) { tokens.push(token[0].toUpperCase() + token.substring(1)); }); var partialres = tokens.join(''); tokens = []; partialres.split('.').forEach(function (token, index) { tokens.push(token[0].toUpperCase() + token.substring(1)); }); return tokens.join(''); }; Generator.prototype.firstLetterCapitalize = function (text) { return text.charAt(0).toUpperCase() + text.slice(1); }; Generator.prototype.LogMessage = function (text, param) { if (this.Debug) console.log(text, param || ''); }; Generator.prototype.createEnumDefinition = function (name, values) { var enumDefinition = { name: name, filename: _.kebabCase(name) + ".model", properties: [], refs: [], isEnum: true, enumValues: values }; var enumObjValues = []; values.forEach(function (enumValue) { var enumName = isNaN(enumValue[0]) ? enumValue : name.toUpperCase() + '_' + enumValue; enumObjValues.push ({'enumName': enumName, 'enumValue': enumValue}); }); enumDefinition.enumValues = enumObjValues; return enumDefinition; }; return Generator; })(); module.exports.Generator = Generator;