UNPKG

ng-swagger-gen

Version:
1,419 lines (1,328 loc) 44 kB
'use strict'; /* jshint -W014 */ /* jshint -W083 */ const fs = require('fs'); const path = require('path'); const Mustache = require('mustache'); const $RefParser = require('json-schema-ref-parser'); var npmConfig = require('npm-conf'); /** * Main generate function */ function ngSwaggerGen(options) { if (typeof options.swagger != 'string') { console.error("Swagger file not specified in the 'swagger' option"); process.exit(1); } if (!options.skipProxySetup) { setupProxy(); } $RefParser.bundle(options.swagger, { dereference: { circular: false }, resolve: { http: { timeout: options.timeout } } }).then( data => { doGenerate(data, options); }, err => { console.error( `Error reading swagger location ${options.swagger}: ${err}` ); process.exit(1); } ).catch(function (error) { console.error(`Error: ${error}`); process.exit(1); }); } /** * Sets up the environment to work behind proxies. * Uses global-agent from NodeJS >= 10, * and global-tunnel-ng for previous versions. */ function setupProxy() { var globalAgent = require('global-agent'); var globalTunnel = require('global-tunnel-ng'); var proxyAddress = getProxyAndSetupEnv(); const NODEJS_VERSION = parseInt(process.version.slice(1).split('.')[0], 10); if (NODEJS_VERSION >= 10 && proxyAddress) { // `global-agent` works with Node.js v10 and above. globalAgent.bootstrap(); global.GLOBAL_AGENT.HTTP_PROXY = proxyAddress; } else { // `global-tunnel-ng` works only with Node.js v10 and below. globalTunnel.initialize(); } } /** * For full compatibility with globalTunnel we need to check a few places for * the correct proxy address. Additionally we need to remove HTTP_PROXY * and HTTPS_PROXY environment variables, if present. * This is again for globalTunnel compatibility. * * This method only needs to be run when global-agent is used */ function getProxyAndSetupEnv() { var proxyEnvVariableNames = [ 'https_proxy', 'HTTPS_PROXY', 'http_proxy', 'HTTP_PROXY' ]; var npmVariableNames = ['https-proxy', 'http-proxy', 'proxy']; var key; var val; var result; for (var i = 0; i < proxyEnvVariableNames.length; i++) { key = proxyEnvVariableNames[i]; val = process.env[key]; if (val) { // Get the first non-empty result = result || val; // Delete all // NB: we do it here to prevent double proxy handling // (and for example path change) // by us and the `request` module or other sub-dependencies delete process.env[key]; } } if (!result) { var config = npmConfig(); for (i = 0; i < npmVariableNames.length && !result; i++) { result = config.get(npmVariableNames[i]); } } return result; } /** * Proceeds with the generation given the parsed swagger object */ function doGenerate(swagger, options) { if (!options.templates) { options.templates = path.join(__dirname, 'templates'); } var output = path.normalize(options.output || 'src/app/api'); var prefix = options.prefix || 'Api'; if (swagger.swagger !== '2.0') { console.error( 'Invalid swagger specification. Must be a 2.0. Currently ' + swagger.swagger ); process.exit(1); } swagger.paths = swagger.paths || {}; swagger.models = swagger.models || []; var models = processModels(swagger, options); var services = processServices(swagger, models, options); // Apply the tag filter. If includeTags is null, uses all services, // but still can remove unused models const includeTags = options.includeTags; if (typeof includeTags == 'string') { options.includeTags = includeTags.split(','); } const excludeTags = options.excludeTags; if (typeof excludeTags == 'string') { options.excludeTags = excludeTags.split(','); } applyTagFilter(models, services, options); // Read the templates var templates = {}; var files = fs.readdirSync(options.templates); files.forEach(function (file, index) { var pos = file.indexOf('.mustache'); if (pos >= 0) { var fullFile = path.join(options.templates, file); templates[file.substr(0, pos)] = fs.readFileSync(fullFile, 'utf-8'); } }); // read the fallback templates var fallbackTemplates = path.join(__dirname, 'templates'); fs.readdirSync(fallbackTemplates) .forEach(function (file) { var pos = file.indexOf('.mustache'); if (pos >= 0) { var fullFile = path.join(fallbackTemplates, file); if (!(file.substr(0, pos) in templates)) { templates[file.substr(0, pos)] = fs.readFileSync(fullFile, 'utf-8'); } } }); // Prepare the output folder const modelsOutput = path.join(output, 'models'); const servicesOutput = path.join(output, 'services'); mkdirs(modelsOutput); mkdirs(servicesOutput); var removeStaleFiles = options.removeStaleFiles !== false; var generateEnumModule = options.enumModule !== false; // Utility function to render a template and write it to a file var generate = function (template, model, file) { var code = Mustache.render(template, model, templates) .replace(/[^\S\r\n]+$/gm, ''); fs.writeFileSync(file, code, 'UTF-8'); console.info('Wrote ' + file); }; // Calculate the globally used names var moduleClass = toClassName(prefix + 'Module'); var moduleFile = toFileName(moduleClass); // Angular's best practices demands xxx.module.ts, not xxx-module.ts moduleFile = moduleFile.replace(/\-module$/, '.module'); var configurationClass = toClassName(prefix + 'Configuration'); var configurationInterface = toClassName(prefix + 'ConfigurationInterface'); var configurationFile = toFileName(configurationClass); function applyGlobals(to) { to.prefix = prefix; to.moduleClass = moduleClass; to.moduleFile = moduleFile; to.configurationClass = configurationClass; to.configurationInterface = configurationInterface; to.configurationFile = configurationFile; return to; } // Write the models and examples var modelsArray = []; for (var modelName in models) { var model = models[normalizeModelName(modelName)]; if (model.modelIsEnum) { model.enumModule = generateEnumModule; } applyGlobals(model); // When the model name differs from the class name, it will be duplicated // in the array. For example the-user would be TheUser, and would be twice. if (modelsArray.includes(model)) { continue; } modelsArray.push(model); generate( templates.model, model, path.join(modelsOutput, model.modelFile + '.ts') ); if (options.generateExamples && model.modelExample) { var value = resolveRefRecursive(model.modelExample, swagger); var example = JSON.stringify(value, null, 2); example = example.replace(/'/g, "\\'"); example = example.replace(/"/g, "'"); example = example.replace(/\n/g, "\n "); model.modelExampleStr = example; generate( templates.example, model, path.join(modelsOutput, model.modelExampleFile + '.ts') ); } } if (modelsArray.length > 0) { modelsArray[modelsArray.length - 1].modelIsLast = true; } if (removeStaleFiles) { var modelFiles = fs.readdirSync(modelsOutput); modelFiles.forEach((file, index) => { var ok = false; var basename = path.basename(file); for (var modelName in models) { var model = models[normalizeModelName(modelName)]; if (basename == model.modelFile + '.ts' || basename == model.modelExampleFile + '.ts' && model.modelExampleStr != null) { ok = true; break; } } if (!ok) { rmIfExists(path.join(modelsOutput, file)); } }); } // Write the model index var modelIndexFile = path.join(output, 'models.ts'); if (options.modelIndex !== false) { generate(templates.models, { models: modelsArray }, modelIndexFile); } else if (removeStaleFiles) { rmIfExists(modelIndexFile); } // Write the StrictHttpResponse type generate(templates.strictHttpResponse, {}, path.join(output, 'strict-http-response.ts')); // Write the services var servicesArray = []; for (var serviceName in services) { var service = services[serviceName]; service.generalErrorHandler = options.errorHandler !== false; applyGlobals(service); servicesArray.push(service); generate( templates.service, service, path.join(servicesOutput, service.serviceFile + '.ts') ); } if (servicesArray.length > 0) { servicesArray[servicesArray.length - 1].serviceIsLast = true; } if (removeStaleFiles) { var serviceFiles = fs.readdirSync(servicesOutput); serviceFiles.forEach((file, index) => { var ok = false; var basename = path.basename(file); for (var serviceName in services) { var service = services[serviceName]; if (basename == service.serviceFile + '.ts') { ok = true; break; } } if (!ok) { rmIfExists(path.join(servicesOutput, file)); } }); } // Write the service index var serviceIndexFile = path.join(output, 'services.ts'); if (options.serviceIndex !== false) { generate(templates.services, { services: servicesArray }, serviceIndexFile); } else if (removeStaleFiles) { rmIfExists(serviceIndexFile); } // Write the module var fullModuleFile = path.join(output, moduleFile + '.ts'); if (options.apiModule !== false) { generate(templates.module, applyGlobals({ services: servicesArray }), fullModuleFile); } else if (removeStaleFiles) { rmIfExists(fullModuleFile); } // Write the configuration { var rootUrl = ''; if (swagger.hasOwnProperty('host') && swagger.host !== '') { var schemes = swagger.schemes || []; var scheme = schemes.length === 0 ? '//' : schemes[0] + '://'; rootUrl = scheme + swagger.host; } if (swagger.hasOwnProperty('basePath') && swagger.basePath !== '' && swagger.basePath !== '/') { rootUrl += swagger.basePath; } generate(templates.configuration, applyGlobals({ rootUrl: rootUrl, }), path.join(output, configurationFile + '.ts') ); } // Write the BaseService { generate(templates.baseService, applyGlobals({}), path.join(output, 'base-service.ts')); } } function normalizeModelName(name) { return name.toLowerCase(); } /** * Applies a filter over the given services, keeping only the specific tags. * Also optionally removes any unused models, even services are filtered. */ function applyTagFilter(models, services, options) { var i; // Normalize the included tag names const includeTags = options.includeTags; var included = null; if (includeTags && includeTags.length > 0) { included = []; for (i = 0; i < includeTags.length; i++) { included.push(tagName(includeTags[i], options)); } } // Normalize the excluded tag names const excludeTags = options.excludeTags; var excluded = null; if (excludeTags && excludeTags.length > 0) { excluded = []; for (i = 0; i < excludeTags.length; i++) { excluded.push(tagName(excludeTags[i], options)); } } // Filter out the unused models var ignoreUnusedModels = options.ignoreUnusedModels !== false; var usedModels = new Set(); const addToUsed = (dep) => usedModels.add(dep); for (var serviceName in services) { var include = (!included || included.indexOf(serviceName) >= 0) && (!excluded || excluded.indexOf(serviceName) < 0); if (!include) { // This service is skipped - remove it console.info( 'Ignoring service ' + serviceName + ' because it was not included' ); delete services[serviceName]; } else if (ignoreUnusedModels) { // Collect the models used by this service var service = services[serviceName]; service.serviceDependencies.forEach(addToUsed); service.serviceErrorDependencies.forEach(addToUsed); } } if (ignoreUnusedModels) { // Collect the model dependencies of models, so unused can be removed var allDependencies = new Set(); usedModels.forEach(dep => collectDependencies(allDependencies, dep, models) ); // Remove all models that are unused for (var modelName in models) { var model = models[normalizeModelName(modelName)]; if (!allDependencies.has(model.modelClass)) { // This model is not used - remove it console.info( 'Ignoring model ' + modelName + ' because it was not used by any service' ); delete models[normalizeModelName(modelName)]; } } } } /** * Collects on the given dependencies set all dependencies of the given model */ function collectDependencies(dependencies, model, models) { if (!model || dependencies.has(model.modelClass)) { return; } dependencies.add(model.modelClass); if (model.modelDependencies) { model.modelDependencies.forEach((dep) => collectDependencies(dependencies, dep, models) ); } } /** * Creates all sub-directories for a nested path * Thanks to https://github.com/grj1046/node-mkdirs/blob/master/index.js */ function mkdirs(folderPath, mode) { var folders = []; var tmpPath = path.normalize(folderPath); var exists = fs.existsSync(tmpPath); while (!exists) { folders.push(tmpPath); tmpPath = path.join(tmpPath, '..'); exists = fs.existsSync(tmpPath); } for (var i = folders.length - 1; i >= 0; i--) { fs.mkdirSync(folders[i], mode); } } /** * Removes the given file if it exists (logging the action) */ function rmIfExists(file) { if (fs.existsSync(file)) { console.info('Removing stale file ' + file); fs.unlinkSync(file); } } /** * Converts a given type name into a TS file name */ function toFileName(typeName) { var result = ''; var wasLower = false; for (var i = 0; i < typeName.length; i++) { var c = typeName.charAt(i); var isLower = /[a-z]/.test(c); if (!isLower && wasLower) { result += '-'; } result += c.toLowerCase(); wasLower = isLower; } return result; } /** * Converts a given name into a valid class name */ function toClassName(name) { var result = ''; var upNext = false; for (var i = 0; i < name.length; i++) { var c = name.charAt(i); var valid = /[\w]/.test(c); if (!valid) { upNext = true; } else if (upNext) { result += c.toUpperCase(); upNext = false; } else if (result === '') { result = c.toUpperCase(); } else { result += c; } } if (/[0-9]/.test(result.charAt(0))) { result = '_' + result; } return result; } /** * Resolves the simple reference name from a qualified reference */ function simpleRef(ref) { if (!ref) { return null; } var index = ref.lastIndexOf('/'); if (index >= 0) { ref = ref.substr(index + 1); } return toClassName(ref); } /** * Converts a given enum value into the enum name */ function toEnumName(value) { var result = ''; var wasLower = false; for (var i = 0; i < value.length; i++) { var c = value.charAt(i); var isLower = /[a-z]/.test(c); if (!isLower && wasLower) { result += '_'; } result += c.toUpperCase(); wasLower = isLower; } if (!isNaN(value[0])) { result = '_' + result; } result = result.replace(/[^\w]/g, '_'); result = result.replace(/_+/g, '_'); return result; } /** * Returns a multi-line comment for the given text */ function toComments(text, level) { var indent = ''; var i; for (i = 0; i < level; i++) { indent += ' '; } if (text == null || text.length === 0) { return indent; } const lines = text.trim().split('\n'); var result = '\n' + indent + '/**\n'; lines.forEach(line => { result += indent + ' *' + (line === '' ? '' : ' ' + line) + '\n'; }); result += indent + ' */\n' + indent; return result; } /** * Class used to resolve the model dependencies */ function DependenciesResolver(models, ownType) { this.models = models; this.ownType = ownType; this.dependencies = []; this.dependencyNames = []; } /** * Adds a candidate dependency */ DependenciesResolver.prototype.add = function (input) { let deps; if (input.allTypes) { deps = input.allTypes; } else { deps = [removeBrackets(input)]; } for (let i = 0; i < deps.length; i++) { let dep = deps[i]; if (this.dependencyNames.indexOf(dep) < 0 && dep !== this.ownType) { var depModel = this.models[normalizeModelName(dep)]; if (depModel) { this.dependencies.push(depModel); this.dependencyNames.push(depModel.modelClass); } } } }; /** * Returns the resolved dependencies as a list of models */ DependenciesResolver.prototype.get = function () { return this.dependencies; }; /** * Process each model, returning an object keyed by model name, whose values * are simplified descriptors for models. */ function processModels(swagger, options) { var name, model, i, property; var models = {}; for (name in swagger.definitions) { model = swagger.definitions[name]; var parents = null; var properties = null; var requiredProperties = null; var additionalPropertiesType = false; var example = model.example || null; var enumValues = null; var elementType = null; var simpleType = null; if (model.allOf != null && model.allOf.length > 0) { parents = model.allOf .filter(parent => !!parent.$ref) .map(parent => simpleRef(parent.$ref)); properties = (model.allOf.find(val => !!val.properties) || {}).properties || {}; requiredProperties = (model.allOf.find(val => !!val.required) || {}).required || []; if (parents && parents.length) { simpleType = null; enumValues = null; } } else if (model.type === 'string') { enumValues = model.enum || []; if (enumValues.length == 0) { simpleType = 'string'; enumValues = null; } else { for (i = 0; i < enumValues.length; i++) { var enumValue = enumValues[i]; var enumDescriptor = { enumName: toEnumName(enumValue), enumValue: String(enumValue).replace(/\'/g, '\\\''), enumIsLast: i === enumValues.length - 1, }; enumValues[i] = enumDescriptor; } } } else if (model.type === 'array') { elementType = propertyType(model); } else if (!model.type && (model.anyOf || model.oneOf)) { let of = model.anyOf || model.oneOf; let variants = of.map(propertyType); simpleType = { allTypes: mergeTypes(...variants), toString: () => variants.join(' |\n ') }; } else if (model.type === 'object' || model.type === undefined) { properties = model.properties || {}; requiredProperties = model.required || []; additionalPropertiesType = model.additionalProperties && (typeof model.additionalProperties === 'object' ? propertyType(model.additionalProperties) : 'any'); } else { simpleType = propertyType(model); } var modelClass = toClassName(name); var descriptor = { modelName: name, modelClass: modelClass, modelFile: toFileName(modelClass) + options.customFileSuffix.model, modelComments: toComments(model.description), modelParents: parents, modelIsObject: properties != null, modelIsEnum: enumValues != null, modelIsArray: elementType != null, modelIsSimple: simpleType != null, modelSimpleType: simpleType, properties: properties == null ? null : processProperties(swagger, properties, requiredProperties), modelExample: example, modelAdditionalPropertiesType: additionalPropertiesType, modelExampleFile: toFileName(name) + options.customFileSuffix.example, modelEnumValues: enumValues, modelElementType: elementType, modelSubclasses: [], }; if (descriptor.properties != null) { descriptor.modelProperties = []; for (var propertyName in descriptor.properties) { property = descriptor.properties[propertyName]; descriptor.modelProperties.push(property); } descriptor.modelProperties.sort((a, b) => { return a.propertyName < b.propertyName ? -1 : a.propertyName > b.propertyName ? 1 : 0; }); if (descriptor.modelProperties.length > 0) { descriptor.modelProperties[ descriptor.modelProperties.length - 1 ].propertyIsLast = true; } } models[normalizeModelName(name)] = descriptor; models[normalizeModelName(descriptor.modelClass)] = descriptor; } // Now that we know all models, process the hierarchies for (name in models) { model = models[normalizeModelName(name)]; if (!model.modelIsObject) { // Only objects can have hierarchies continue; } // Process the hierarchy var parents = model.modelParents; if (parents && parents.length > 0) { model.modelParents = parents .filter(parentName => !!parentName) .map(parentName => { // Make the parent be the actual model, not the name var parentModel = models[normalizeModelName(parentName)]; // Append this model on the parent's subclasses parentModel.modelSubclasses.push(model); return parentModel; }); model.modelParentNames = model.modelParents.map( (parent, index) => ({ modelClass: parent.modelClass, parentIsFirst: index === 0, }) ); } } // Now that the model hierarchy is ok, resolve the dependencies var addToDependencies = t => { if (Array.isArray(t.allTypes)) { t.allTypes.forEach(it => dependencies.add(it)); } else dependencies.add(t); }; for (name in models) { model = models[normalizeModelName(name)]; if (model.modelIsEnum || model.modelIsSimple && !model.modelSimpleType.allTypes) { // Enums or simple types have no dependencies continue; } var dependencies = new DependenciesResolver(models, model.modelName); // The parent is a dependency if (model.modelParents) { model.modelParents.forEach(modelParent => { dependencies.add(modelParent.modelName); }) } // Each property may add a dependency if (model.modelProperties) { for (i = 0; i < model.modelProperties.length; i++) { property = model.modelProperties[i]; addToDependencies(property.propertyType); } } // If an array, the element type is a dependency if (model.modelElementType) addToDependencies(model.modelElementType); if (model.modelSimpleType) addToDependencies(model.modelSimpleType); if (model.modelAdditionalPropertiesType) addToDependencies(model.modelAdditionalPropertiesType); model.modelDependencies = dependencies.get(); } return models; } /** * Removes an array designation from the given type. * For example, "Array<a>" returns "a", "a[]" returns "a", while "b" returns "b". * A special case is for inline objects. In this case, the result is "object". */ function removeBrackets(type, nullOrUndefinedOnly) { if (typeof nullOrUndefinedOnly === "undefined") { nullOrUndefinedOnly = false; } if (typeof type === 'object') { if (type.allTypes && type.allTypes.length === 1) { return removeBrackets(type.allTypes[0], nullOrUndefinedOnly); } return 'object'; } else if (type.replace(/ /g, '') !== type) { return removeBrackets(type.replace(/ /g, '')); } else if (type.indexOf('null|') === 0) { return removeBrackets(type.substr('null|'.length)); } else if (type.indexOf('undefined|') === 0) { // Not used currently, but robust code is better code :) return removeBrackets(type.substr('undefined|'.length)); } if (type == null || type.length === 0 || nullOrUndefinedOnly) { return type; } var pos = type.indexOf('Array<'); if (pos >= 0) { var start = 'Array<'.length; return type.substr(start, type.length - start - 1); } pos = type.indexOf('['); return pos >= 0 ? type.substr(0, pos) : type; } /** * Combine dependencies of multiple types. * @param types * @return {Array} */ function mergeTypes(...types) { let allTypes = []; types.forEach(type => { (type.allTypes || [type]).forEach(type => { if (allTypes.indexOf(type) < 0) allTypes.push(type); }); }); return allTypes; } /** * Returns the TypeScript property type for the given raw property */ function propertyType(property) { var type; if (property === null || property.type === null) { return 'null'; } else if (property.$ref != null) { // Type is a reference return simpleRef(property.$ref); } else if (property['x-type']) { // Type is read from the x-type vendor extension type = (property['x-type'] || '').toString().replace('List<', 'Array<'); return type.length == 0 ? 'null' : type; } else if (property['x-nullable']) { return 'null | ' + propertyType( Object.assign(property, { 'x-nullable': undefined })); } else if (!property.type && (property.anyOf || property.oneOf)) { let variants = (property.anyOf || property.oneOf).map(propertyType); return { allTypes: mergeTypes(...variants), toString: () => variants.join(' | ') }; } else if (!property.type && property.allOf) { // Do not want to include x-nullable types as part of an allOf union. let variants = (property.allOf).filter(prop => !prop['x-nullable']).map(propertyType); return { allTypes: mergeTypes(...variants), toString: () => variants.join(' & ') }; } else if (Array.isArray(property.type)) { let variants = property.type.map(type => propertyType(Object.assign({}, property, { type }))); return { allTypes: mergeTypes(...variants), toString: () => variants.join(' | ') }; } switch (property.type) { case 'null': return 'null'; case 'string': if (property.enum && property.enum.length > 0) { return '\'' + property.enum.join('\' | \'') + '\''; } else if (property.const) { return '\'' + property.const + '\''; } else if (property.format === 'byte') { return 'ArrayBuffer'; } return 'string'; case 'array': if (Array.isArray(property.items)) { // support for tuples if (!property.maxItems) return 'Array<any>'; // there is unable to define unlimited tuple in TypeScript let minItems = property.minItems || 0, maxItems = property.maxItems, types = property.items.map(propertyType); types.push(property.additionalItems ? propertyType(property.additionalItems) : 'any'); let variants = []; for (let i = minItems; i <= maxItems; i++) variants.push(types.slice(0, i)); return { allTypes: mergeTypes(...types.slice(0, maxItems)), toString: () => variants.map(types => `[${types.join(', ')}]`).join(' | ') }; } else { let itemType = propertyType(property.items); return { allTypes: mergeTypes(itemType), toString: () => 'Array<' + itemType + '>' }; } case 'integer': case 'number': if (property.enum && property.enum.length > 0) return property.enum.join(' | '); if (property.const) return property.const; return 'number'; case 'boolean': return 'boolean'; case 'file': return 'Blob'; case 'object': var def = '{'; let memberCount = 0; var allTypes = []; if (property.properties) { for (var name in property.properties) { var prop = property.properties[name]; if (memberCount++) def += ', '; type = propertyType(prop); allTypes.push(type); let required = property.required && property.required.indexOf(name) >= 0; def += name + (required ? ': ' : '?: ') + type; } } if (property.additionalProperties) { if (memberCount++) def += ', '; type = typeof property.additionalProperties === 'object' ? propertyType(property.additionalProperties) : 'any'; allTypes.push(type); def += '[key: string]: ' + type; } def += '}'; return { allTypes: mergeTypes(...allTypes), toString: () => def, }; default: return 'any'; } } /** * Process each property for the given properties object, returning an object * keyed by property name with simplified property types */ function processProperties(swagger, properties, requiredProperties) { var result = {}; for (var name in properties) { var property = properties[name]; var descriptor = { propertyName: name.indexOf('-') === -1 && name.indexOf(".") === -1 ? name : `"${name}"`, propertyComments: toComments(property.description, 1), propertyRequired: requiredProperties.indexOf(name) >= 0, propertyType: propertyType(property), }; result[name] = descriptor; } return result; } /** * Resolves a local reference in the given swagger file */ function resolveRef(swagger, ref) { if (ref.indexOf('#/') != 0) { console.error('Resolved references must start with #/. Current: ' + ref); process.exit(1); } var parts = ref.substr(2).split('/'); var result = swagger; for (var i = 0; i < parts.length; i++) { var part = parts[i]; result = result[part]; } return result === swagger ? {} : result; } /* * Process an operation's possible responses. Returns an object keyed * by each HTTP code, whose values are objects with code and type properties, * plus a property resultType, which is the type to the HTTP 2xx code. */ function processResponses(swagger, def, path, models) { var responses = def.responses || {}; var operationResponses = {}; operationResponses.returnHeaders = false; for (var code in responses) { var response = responses[code]; if (response.$ref) { response = resolveRef(swagger, response.$ref); } if (!response.schema) { continue; } var type = propertyType(response.schema); if (/2\d\d/.test(code)) { // Successful response if (operationResponses.resultType) { // More than one successful response, use union type operationResponses.resultType += ` | ${type}`; operationResponses.resultDescription += ` or ${response.description}` } else { operationResponses.resultType = type; operationResponses.resultDescription = response.description; } var headers = response.headers || {}; for (var prop in headers) { // This operation returns at least one header operationResponses.returnHeaders = true; break; } } operationResponses[code] = { code: code, type: type, }; } if (!operationResponses.resultType) { operationResponses.resultType = 'null'; } return operationResponses; } /** * Returns a path expression to be evaluated, for example: * "/a/{var1}/b/{var2}/" returns "/a/${params.var1}/b/${params.var2}" * if there is a parameters class, or "/a/${var1}/b/${var2}" otherwise. */ function toPathExpression(operationParameters, paramsClass, path) { return (path || '').replace(/\{([^}]+)}/g, (_, pName) => { const param = operationParameters.find(p => p.paramName === pName); const paramName = param ? param.paramVar : pName; return paramsClass ? "${encodeURIComponent(String(params." + paramName + "))}" : "${encodeURIComponent(String(" + paramName + "))}"; }); } /** * Transforms the given string into a valid identifier */ function toIdentifier(string) { var result = ''; var wasSep = false; for (var i = 0; i < string.length; i++) { var c = string.charAt(i); if (/[a-zA-Z0-9]/.test(c)) { if (wasSep) { c = c.toUpperCase(); wasSep = false; } result += c; } else { wasSep = true; } } return result; } /** * Normalizes the tag name. Actually, capitalizes the given name. * If the given tag is null, returns the default from options */ function tagName(tag, options) { if (tag == null || tag === '') { tag = options.defaultTag || 'Api'; } tag = toIdentifier(tag); return tag.charAt(0).toUpperCase() + (tag.length == 1 ? '' : tag.substr(1)); } /** * Returns the actual operation id, assuming the one given. * If none is given, generates one */ function operationId(given, method, url, allKnown) { var id; var generate = given == null; if (generate) { id = toIdentifier(method + url); } else { id = toIdentifier(given); } var duplicated = allKnown.has(id); if (duplicated) { var i = 1; while (allKnown.has(id + '_' + i)) { i++; } id = id + '_' + i; } if (generate) { console.warn( "Operation '" + method + "' on '" + url + "' defines no operationId. Assuming '" + id + "'." ); } else if (duplicated) { console.warn( "Operation '" + method + "' on '" + url + "' defines a duplicated operationId: " + given + '. ' + "Assuming '" + id + "'." ); } allKnown.add(id); return id; } /** * Process API paths, returning an object with descriptors keyed by tag name. * It is required that operations define a single tag, or they are ignored. */ function processServices(swagger, models, options) { var param, name, i, j; var services = {}; var minParamsForContainer = options.minParamsForContainer || 2; var sortParams = options.sortParams || 'desc'; for (var url in swagger.paths) { var path = swagger.paths[url]; var methodParameters = path.parameters; for (var method in path || {}) { var def = path[method]; if (!def || method == 'parameters') { continue; } var tags = def.tags || []; var tag = tagName(tags.length == 0 ? null : tags[0], options); var descriptor = services[tag]; if (descriptor == null) { var serviceClass = toClassName(tag); descriptor = { serviceName: tag, serviceClass: serviceClass + 'Service', serviceFile: toFileName(serviceClass) + options.customFileSuffix.service, operationIds: new Set(), serviceOperations: [], }; services[tag] = descriptor; } var id = operationId( def.operationId, method, url, descriptor.operationIds ); var parameters = def.parameters || []; if (methodParameters) { parameters = parameters.concat(methodParameters); } var paramsClass = null; var paramsClassComments = null; if (parameters.length >= minParamsForContainer) { paramsClass = id.charAt(0).toUpperCase() + id.substr(1) + 'Params'; paramsClassComments = toComments('Parameters for ' + id, 1); } var operationParameters = []; for (var p = 0; p < parameters.length; p++) { param = parameters[p]; if (param.$ref) { param = resolveRef(swagger, param.$ref); } var paramType; if (param.schema) { paramType = propertyType(param.schema); } else { paramType = propertyType(param); } var paramTypeNoNull = removeBrackets(paramType, true); var paramVar = toIdentifier(param.name); var paramDescriptor = { paramName: param.name, paramIn: param.in, paramVar: paramVar, paramFullVar: (paramsClass == null ? '' : 'params.') + paramVar, paramRequired: param.required === true || param.in === 'path', paramIsQuery: param.in === 'query', paramIsPath: param.in === 'path', paramIsHeader: param.in === 'header', paramIsBody: param.in === 'body', paramIsFormData: param.in === 'formData', paramIsArray: param.type === 'array', paramToJson: param.in === 'formData' && !param.enum && paramTypeNoNull !== 'Blob' && paramTypeNoNull !== 'string', paramDescription: param.description, paramComments: toComments(param.description, 2), paramType: paramType, paramCollectionFormat: param.collectionFormat, }; operationParameters.push(paramDescriptor); } operationParameters.sort((a, b) => { if (a.paramRequired && !b.paramRequired) return -1; if (!a.paramRequired && b.paramRequired) return 1; switch (sortParams) { case 'asc': return a.paramName > b.paramName ? 1 : a.paramName < b.paramName ? -1 : 0; case 'desc': return a.paramName > b.paramName ? -1 : a.paramName < b.paramName ? 1 : 0; default: return 0; } }); if (operationParameters.length > 0) { operationParameters[operationParameters.length - 1].paramIsLast = true; } var operationResponses = processResponses(swagger, def, path, models); var resultType = operationResponses.resultType; var isMultipart = false; for (i = 0; i < operationParameters.length; i++) { if (operationParameters[i].paramIsFormData) { isMultipart = true; break; } } var docString = (def.description || '').trim(); var summary = (def.summary || path.summary || '').trim(); if (summary !== '') { if (docString === '') { docString = summary; } else { docString = summary + '\n\n' + docString; } } if (paramsClass == null) { for (i = 0; i < operationParameters.length; i++) { param = operationParameters[i]; docString += '\n@param ' + param.paramName + ' ' + param.paramDescription; } } else { docString += '\n@param params The `' + descriptor.serviceClass + '.' + paramsClass + '` containing the following parameters:\n'; for (i = 0; i < operationParameters.length; i++) { param = operationParameters[i]; docString += '\n- `' + param.paramName + '`: '; var lines = (param.paramDescription || '').trim().split('\n'); for (var l = 0; l < lines.length; l++) { var line = lines[l]; if (line === '') { docString += '\n'; } else { docString += (l == 0 ? '' : ' ') + line + '\n'; } } } } if (operationResponses.resultDescription) { docString += '\n@return ' + operationResponses.resultDescription; } function getOperationName(string) { if (options.camelCase) return string.charAt(0).toLowerCase() + string.slice(1); else return string; } var operation = { operationName: getOperationName(id), operationParamsClass: paramsClass, operationParamsClassComments: paramsClassComments, operationMethod: method.toLocaleUpperCase(), operationPath: url.replace(/\'/g, '\\\''), operationPathExpression: toPathExpression(operationParameters, paramsClass, url), operationResultType: resultType, operationHttpResponseType: '__StrictHttpResponse<' + resultType + '>', operationComments: toComments(docString, 1), operationParameters: operationParameters, operationResponses: operationResponses, }; var modelResult = models[normalizeModelName(removeBrackets(resultType))]; var actualType = resultType; if (modelResult && modelResult.modelIsSimple) { actualType = modelResult.modelSimpleType; } operation.operationIsMultipart = isMultipart; operation.operationIsVoid = actualType === 'void'; operation.operationIsString = actualType === 'string'; operation.operationIsNumber = actualType === 'number'; operation.operationIsOther = !['void', 'number', 'boolean'].includes(actualType); operation.operationIsBoolean = actualType === 'boolean'; operation.operationIsEnum = modelResult && modelResult.modelIsEnum; operation.operationIsObject = modelResult && modelResult.modelIsObject; operation.operationIsPrimitiveArray = !modelResult && (resultType.toString().includes('Array<') || resultType.toString().includes('[]')); operation.operationIsFile = actualType === 'Blob'; operation.operationIsByteArray = actualType === 'ArrayBuffer'; operation.operationResponseType = operation.operationIsFile ? 'blob' : operation.operationIsByteArray ? 'arraybuffer' : operation.operationIsVoid || operation.operationIsString || operation.operationIsNumber || operation.operationIsBoolean || operation.operationIsEnum ? 'text' : 'json'; operation.operationIsUnknown = !( operation.operationIsVoid || operation.operationIsString || operation.operationIsNumber || operation.operationIsBoolean || operation.operationIsEnum || operation.operationIsObject || operation.operationIsFile || operation.operationIsPrimitiveArray ); descriptor.serviceOperations.push(operation); } } // Read the comments of each tag to use for service comments if (swagger.tags && swagger.tags.length > 0) { for (i = 0; i < swagger.tags.length; i++) { const tag = swagger.tags[i]; const name = tagName(tag.name); const service = services[name]; if (service && tag.description) { service.serviceComments = toComments(tag.description); } } } // Resolve the models used by each service for (name in services) { var service = services[name]; var dependencies = new DependenciesResolver(models); var errorDependencies = new DependenciesResolver(models); for (i = 0; i < service.serviceOperations.length; i++) { var op = service.serviceOperations[i]; for (var code in op.operationResponses) { var status = Number(code); if (!isNaN(status)) { var actualDeps = (status < 200 || status >= 300) ? errorDependencies : dependencies; var response = op.operationResponses[code]; if (response && response.type) { var type = response.type; if (type && type.allTypes) { // This is an inline object. Append all types type.allTypes.forEach(t => actualDeps.add(t)); } else { actualDeps.add(type); } } } } for (j = 0; j < op.operationParameters.length; j++) { param = op.operationParameters[j]; dependencies.add(param.paramType); } } service.serviceDependencies = dependencies.get(); service.serviceErrorDependencies = errorDependencies.get(); } return services; } /** * Processes all $ref objects recursively * * @param array Array to be searched for $ref * @param swagger Full Swagger Config * @return {*} */ function resolveRefRecursive(array, swagger) { if (!array || typeof array !== 'object') { return array; } if (typeof array["$ref"] === "string") { return resolveRefRecursive(resolveRef(swagger, array["$ref"]), swagger); } for (var key in array) { var funcArgs = [array[key], swagger] array[key] = resolveRefRecursive.apply(null, funcArgs); } return array; } module.exports = ngSwaggerGen;