swagger-ts-generator
Version:
Given a swagger.json file, generates a number of TypeScript files which can be used as models and model-driven forms in Angular 2 (and above)
604 lines (548 loc) • 21.5 kB
JavaScript
;
let fs = require('fs');
let path = require('path');
let _ = require('lodash');
let utils = require('./utils');
const TS_SUFFIX = '.ts';
const MODEL_SUFFIX = '.model';
const MODEL_FILE_SUFFIX = `${MODEL_SUFFIX}${TS_SUFFIX}`;
const ROOT_NAMESPACE = 'root';
const VALDIDATORS_FILE_NAME = 'validators.ts';
const BASE_MODEL_FILE_NAME = 'base-model.ts';
module.exports = {
generateModelTSFiles: generateModelTSFiles,
}
function generateModelTSFiles(swagger, options) {
let folder = path.normalize(options.modelFolder);
// generate fixed file with non-standard validators for validation rules which can be defined in the swagger file
generateTSValidations(folder, options);
// generate fixed file with the BaseModel class
generateTSBaseModel(folder, options);
// get type definitions from swagger
let typeCollection = getTypeDefinitions(swagger, options, MODEL_SUFFIX, MODEL_FILE_SUFFIX);
// group types per namespace
let namespaceGroups = getNamespaceGroups(typeCollection);
// generate model files
generateTSModels(namespaceGroups, folder, options);
// generate barrel files (index files to simplify import statements)
generateBarrelFiles(namespaceGroups, folder, options);
}
function generateTSValidations(folder, options) {
if(!options.generateClasses)
return;
let outputFileName = path.join(folder, VALDIDATORS_FILE_NAME);
let data = {}
let template = utils.readAndCompileTemplateFile('generate-validators-ts.hbs');
let result = template(data);
utils.ensureFolder(folder);
let isChanged = utils.writeFileIfContentsIsChanged(outputFileName, result);
if (isChanged) {
utils.log(`generated ${outputFileName}`);
}
}
function generateTSBaseModel(folder, options) {
if(!options.generateClasses)
return;
let outputFileName = path.join(folder, BASE_MODEL_FILE_NAME);
let data = {}
let template = utils.readAndCompileTemplateFile('generate-base-model-ts.hbs');
let result = template(data);
utils.ensureFolder(folder);
let isChanged = utils.writeFileIfContentsIsChanged(outputFileName, result);
if (isChanged) {
utils.log(`generated ${outputFileName}`);
}
}
function getTypeDefinitions(swagger, options, suffix, fileSuffix) {
let typeCollection = new Array();
// console.log('typesToFilter', options.typesToFilter);
_.forEach(swagger.definitions, (item, key) => {
if (!utils.isInTypesToFilter(item, key, options)) {
let type = getTypeDefinition(swagger, typeCollection, item, key, options, suffix, fileSuffix);
if (type) {
typeCollection.push(type);
}
}
});
// filter on unique types
typeCollection = _.uniq(typeCollection, 'type');
return typeCollection;
}
function getTypeDefinition(swagger, typeCollection, item, key, options, suffix, fileSuffix) {
// filter enum types (these are gererated by the enumGenerator)
let isEnumType = getIsEnumType(item);
if (isEnumType) {
return undefined;
}
let required = item.required || [];
let namespace = getNamespace(key, options, true);
let fullNamespace = getNamespace(key, options, false);
let typeName = getTypeName(key, options);
if (getIsGenericType(typeName)) {
typeName = convertGenericToGenericT(typeName);
}
let pathToRoot = utils.getPathToRoot(namespace);
let properties = options.sortModelProperties ? utils.getSortedObjectProperties(item.properties) : item.properties;
let baseType = undefined;
let baseImportFile = undefined;
let isSubType = getIsSubType(item);
if (isSubType) {
baseType = getBaseType(typeCollection, item, options);
baseImportFile = getImportFile(baseType.typeName, baseType.namespace, pathToRoot, suffix);
required = getSubTypeRequired(item);
properties = getSubTypeProperties(item, baseType);
}
let type = {
fileName: getFileName(key, options, fileSuffix),
typeName: typeName,
namespace: namespace,
fullNamespace: fullNamespace,
isSubType: isSubType,
baseType: baseType,
baseImportFile: baseImportFile,
path: utils.convertNamespaceToPath(namespace),
pathToRoot: pathToRoot,
properties: []
}
_.forEach(properties, (item, key) => {
let property = getTypePropertyDefinition(swagger, type, baseType, required, item, key, options, suffix, fileSuffix)
type.properties.push(property);
});
return type;
}
function getTypePropertyDefinition(swagger, type, baseType, required, item, key, options, suffix, fileSuffix) {
let isRefType = item.$ref;
let isArray = item.type == 'array';
let isEnum = (item.type == 'string' && item.enum) ||
(isArray && item.items && item.items.type == 'string' && item.items.enum) ||
((isRefType || isArray) && getIsEnumRefType(swagger, item, isArray));
// enum ref types are not handles as model types (they are handled in the enumGenerator)
if (isEnum) {
isRefType = false;
}
let propertyType = getPropertyType(item, key, options, isEnum);
let isComplexType = isRefType || isArray; // new this one in constructor
let isImportType = isRefType || (isArray && item.items.$ref && !isEnum);
let importType = isImportType ? getImportType(propertyType.typeName, isArray) : undefined;
let importFile = isImportType ? getImportFile(importType, propertyType.namespace, type.pathToRoot, suffix) : undefined;
let importTypeIsPropertyType = importType === type.typeName;
let isUniqueImportType = isImportType && !importTypeIsPropertyType && getIsUniqueImportType(importType, baseType, type.properties); // import this type
let validators = getTypePropertyValidatorDefinitions(required, item, key, propertyType.typeName, isEnum);
let hasValidation = _.keys(validators.validation).length > 0;
let importEnumType = isEnum ? removeGenericArray(propertyType.typeName, isArray) : undefined;
let isUniqueImportEnumType = isEnum && getIsUniqueImportEnumType(propertyType.typeName, type.properties); // import this enumType
let property = {
name: key,
typeName: propertyType.typeName,
namespace: propertyType.namespace,
description: item.description,
hasValidation: hasValidation,
isComplexType: isComplexType,
isImportType: isImportType,
isUniqueImportType: isUniqueImportType,
importType: importType,
importFile: importFile,
isEnum: isEnum,
isUniqueImportEnumType: isUniqueImportEnumType,
importEnumType: importEnumType,
isArray: isArray,
isArrayComplexType: propertyType.isArrayComplexType,
arrayTypeName: propertyType.arrayTypeName,
validators: validators,
enum: item.enum,
}
return property;
}
function getTypePropertyValidatorDefinitions(required, item, key, typeName, isEnum) {
let isRequired = _.indexOf(required, key) !== -1;
// console.log('key=', key, 'typeName', typeName, 'item=', item, 'enum=', item.enum);
let validators = {
validation: {},
validatorArray: []
}
if (isRequired) {
validators.validation.required = isRequired
validators.validatorArray.push('Validators.required');
}
if (_.has(item, 'minimum')) {
validators.validation.minimum = item.minimum
validators.validatorArray.push(`minValueValidator(${item.minimum})`);
}
if (_.has(item, 'maximum')) {
validators.validation.maximum = item.maximum
validators.validatorArray.push(`maxValueValidator(${item.maximum})`);
}
if (isEnum) {
validators.validation.enum = `'${item.enum}'`
validators.validatorArray.push(`enumValidator(${typeName})`);
}
if (_.has(item, 'minLength')) {
validators.validation.minLength = item.minLength
validators.validatorArray.push(`Validators.minLength(${item.minLength})`);
}
if (_.has(item, 'maxLength')) {
validators.validation.maxLength = item.maxLength
validators.validatorArray.push(`Validators.maxLength(${item.maxLength})`);
}
if (_.has(item, 'pattern')) {
validators.validation.pattern = `'${item.pattern}'`
validators.validatorArray.push(`Validators.pattern('${item.pattern}')`);
}
return validators;
}
function getIsUniqueImportType(currentTypeName, baseType, typeProperties) {
let baseTypeName = baseType ? baseType.typeName : undefined;
if (currentTypeName === baseTypeName) {
return false;
}
return !_.some(typeProperties, (property) => {
return property.importType === currentTypeName;
});
}
function getIsUniqueImportEnumType(currentTypeName, typeProperties) {
return !_.some(typeProperties, (property) => {
return property.importEnumType === currentTypeName;
});
}
function getTypeNameWithoutNamespacePrefixesToRemove(key, options) {
if (!options.namespacePrefixesToRemove) {
return key;
}
let namespaces = options.namespacePrefixesToRemove;
namespaces.forEach((item) => {
key = key.replace(item, '');
if (key[0] === '.') {
key = key.substring(1);
}
});
return key;
}
function getPropertyType(item, name, options, isEnum) {
let result = {
typeName: '',
namespace: '',
fullNamespace: undefined,
isArrayComplexType: false,
arrayTypeName: undefined
}
if (item.type) {
result.typeName = item.type;
if (item.type == 'integer') {
result.typeName = 'number'
};
if (item.type == 'string' && item.format == 'date') {
result.typeName = 'Date'
};
if (item.type == 'string' && item.format == 'date-time') {
result.typeName = 'Date'
};
if (item.type == 'string' && item.enum) {
result.typeName = `${name}`
};
if (item.type == 'array' && item.items) {
let arrayPropType = getPropertyType(item.items, name, options, isEnum);
result.typeName = `Array<${arrayPropType.typeName}>`;
result.namespace = arrayPropType.namespace;
result.isArrayComplexType = !isEnum ? item.items.$ref : false;
result.arrayTypeName = arrayPropType.typeName;
};
// description may contain an overrule type for enums, eg /** type CoverType */
if (utils.hasTypeFromDescription(item.description)) {
result.typeName = _.lowerFirst(utils.getTypeFromDescription(item.description))
// fix enum array with overrule type
if (item.type == 'array' && item.items) {
result.arrayTypeName = result.typeName;
result.typeName = `Array<${result.typeName}>`;
}
}
return result;
}
if (item.$ref) {
let type = removeDefinitionsRef(item.$ref);
result.typeName = getTypeName(type, options);
result.namespace = getNamespace(type, options, true);
result.fullNamespace = getNamespace(type, options, false);
// TODO add more C# primitive types
if (getIsGenericType(result.typeName)) {
let genericTType = getGenericTType(result.typeName);
if (genericTType && genericTType === 'System.DateTime') {
result.typeName = result.typeName.replace(genericTType, 'Date');
}
}
return result;
}
}
function removeDefinitionsRef(value) {
let result = value.replace('#/definitions/', '');
return result;
}
function getFileName(type, options, fileSuffix) {
let typeName = removeGenericTType(getTypeName(type, options));
return `${_.kebabCase(typeName)}${fileSuffix}`;
}
function getTypeName(type, options) {
let typeName;
if (getIsGenericType(type)) {
let startGenericT = type.indexOf('[');
let startGenericType = type.lastIndexOf('.', startGenericT) + 1;
typeName = type.substring(startGenericType);
typeName = convertGenericTypeName(typeName);
typeName = getTypeNameWithoutSuffixesToRemove(typeName, options);
} else {
typeName = type.split('.').pop();
typeName = getTypeNameWithoutSuffixesToRemove(typeName, options);
// C# Object affects Typescript Object - fix this
if (typeName === 'Object') {
typeName = 'SystemObject';
}
//classNameSuffixesToRemove
}
return _.upperFirst(typeName);
}
function getTypeNameWithoutSuffixesToRemove(typeName, options) {
if (!options.typeNameSuffixesToRemove) {
return typeName;
}
let typeNameSuffixesToRemove = options.typeNameSuffixesToRemove;
typeNameSuffixesToRemove.forEach((item) => {
if (_.endsWith(typeName, item)) {
typeName = typeName.slice(0, -item.length);
}
});
return typeName;
}
function getIsGenericType(type) {
return type.indexOf('[') !== -1 || type.indexOf('<') !== -1;
}
/**
* NullableOrEmpty<System.Date> -> System.Data
*/
function getGenericTType(type) {
if (getIsGenericType(type)) {
return /\<(.*)\>/.exec(type)[1];
}
return undefined;
}
/**
* NullableOrEmpty<System.Date> -> NullableOrEmpty
*/
function removeGenericTType(type) {
if (getIsGenericType(type)) {
type = type.substring(0, type.indexOf('<'));
}
return type;
}
/**
* NullableOrEmpty[System.Date] -> NullableOrEmpty<System.Date>
*/
function convertGenericTypeName(typeName) {
return typeName.replace('[', '<').replace(']', '>');
}
/**
* NullableOrEmpty<System.Date> -> NullableOrEmpty<T>
*/
function convertGenericToGenericT(typeName) {
return typeName.replace(/\<.*\>/, '<T>');
}
function getIsSubType(item) {
return item.allOf !== undefined;
}
function getBaseType(typeCollection, item, options) {
// TODO how about more than one baseType?
let type = removeDefinitionsRef(item.allOf[0].$ref);
let typeName = getTypeName(type, options);
//let namespace = getNamespace(type, options);
let baseType = _.find(typeCollection, type => {
return type.typeName === typeName;
//return type.typeName === typeName && type.namespace === namespace;
})
return baseType;
}
function getSubTypeProperties(item, baseType) {
// TODO strip properties which are defined in the baseType?
let properties = item.allOf[1].properties;
return properties;
}
function getSubTypeRequired(item) {
let required = item.allOf[1].required || [];
return required;
}
function getIsEnumType(item) {
return item && item.enum;
}
function getIsEnumRefType(swagger, item, isArray) {
let refItemName;
if (isArray) {
// "perilTypesIncluded": {
// "type": "array",
// "items": {
// "$ref": "#/definitions/PerilTypeIncluded"
// }
// },
if (item.items.$ref) {
refItemName = removeDefinitionsRef(item.items.$ref);
}
} else {
// "riskLocationSupraentity": {
// "$ref": "#/definitions/LocationSupraentity",
// "description": "type LocationSupraentity "
// },
refItemName = removeDefinitionsRef(item.$ref);
}
let refItem = swagger.definitions[refItemName];
return getIsEnumType(refItem);
}
function getNamespace(type, options, removePrefix) {
let typeName = removePrefix ? getTypeNameWithoutNamespacePrefixesToRemove(type, options) : type;
if (getIsGenericType(typeName)) {
let first = typeName.substring(0, typeName.indexOf('['));
typeName = first + first.substring(typeName.indexOf(']'));
}
let parts = typeName.split('.');
parts.pop();
return parts.join('.');
}
function getImportType(type, isArray) {
if (isArray) {
let result = removeGenericArray(type, isArray);
return result;
}
type = removeGenericTType(type);
return type;
}
function removeGenericArray(type, isArray) {
if (isArray) {
let result = type.replace('Array<', '').replace('>', '');
return result;
}
return type;
}
function getImportFile(propTypeName, propNamespace, pathToRoot, suffix) {
//return `${_.lowerFirst(type)}${suffix}`;
let importPath = `${_.kebabCase(propTypeName)}${suffix}`;
if (propNamespace) {
let namespacePath = utils.convertNamespaceToPath(propNamespace);
importPath = `${namespacePath}/${importPath}`;
}
return (pathToRoot + importPath).toLocaleLowerCase();
}
function getNamespaceGroups(typeCollection) {
let namespaces = {
[ROOT_NAMESPACE]: []
};
for (let i = 0; i < typeCollection.length; ++i) {
let type = typeCollection[i];
let namespace = type.namespace || ROOT_NAMESPACE;
if (!namespaces[namespace]) {
namespaces[namespace] = [];
}
namespaces[namespace].push(type);
}
return namespaces;
}
function generateTSModels(namespaceGroups, folder, options) {
let data = {
generateClasses: options.generateClasses,
moduleName: options.modelModuleName,
enumModuleName: options.enumModuleName,
enumRef: options.enumRef,
type: undefined,
}
let template = utils.readAndCompileTemplateFile('generate-model-ts.hbs');
utils.ensureFolder(folder);
for (let namespace in namespaceGroups) {
let typeCol = namespaceGroups[namespace];
let firstType = typeCol[0] || {
namespace: ''
};
let namespacePath = utils.convertNamespaceToPath(firstType.namespace);
let typeFolder = `${folder}${namespacePath}`;
let folderParts = namespacePath.split('/');
let prevParts = folder;
folderParts.forEach((part) => {
prevParts += part + '/';
utils.ensureFolder(prevParts);
});
let nrGeneratedFiles = 0;
_.each(typeCol, (type) => {
let outputFileName = path.join(typeFolder, type.fileName);
data.type = type;
let result = template(data);
let isChanged = utils.writeFileIfContentsIsChanged(outputFileName, result);
if (isChanged) {
nrGeneratedFiles++;
}
//fs.writeFileSync(outputFileName, result, { flag: 'w', encoding: utils.ENCODING });
});
utils.log(`generated ${nrGeneratedFiles} type${nrGeneratedFiles === 1 ? '' : 's'} in ${typeFolder}`);
removeFilesOfNonExistingTypes(typeCol, typeFolder, options, MODEL_FILE_SUFFIX);
}
let namespacePaths = Object.keys(namespaceGroups).map(namespace => {
return path.join(folder, utils.convertNamespaceToPath(namespace));
});
cleanFoldersForObsoleteFiles(folder, namespacePaths);
}
function cleanFoldersForObsoleteFiles(folder, namespacePaths) {
utils.getDirectories(folder).forEach(name => {
let folderPath = path.join(folder, name);
// TODO bij swagger-zib-v2 wordt de webapi/ZIB folder weggegooid !
let namespacePath = _.find(namespacePaths, path => {
return path.startsWith(folderPath);
});
if (!namespacePath) {
utils.removeFolder(folderPath);
utils.log(`removed obsolete folder ${name} in ${folder}`);
} else {
cleanFoldersForObsoleteFiles(folderPath, namespacePaths);
}
});
}
function generateBarrelFiles(namespaceGroups, folder, options) {
let data = {
fileNames: undefined
};
let template = utils.readAndCompileTemplateFile('generate-barrel-ts.hbs');
for (let key in namespaceGroups) {
data.fileNames = namespaceGroups[key].map((type) => {
return removeTsExtention(type.fileName);
});
if (key === ROOT_NAMESPACE) {
addRootFixedFileNames(data.fileNames, options);
}
let namespacePath = namespaceGroups[key][0] ? namespaceGroups[key][0].path : '';
let outputFileName = path.join(folder + namespacePath, 'index.ts');
let result = template(data);
let isChanged = utils.writeFileIfContentsIsChanged(outputFileName, result);
if (isChanged) {
utils.log(`generated ${outputFileName}`);
}
}
}
function addRootFixedFileNames(fileNames, options) {
let enumOutputFileName = path.normalize(options.enumTSFile.split('/').pop());
fileNames.splice(0, 0, removeTsExtention(enumOutputFileName));
if(options.generateClasses) {
let validatorsOutputFileName = path.normalize(VALDIDATORS_FILE_NAME);
fileNames.splice(0, 0, removeTsExtention(validatorsOutputFileName));
}
}
function removeTsExtention(fileName) {
return fileName.replace('.ts', '');
}
function removeFilesOfNonExistingTypes(typeCollection, folder, options, suffix) {
// remove files of types which are no longer defined in typeCollection
let counter = 0;
let files = fs.readdirSync(folder);
_.each(files, (file) => {
if (_.endsWith(file, suffix) && !_.find(typeCollection, (type) => {
return type.fileName == file;
})) {
counter++;
fs.unlinkSync(path.join(folder, file));
utils.log(`removed ${file} in ${folder}`);
}
});
if (counter > 0) {
utils.log(`removed ${counter} types in ${folder}`);
}
}