swagger-typescript-api-generator
Version:
Angular API client generator from swagger json
752 lines (616 loc) • 31.2 kB
JavaScript
'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;