swagger-converter
Version:
Converts Swagger documents from version 1.x to version 2.0
1,122 lines (976 loc) • 32.1 kB
JavaScript
/*
* @license
* The MIT License (MIT)
*
* Copyright (c) 2014 Apigee Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
'use strict';
var assert = require('assert');
var URI = require('urijs');
module.exports = {
convert: convert,
listApiDeclarations: listApiDeclarations,
};
/**
* Swagger Converter Error
* @param {string} message - error message
*/
function SwaggerConverterError(message) {
this.message = message;
this.stack = new Error(message).stack;
}
SwaggerConverterError.prototype = Object.create(Error.prototype);
SwaggerConverterError.prototype.name = 'SwaggerConverterError';
/*
* List all apiDeclarations refenced in resourceListing
* @param sourceUrl {string} - source URL for root Swagger 1.x document
* @param resourceListing {object} - root Swagger 1.x document
* @returns {object} - map of apiDeclarations paths to absolute URLs
*/
function listApiDeclarations(sourceUrl, resourceListing) {
/*
* Warning: This code is intended to cover us much as possible real-life
* cases and was tested using hundreds of public Swagger documents. If you
* change code please do not introduce any breaking changes.
* Possible workaround: you can alter algorithm by add/change 'basePath'
* before passing it into this function.
*/
sourceUrl = URI(sourceUrl || '').query('');
var baseUrl = URI(resourceListing.basePath || '');
if (baseUrl.is('relative')) {
baseUrl = baseUrl.absoluteTo(sourceUrl);
}
if (resourceListing.swaggerVersion === '1.0' && baseUrl.suffix() === 'json') {
baseUrl.filename('');
}
var result = {};
resourceListing.apis.forEach(function (api) {
// skip embedded documents
if (!isValue(api.path) || isValue(api.operations)) {
return;
}
var resourceUrl = URI(api.path.replace('{format}', 'json'));
if (resourceUrl.is('relative')) {
resourceUrl = URI(baseUrl.href() + resourceUrl.href());
}
result[api.path] = resourceUrl.normalize().href();
});
return result;
}
/*
* Converts Swagger 1.x specs file to Swagger 2.0 specs.
* @param resourceListing {object} - root Swagger 1.x document where it has a
* list of all paths
* @param apiDeclarations {object} - a map with paths as keys and resources as values
* @param options {object} - additonal options
* @returns {object} - Fully converted Swagger 2.0 document
*/
function convert(resourceListing, apiDeclarations, options) {
if (Array.isArray(apiDeclarations)) {
throw new SwaggerConverterError(
'Second argument(apiDeclarations) should be plain object, ' +
'see release notes.',
);
}
var converter = new Converter();
converter.options = options || {};
return converter.convert(resourceListing, apiDeclarations);
}
var Converter = function () {};
var prototype = Converter.prototype;
/*
* Converts Swagger 1.x specs file to Swagger 2.0 specs.
* @param resourceListing {object} - root of Swagger 1.x document
* @param apiDeclarations {object} - a map with paths as keys and resources as values
* @returns {object} - Fully converted Swagger 2.0 document
*/
prototype.convert = function (resourceListing, apiDeclarations) {
assert(typeof resourceListing === 'object');
assert(typeof apiDeclarations === 'object');
var resources = this.getResources(resourceListing, apiDeclarations);
var tags = this.buildTags(resourceListing, resources);
var securityDefinitions = this.buildSecurityDefinitions(
resourceListing.authorizations,
);
var paths = {};
var definitions = {};
this.customTypes = [];
this.forEach(resources, function (resource) {
if (isValue(resource.models)) {
//TODO: check that types don't overridden
this.customTypes = this.customTypes.concat(Object.keys(resource.models));
}
});
this.forEach(resources, function (resource, index) {
var operationTags;
var tag = tags[index];
if (isValue(tag)) {
operationTags = [tag.name];
}
extend(definitions, this.buildDefinitions(resource.models));
extend(paths, this.buildPaths(resource, operationTags));
});
return extend(
{},
this.aggregatePathComponents(resourceListing, apiDeclarations),
{
swagger: '2.0',
info: this.buildInfo(resourceListing),
//Order of tags depend on order of 'resourceListing.apis', so sort it
tags: undefinedIfEmpty(sortBy(tags, 'name')),
paths: undefinedIfEmpty(paths),
securityDefinitions: undefinedIfEmpty(securityDefinitions),
definitions: undefinedIfEmpty(definitions),
},
);
};
/*
* Get list of resources.
* @param resourceListing {object} - root of Swagger 1.x document
* @param apiDeclarations {object} - a map with paths as keys and resources as values
* @returns {array} - list of resources
*/
Converter.prototype.getResources = function (resourceListing, apiDeclarations) {
var resources = [];
var embedded = false;
this.forEach(resourceListing.apis, function (resource) {
var path = resource.path;
if (!isValue(path) || !isEmpty(resource.operations)) {
embedded = true;
return;
}
if (embedded) {
throw new SwaggerConverterError(
'Resource listing can not have both operations and API declarations.',
);
}
resource = apiDeclarations[path];
if (!isValue(resource)) {
throw new SwaggerConverterError(
'resourceListing addressing missing declaration on path: ' + path,
);
}
resources.push(resource);
});
if (embedded) {
return [resourceListing];
}
return resources;
};
/*
* Builds "tags" section of Swagger 2.0 document
* @param resourceListing {object} - root of Swagger 1.x document
* @param resources {object} - list of resources
* @returns {array} - list of Swagger 2.0 tags
*/
Converter.prototype.buildTags = function (resourceListing, resources) {
var resourcePaths = [];
if (this.options.buildTagsFromPaths !== true) {
resourcePaths = this.mapEach(resources, function (resource) {
return resource.resourcePath;
});
//'resourcePath' is optional parameter and also frequently have invalid values
// if so than we discard all values and use resource paths for listing instead.
if (getLength(removeDuplicates(resourcePaths)) < getLength(resources)) {
resourcePaths = [];
}
}
if (isEmpty(resourcePaths)) {
resourcePaths = this.mapEach(resourceListing.apis, function (resource) {
return resource.path;
});
}
resourcePaths = stripCommonPath(resourcePaths);
var tags = [];
this.forEach(resourceListing.apis, function (resource, index) {
if (!isEmpty(resource.operations)) {
return;
}
var tagName = URI(resourcePaths[index] || '')
.path(true)
.replace('{format}', 'json')
.replace(/\/$/, '')
.replace(/.json$/, '');
if (!isValue(tagName)) {
return;
}
tags.push(
extend(
{},
{
name: tagName,
description: resource.description,
},
),
);
});
return tags;
};
/*
* Builds "info" section of Swagger 2.0 document
* @param resourceListing {object} - root of Swagger 1.x document
* @returns {object} - "info" section of Swagger 2.0 document
*/
prototype.buildInfo = function (resourceListing) {
var info = {
title: 'Title was not specified',
version: resourceListing.apiVersion || '1.0.0',
};
var oldInfo = resourceListing.info;
if (!isValue(oldInfo)) {
return info;
}
var contact = extend({}, { email: oldInfo.contact });
var license;
if (isValue(oldInfo.license)) {
license = extend(
{},
{
name: oldInfo.license,
url: oldInfo.licenseUrl,
},
);
}
return extend(info, {
title: oldInfo.title,
description: oldInfo.description,
contact: undefinedIfEmpty(contact),
license: undefinedIfEmpty(license),
termsOfService: oldInfo.termsOfServiceUrl,
});
};
/*
* Merge path components from all resources.
* @param resourceListing {object} - root of Swagger 1.x document
* @param apiDeclarations {array} - a list of resources
* @returns {object} - Swagger 2.0 path components
* @throws {SwaggerConverterError}
*/
prototype.aggregatePathComponents = function (
resourceListing,
apiDeclarations,
) {
var path = extend({}, this.buildPathComponents(resourceListing.basePath));
var globalBasePath;
this.forEach(apiDeclarations, function (api) {
var basePath = api.basePath;
//Test if basePath is relative(start with '.' or '..').
if (/^\.\.?(\/|$)/.test(basePath)) {
basePath = URI(basePath).absoluteTo(path.basePath).path(true);
}
//TODO: Swagger 1.x support per resource 'basePath', but Swagger 2.0 doesn't
// solution could be to create separate spec per each 'basePath'.
if (isValue(globalBasePath) && basePath !== globalBasePath) {
throw new SwaggerConverterError(
'Resources can not override each other basePaths',
);
}
globalBasePath = basePath;
});
return extend(path, this.buildPathComponents(globalBasePath));
};
/*
* Get host, basePath and schemes for Swagger 2.0 result document from
* Swagger 1.x basePath.
* @param basePath {string} - the base path from Swagger 1.x
* @returns {object} - Swagger 2.0 path components
*/
prototype.buildPathComponents = function (basePath) {
if (!basePath) {
return {};
}
var url = URI(basePath).absoluteTo('/');
var protocol = url.protocol();
return extend(
{},
{
host: url.host(),
basePath: url.path(true),
schemes: protocol && [protocol],
},
);
};
/*
* Builds a Swagger 2.0 type properties from a Swagger 1.x type properties
*
* @param oldDataType {object} - Swagger 1.x type object
*
* @returns {object} - Swagger 2.0 equivalent
* @throws {SwaggerConverterError}
*/
prototype.buildTypeProperties = function (oldType, allowRef) {
if (!oldType) {
return {};
}
assert(typeof allowRef === 'boolean');
oldType = oldType.trim();
if (allowRef && this.customTypes.indexOf(oldType) !== -1) {
return { $ref: '#/definitions/' + oldType };
}
var typeMap = {
//Swagger 1.x types
integer: { type: 'integer' },
number: { type: 'number' },
string: { type: 'string' },
boolean: { type: 'boolean' },
array: { type: 'array' },
object: { type: 'object' },
file: { type: 'file' },
void: {},
//Swagger 1.1 types
int: { type: 'integer', format: 'int32' },
long: { type: 'integer', format: 'int64' },
float: { type: 'number', format: 'float' },
double: { type: 'number', format: 'double' },
byte: { type: 'string', format: 'byte' },
date: { type: 'string', format: 'date' },
list: { type: 'array' },
set: { type: 'array', uniqueItems: true },
//JSON Schema Draft-3
any: {},
//Unofficial but very common mistakes
datetime: { type: 'string', format: 'date-time' },
'date-time': { type: 'string', format: 'date-time' },
map: { type: 'object' },
};
var type = typeMap[oldType.toLowerCase()];
if (isValue(type)) {
return type;
}
//handle "<TYPE>[<ITEMS>]" types from 1.1 spec
//use RegEx with capture groups to get <TYPE> and <ITEMS> values.
var match = oldType.match(/^([^[]*)\[(.*)\]$/);
if (isValue(match)) {
var collection = match[1].toLowerCase();
var items = match[2];
if (collection === 'map') {
//handle "Map[String,<VALUES>]" types
//see https://github.com/swagger-api/swagger-core/issues/244
var commaIndex = items.indexOf(',');
var keyType = items.slice(0, commaIndex);
var valueType = items.slice(commaIndex + 1);
if (keyType.toLowerCase() === 'string') {
return {
additionalProperties: this.buildTypeProperties(valueType, allowRef),
};
}
} else if (collection === '') {
//handle "[<ITEMS>]" types
//see https://github.com/apigee-127/swagger-converter/pull/83
return {
type: 'array',
items: this.buildTypeProperties(items, allowRef),
};
} else {
type = typeMap[collection];
if (isValue(type)) {
type.items = this.buildTypeProperties(items, allowRef);
return type;
}
}
}
//At this point we know that it not standard type, but at the same time we
//can't find such user type. To proceed further we just add it as is.
//TODO: add warning
return allowRef ? { $ref: '#/definitions/' + oldType } : { type: oldType };
};
/*
* Builds a Swagger 2.0 data type properties from a Swagger 1.x data type properties
*
* @see {@link https://github.com/swagger-api/swagger-spec/blob/master/versions/
* 1.x.md#433-data-type-fields}
*
* @param oldDataType {object} - Swagger 1.x data type object
*
* @returns {object} - Swagger 2.0 equivalent
*/
prototype.buildDataType = function (oldDataType, allowRef) {
if (!oldDataType) {
return {};
}
assert(typeof oldDataType === 'object');
assert(typeof allowRef === 'boolean');
var oldTypeName =
oldDataType.type ||
oldDataType.dataType ||
oldDataType.responseClass ||
oldDataType.$ref;
var result = this.buildTypeProperties(oldTypeName, allowRef);
var oldItems = oldDataType.items;
if (isValue(oldItems)) {
if (typeof oldItems === 'string') {
oldItems = { type: oldItems };
}
oldItems = this.buildDataType(oldItems, allowRef);
}
//TODO: handle '0' in default
var defaultValue = oldDataType.default || oldDataType.defaultValue;
if (result.type !== 'string') {
defaultValue = fixNonStringValue(defaultValue, true);
}
//TODO: support 'allowableValues' from 1.1 spec
extend(result, {
format: oldDataType.format,
items: oldItems,
uniqueItems: fixNonStringValue(oldDataType.uniqueItems),
minimum: fixNonStringValue(oldDataType.minimum),
maximum: fixNonStringValue(oldDataType.maximum),
maxItems: fixNonStringValue(oldDataType.maxItems),
minItems: fixNonStringValue(oldDataType.minItems),
default: defaultValue,
enum: oldDataType.enum,
});
if (result.type === 'array' && !isValue(result.items)) {
result.items = {};
}
return result;
};
/*
* Builds a Swagger 2.0 paths object form a Swagger 1.x path object
* @param apiDeclaration {object} - Swagger 1.x apiDeclaration
* @param tag {array} - array of Swagger 2.0 tag names
* @returns {object} - Swagger 2.0 path object
*/
prototype.buildPaths = function (apiDeclaration, tags) {
var paths = {};
var operationDefaults = {
produces: apiDeclaration.produces,
consumes: apiDeclaration.consumes,
tags: tags,
security: undefinedIfEmpty(
this.buildSecurity(apiDeclaration.authorizations),
),
};
this.forEach(apiDeclaration.apis, function (api) {
if (!isValue(api.operations)) {
return;
}
var pathString = URI(api.path).absoluteTo('/').path(true);
pathString = pathString.replace('{format}', 'json');
if (!isValue(paths[pathString])) {
paths[pathString] = {};
}
var path = paths[pathString];
this.forEach(api.operations, function (oldOperation) {
var method = oldOperation.method || oldOperation.httpMethod;
method = method.toLowerCase();
path[method] = this.buildOperation(oldOperation, operationDefaults);
});
});
return paths;
};
/*
* Builds a Swagger 2.0 security object form a Swagger 1.x authorizations object
* @param oldAuthorizations {object} - Swagger 1.x authorizations object
* @returns {object} - Swagger 2.0 security object
*/
prototype.buildSecurity = function (oldAuthorizations) {
var security = [];
this.mapEach(oldAuthorizations, function (oldScopes, oldName) {
var names = this.securityNamesMap[oldName];
if (isEmpty(names)) {
//TODO: add warning
names = [oldName];
}
this.forEach(names, function (name) {
var requirement = {};
requirement[name] = this.mapEach(oldScopes, function (oldScope) {
return oldScope.scope;
});
security.push(requirement);
});
});
return security;
};
/*
* Builds a Swagger 2.0 operation object form a Swagger 1.x operation object
* @param oldOperation {object} - Swagger 1.x operation object
* @param operationDefaults {object} - defaults from containing apiDeclaration
* @returns {object} - Swagger 2.0 operation object
*/
prototype.buildOperation = function (oldOperation, operationDefaults) {
var parameters = [];
this.forEach(oldOperation.parameters, function (oldParameter) {
parameters.push(this.buildParameter(oldParameter));
});
/*
* Merges the tags from the resourceListing and the ones specified in the apiDeclaration.
*/
var tags = (operationDefaults.tags || []).concat(oldOperation.tags || []);
tags = removeDuplicates(tags);
var customProperties = getCustomProperties(oldOperation);
return extend({}, operationDefaults, customProperties, {
operationId: oldOperation.nickname,
summary: oldOperation.summary,
description: oldOperation.description || oldOperation.notes,
tags: undefinedIfEmpty(tags),
deprecated: fixNonStringValue(oldOperation.deprecated),
produces: oldOperation.produces,
consumes: oldOperation.consumes,
parameters: undefinedIfEmpty(parameters),
responses: this.buildResponses(oldOperation),
security: undefinedIfEmpty(this.buildSecurity(oldOperation.authorizations)),
});
};
/*
* Builds a Swagger 2.0 responses object form a Swagger 1.x responseMessages object
* @param oldOperation {object} - Swagger 1.x operation object
* @returns {object} - Swagger 2.0 response object
*/
prototype.buildResponses = function (oldOperation) {
var responses = {};
this.forEach(oldOperation.responseMessages, function (oldResponse) {
var code = '' + oldResponse.code;
responses[code] = extend(
{},
{
description: oldResponse.message || 'Description was not specified',
schema: undefinedIfEmpty(
this.buildTypeProperties(oldResponse.responseModel, true),
),
},
);
});
if (!Object.keys(responses).some((key) => /^2\d\d$/.test(key))) {
responses['200'] = {
description: 'No response was specified',
};
extend(responses['200'], {
schema: undefinedIfEmpty(this.buildDataType(oldOperation, true)),
});
}
return responses;
};
/*
* Converts Swagger 1.x parameter object to Swagger 2.0 parameter object
* @param oldParameter {object} - Swagger 1.x parameter object
* @returns {object} - Swagger 2.0 parameter object
* @throws {SwaggerConverterError}
*/
prototype.buildParameter = function (oldParameter) {
var parameter = extend(
{},
{
in: oldParameter.paramType,
description: oldParameter.description,
name: oldParameter.name,
required: fixNonStringValue(oldParameter.required),
},
);
this.forEach(oldParameter, function (oldProperty, name) {
if (name.match(/^X-/i) !== null) {
parameter[name] = oldProperty;
}
});
if (parameter.in === 'form') {
parameter.in = 'formData';
}
if (oldParameter.paramType === 'body') {
parameter.schema = this.buildDataType(oldParameter, true);
if (!isValue(parameter.name)) {
parameter.name = 'body';
}
return parameter;
}
var schema = this.buildDataType(oldParameter, false);
//Encoding of non-body arguments is the same not matter which type is specified.
//So type only affects parameter validation, so it "safe" to add missing types.
if (!isValue(schema.type)) {
schema.type = 'string';
}
if (schema.type === 'array' && !isValue(schema.items.type)) {
schema.items.type = 'string';
}
var allowMultiple = fixNonStringValue(oldParameter.allowMultiple);
//Non-body parameters doesn't support array inside array. But in some specs
//both 'allowMultiple' is true and 'type' is array, so just ignore it.
if (allowMultiple === true && schema.type !== 'array') {
schema = { type: 'array', items: schema };
}
//According to Swagger 2.0 spec: If the parameter is in "path",
//this property is required and its value MUST be true.
if (parameter.in === 'path') {
schema.required = true;
}
var collectionFormat = this.options.collectionFormat;
if (isValue(collectionFormat) && schema.type === 'array') {
//'multi' is valid only for parameters in "query" or "formData".
if (
collectionFormat !== 'multi' ||
['path', 'formData'].indexOf(parameter.in) === -1
) {
schema.collectionFormat = this.options.collectionFormat;
}
}
return extend(parameter, schema);
};
/*
* Converts Swagger 1.x authorization definitions into Swagger 2.0 definitions
* Definitions couldn't be converted 1 to 1, 'this.securityNamesMap' should be
* used to map between Swagger 1.x names and one or more Swagger 2.0 names.
*
* @param oldAuthorizations {object} - The Swagger 1.x Authorizations definitions
* @returns {object} - Swagger 2.0 security definitions
*/
prototype.buildSecurityDefinitions = function (oldAuthorizations) {
var securityDefinitions = {};
this.securityNamesMap = {};
this.forEach(oldAuthorizations, function (oldAuthorization, name) {
var scopes = {};
this.forEach(oldAuthorization.scopes, function (oldScope) {
var name = oldScope.scope;
scopes[name] = oldScope.description || 'Undescribed ' + name;
});
var securityDefinition = extend(
{},
{
type: oldAuthorization.type,
in: oldAuthorization.passAs,
name: oldAuthorization.keyname,
scopes: undefinedIfEmpty(scopes),
},
);
if (securityDefinition.type === 'basicAuth') {
securityDefinition.type = 'basic';
}
if (!isValue(oldAuthorization.grantTypes)) {
securityDefinitions[name] = securityDefinition;
this.securityNamesMap[name] = [name];
return;
}
this.securityNamesMap[name] = [];
// For OAuth2 types, 1.x describes multiple "flows" in one authorization
// object. But for 2.0 we need to create one security definition per flow.
this.forEach(oldAuthorization.grantTypes, function (oldGrantType, gtName) {
var grantParameters = {};
switch (gtName) {
case 'implicit':
extend(grantParameters, {
flow: 'implicit',
authorizationUrl: getValue(oldGrantType, 'loginEndpoint', 'url'),
});
break;
case 'authorization_code':
extend(grantParameters, {
flow: 'accessCode',
tokenUrl: getValue(oldGrantType, 'tokenEndpoint', 'url'),
authorizationUrl: getValue(
oldGrantType,
'tokenRequestEndpoint',
'url',
),
});
break;
}
var oName = name;
if (getLength(oldAuthorization.grantTypes) > 1) {
oName += '_' + grantParameters.flow;
}
this.securityNamesMap[name].push(oName);
securityDefinitions[oName] = extend(
{},
securityDefinition,
grantParameters,
);
});
});
return securityDefinitions;
};
/*
* Converts a Swagger 1.x model object to a Swagger 2.0 model object
* @param model {object} - Swagger 1.x model object
* @returns {object} - Swagger 2.0 model object
*/
prototype.buildModel = function (oldModel) {
var required = [];
var properties = {};
var items;
this.forEach(oldModel.properties, function (oldProperty, propertyName) {
if (fixNonStringValue(oldProperty.required) === true) {
required.push(propertyName);
}
properties[propertyName] = this.buildModel(oldProperty);
});
if (Array.isArray(oldModel.required)) {
required = oldModel.required;
}
if (isValue(oldModel.items)) {
items = this.buildModel(oldModel.items);
}
var customProperties = getCustomProperties(oldModel);
return extend(this.buildDataType(oldModel, true), customProperties, {
description: oldModel.description,
required: undefinedIfEmpty(required),
properties: undefinedIfEmpty(properties),
discriminator: oldModel.discriminator,
example: oldModel.example,
items: undefinedIfEmpty(items),
});
};
/*
* Converts the "models" object of Swagger 1.x specs to Swagger 2.0 definitions
* object
* @param oldModels {object} - an object containing Swagger 1.x objects
* @returns {object} - Swagger 2.0 definitions object
* @throws {SwaggerConverterError}
*/
prototype.buildDefinitions = function (oldModels) {
var models = {};
this.forEach(oldModels, function (oldModel, modelId) {
models[modelId] = this.buildModel(oldModel);
});
this.forEach(oldModels, function (parent, parentId) {
this.forEach(parent.subTypes, function (childId) {
var child = models[childId];
if (!isValue(child)) {
throw new SwaggerConverterError(
'subTypes resolution: Missing "' + childId + '" type',
);
}
if (!isValue(child.allOf)) {
models[childId] = child = { allOf: [child] };
}
child.allOf.push({ $ref: '#/definitions/' + parentId });
});
});
return models;
};
/*
* Map elements of collection into array by invoking iteratee for each element
* @param collection {array|object} - the collection to iterate over
* @parma iteratee {function} - the function invoked per iteration
* @returns {array|undefined} - result
*/
prototype.mapEach = function (collection, iteratee) {
var result = [];
this.forEach(collection, function (value, key) {
result.push(iteratee.bind(this)(value, key));
});
return result;
};
/*
* Iterates over elements of collection invoking iteratee for each element
* @param collection {array|object} - the collection to iterate over
* @parma iteratee {function} - the function invoked per iteration
*/
prototype.forEach = function (collection, iteratee) {
if (!isValue(collection)) {
return;
}
if (typeof collection !== 'object') {
throw new SwaggerConverterError(
'Expected array or object, instead got: ' +
JSON.stringify(collection, null, 2),
);
}
iteratee = iteratee.bind(this);
if (Array.isArray(collection)) {
collection.forEach(iteratee);
} else {
//In some cases order of iteration influence order of arrays in output.
//To have stable result of convertion, keys need to be sorted.
Object.keys(collection)
.sort()
.forEach(function (key) {
iteratee(collection[key], key);
});
}
};
/*
* Extends an object with another
* @param destination {object} - object that will get extended
* @parma source {object} - object the will used to extend source
*/
function extend(destination) {
assert(typeof destination === 'object');
function assign(source) {
if (!source) {
return;
}
Object.keys(source).forEach(function (key) {
var value = source[key];
if (isValue(value)) {
destination[key] = value;
}
});
}
for (var i = 1; i < arguments.length; ++i) {
assign(arguments[i]);
}
return destination;
}
/*
* Test if value is empty and if so return undefined
* @param value {*} - value to test
* @returns {array|object|undefined} - result
*/
function undefinedIfEmpty(value) {
return isEmpty(value) ? undefined : value;
}
/*
* Test if value isn't null or undefined
* @param value {*} - value to test
* @returns {boolean} - result of test
*/
function isValue(value) {
//Some implementations use empty strings as undefined.
//For all fields we can drop empty string without any problems.
//One notable exception is 'default' values, but it better to
//skip it instead of providing unintended value.
if (value === '') {
return false;
}
return value !== undefined && value !== null;
}
/*
* Get length of container(Array or Object).
* @param value {*} - container
* @returns {number} - length of container
*/
function getLength(value) {
if (typeof value !== 'object') {
return 0;
}
if (isValue(value.length)) {
return value.length;
}
return Object.keys(value).length;
}
/*
* Test if value is empty
* @param value {*} - value to test
* @returns {boolean} - result of test
*/
function isEmpty(value) {
return getLength(value) === 0;
}
/*
* Get property value of object
* @param object {*} - object
* @returns {*} - property value
*/
function getValue(object) {
for (var i = 1; i < arguments.length && isValue(object); ++i) {
var propertyName = arguments[i];
assert(typeof propertyName === 'string');
object = object[propertyName];
}
return object;
}
/*
* Convert string values into the proper type.
* @param value {*} - value to convert
* @param skipError {boolean} - skip error during conversion
* @returns {*} - transformed model object
* @throws {SwaggerConverterError}
*/
function fixNonStringValue(value, skipError) {
if (typeof value !== 'string') {
return value;
}
if (value === '') {
return undefined;
}
var lcValue = value.toLowerCase();
if (lcValue === 'true') {
return true;
}
if (lcValue === 'false') {
return false;
}
try {
return JSON.parse(value);
} catch (e) {
//TODO: report warning
if (skipError === true) {
return undefined;
}
throw new SwaggerConverterError('incorect property value: ' + e.message);
}
}
/*
* Remove duplicates of an array
* @param collection {array}
* @returns {array} - collection without duplicates
*/
function removeDuplicates(collection) {
return collection.filter(function (e, i, arr) {
return isValue(e) && arr.lastIndexOf(e) === i;
});
}
/*
* Strip common prefix from paths
* @param paths {array}
* @returns {array} - path with remove common part
*/
function stripCommonPath(paths) {
const prefixLength = getCommonPathLength(paths);
return paths.map(function (str) {
return str.slice(prefixLength);
});
}
function getCommonPathLength(paths) {
if (paths.length === 0) {
return 0;
}
var prefixLength = 0;
var first = paths[0];
for (var i = 0; i < first.length; ++i) {
const expectedChar = first.charAt(i);
for (var j = 1; j < paths.length; ++j) {
if (paths[j].charAt(i) !== expectedChar) {
return prefixLength;
}
}
if (expectedChar === '/') {
prefixLength = i + 1;
}
}
return prefixLength;
}
/*
* Sort array by value of specified attribute
* @param array {array} - array to sort
* @param attr {attr} - attribute to sort by
* @returns {array} - sorted array
*/
function sortBy(array, attr) {
assert(Array.isArray(array));
return array.sort(function (a, b) {
a = a[attr];
b = b[attr];
return a < b ? -1 : a > b ? 1 : 0;
});
}
/*
* Filter object on keys matching the prefix `x-`.
* @param object {object} - object to fetch properties from
* @returns {object} - object with only properties prefixed with `x-`
*/
function getCustomProperties(object) {
var result = {};
for (var key in object) {
if (/^x-/.test(key)) {
result[key] = object[key];
}
}
return result;
}