Merge pull request #2 from amarzavery/sway2

Sway2
This commit is contained in:
Amar Zavery 2017-01-23 10:35:08 -08:00 коммит произвёл GitHub
Родитель afdadcae54 5886d539bc
Коммит 9c59fd3073
6 изменённых файлов: 394 добавлений и 33 удалений

10
.vscode/launch.json поставляемый
Просмотреть файл

@ -11,10 +11,14 @@
"program": "${workspaceRoot}/cli.js",
"cwd": "${workspaceRoot}",
"args": [
"validate-spec",
"arm-storage/2016-01-01/swagger/storage.json",
"validate-example",
"..\\upstream\\azure-rest-api-specs\\arm-customer-insights\\2016-01-01\\swagger\\customer-insights.json",
"-o",
"Interactions_CreateOrUpdate",
"-j"
]
],
"env": {
}
},
{
"type": "node",

Просмотреть файл

@ -3,12 +3,13 @@
'use strict';
var Sway = require('sway'),
util = require('util'),
msRest = require('ms-rest'),
HttpRequest = msRest.WebResource,
var util = require('util'),
path = require('path'),
fs = require('fs'),
Sway = require('sway'),
msRest = require('ms-rest'),
JsonRefs = require('json-refs'),
HttpRequest = msRest.WebResource,
utils = require('./util/utils'),
Constants = require('./util/constants'),
log = require('./util/logging'),
@ -44,6 +45,130 @@ class SpecValidator {
}
}
findObject(what, where, actualReference, docPath) {
let self = this;
let result = eval(`where.${what}`);
if (!result) {
let msg = `Object '${what}' from the given reference ` +
`'${actualReference}' is not found in the swagger spec '${docPath}'.`;
let e = self.constructErrorObject('OBJECT_NOT_FOUND', msg);
log.error(e);
throw e;
} else {
return result;
}
}
getDefinitionFromReference(reference) {
let self = this;
if (!reference || (reference && reference.trim().length === 0)) {
throw new Error('reference cannot be null or undefined and it must be a non-empty string.');
}
let refObj = utils.parseReferenceInSwagger(reference);
if (refObj.filePath) {
let docPath = utils.joinPath(self.specDir, refObj.filePath);
return utils.parseJson(docPath).then(function(result) {
return self.findObject(refObj.localReference.accessorProperty, result, refObj.localReference.value, docPath);
});
} else {
return self.findObject(refObj.localReference.accessorProperty, self.specInJson, refObj.localReference.value, self.specPath);
}
}
mergeResolvedAllOfObjects(source, target) {
let self = this;
if (!source || (source && typeof source !== 'object')) {
return Promise.reject(new Error(`source must be of type "object".`));
}
if (!target || (target && typeof target !== 'object')) {
return Promise.reject(new Error(`target must be of type "object".`));
}
//merge the target model's properties
source.properties = utils.merge(source.properties, target.properties);
//merge the array of required properties
if (target.required) {
if (!source.required) {
source.required = [];
}
source.required = [...new Set([...source.required, ...target.required])];
}
//merge x-ms-azure-resource
if (target['x-ms-azure-resource']) {
source['x-ms-azure-resource'] = target['x-ms-azure-resource'];
}
return Promise.resolve(source);
}
getRefDefinitionInAllOf(item) {
let self = this;
if (!item || (item && typeof item !== 'object')) {
return Promise.reject(new Error(`item must be of type "object".`));
}
if (item['$ref']) {
return Promise.resolve(self.getDefinitionFromReference(item['$ref']));
} else {
return Promise.resolve(item);
}
}
composeAllOf(model, item) {
let self = this;
return self.getRefDefinitionInAllOf(item).then(function(result) {
if (result && result.allOf) {
return self.resolveAllOf(result).then(function(res) {
return Promise.resolve(self.mergeResolvedAllOfObjects(model, result));
}).catch(function (err) {
log.error(err);
return Promise.reject(err);
});
} else {
return Promise.resolve(self.mergeResolvedAllOfObjects(model, result));
}
});
}
resolveAllOf(model) {
let self = this;
if (!model || (model && typeof model !== 'object')) {
return Promise.reject(new Error(`model cannot be null or undefined and must of type object.`));
}
if (model.allOf) {
let allOfItemPromiseFactories = model.allOf.map(function(item) {
return Promise.resolve(self.composeAllOf(model, item));
});
return utils.executePromisesSequentially(allOfItemPromiseFactories);
}
return Promise.resolve(model);
}
makeModelsStricter() {
let self = this;
let spec = self.specInJson;
let definitions = spec.definitions;
let modelNames = Object.keys(self.specInJson.definitions);
let modelPromiseFactories = modelNames.map(function (modelName) {
let model = definitions[modelName];
if (model && !model.additionalProperties) {
model.additionalProperties = false;
}
return self.resolveAllOf(model);
});
return utils.executePromisesSequentially(modelPromiseFactories);
}
// resolveRelativePaths() {
// let self = this;
// let spec = self.specInJson;
// let options = {
// relativeBase: self.specDir,
// filter: ['relative', 'remote']
// };
// let allRefsRemoteRelative = JsonRefs.findRefs(spec, options);
// Object.keys(allRefsRemoteRelative).map(function(refName) {
// let refDetails = allRefsRemoteRelative[refName];
// })
// }
updateValidityStatus(value) {
if (!Boolean(value)) {
this.specValidationResult.validityStatus = false;
@ -72,6 +197,16 @@ class SpecValidator {
return utils.parseJson(self.specPath).then(function (result) {
self.specInJson = result;
self.unifyXmsPaths();
return self;
}).then(function () {
return self.makeModelsStricter();
}).then(function() {
return Object.keys(self.specInJson.definitions).map(function(definitionName){
if (self.specInJson.definitions[definitionName].allOf) {
delete self.specInJson.definitions[definitionName].allOf;
}
});
}).then(function() {
let options = {};
options.definition = self.specInJson;
options.jsonRefs = {};
@ -82,9 +217,11 @@ class SpecValidator {
self.swaggerApi = api;
return Promise.resolve(api);
}).catch(function (err) {
console.dir(err);
let e = self.constructErrorObject(ErrorCodes.ResolveSpecError, err.message, [err]);
self.specValidationResult.resolveSpec = e;
log.error(`${ErrorCodes.ResolveSpecError}: ${err.message}.`);
log.silly(err.stack);
return Promise.reject(e);
});
}
@ -361,7 +498,7 @@ class SpecValidator {
}
if (exampleParameterValues === null || exampleParameterValues === undefined || typeof exampleParameterValues !== 'object') {
throw new Error('exampleParameterValues cannot be null or undefined and must be of type \'object\' (A dictionary of key-value pairs of parameter-names and their values).');
throw new Error(`In operation "${operation.operationId}", exampleParameterValues cannot be null or undefined and must be of type "object" (A dictionary of key-value pairs of parameter-names and their values).`);
}
let parameters = operation.getParameters();
let options = {};
@ -370,7 +507,7 @@ class SpecValidator {
parameters.forEach(function(parameter) {
if (!exampleParameterValues[parameter.name]) {
if (parameter.required) {
throw new Error(`Parameter ${parameter.name} is required in the swagger spec but is not present in the provided example parameter values.`);
throw new Error(`In operation "${operation.operationId}", parameter ${parameter.name} is required in the swagger spec but is not present in the provided example parameter values.`);
}
return;
}
@ -449,20 +586,36 @@ class SpecValidator {
if (exampleResponseValue === null || exampleResponseValue === undefined || typeof exampleResponseValue !== 'object') {
throw new Error('operation cannot be null or undefined and must be of type \'object\'.');
}
let responsesInSwagger = {};
let responses = operation.getResponses().map(function(response) {
responsesInSwagger[response.statusCode] = response.statusCode;
return response.statusCode;
});
for (let exampleResponseStatusCode in exampleResponseValue) {
let response = operation.getResponse(exampleResponseStatusCode);
result[exampleResponseStatusCode] = {errors: [], warnings: []};
if (responsesInSwagger[exampleResponseStatusCode]) delete responsesInSwagger[exampleResponseStatusCode];
result[exampleResponseStatusCode] = { errors: [], warnings: [] };
//have to ensure how to map negative status codes to default. There have been several issues filed in the Autorest repo, w.r.t how
//default is handled. While solving that issue, we may come up with some extension. Once that is finalized, we should code accordingly over here.
if (!response) {
let msg = `${exampleResponseStatusCode} for operation ${operation.operationId} is provided in exampleResponseValue, however it is not present in the swagger spec.`;
let e = new Error(msg);
e.code = ErrorCodes.ResponseStatusCodeNotInSpec;
let msg = `Response statusCode "${exampleResponseStatusCode}" for operation "${operation.operationId}" is provided in exampleResponseValue, ` +
`however it is not present in the swagger spec.`;
let e = self.constructErrorObject(ErrorCodes.ResponseStatusCodeNotInSpec, msg);
result[exampleResponseStatusCode].errors.push(e);
log.error(e);
continue;
}
let exampleResponseHeaders = exampleResponseValue[exampleResponseStatusCode]['headers'] || {};
let exampleResponseBody = exampleResponseValue[exampleResponseStatusCode]['body'];
if (exampleResponseBody && !response.schema) {
let msg = `Response statusCode "${exampleResponseStatusCode}" for operation "${operation.operationId}" has response body provided in the example, ` +
`however the response does not have a "schema" defined in the swagger spec.`;
let e = self.constructErrorObject(ErrorCodes.ResponseSchemaNotInSpec, msg);
result[exampleResponseStatusCode].errors.push(e);
log.error(e);
continue;
}
//ensure content-type header is present
if (!(exampleResponseHeaders['content-type'] || exampleResponseHeaders['Content-Type'])) {
exampleResponseHeaders['content-type'] = operation.produces[0];
@ -471,6 +624,18 @@ class SpecValidator {
let validationResult = self.validateResponse(operation, exampleResponse);
result[exampleResponseStatusCode] = validationResult;
}
let responseWithoutXmsExamples = Object.keys(responsesInSwagger).filter(function (statusCode) {
if (statusCode !== 'default') {
//let intStatusCode = parseInt(statusCode);
//if (!isNaN(intStatusCode) && intStatusCode < 400) {
return statusCode;
//}
}
});
if (responseWithoutXmsExamples && responseWithoutXmsExamples.length) {
let msg = `Following response status codes "${responseWithoutXmsExamples}" for operation "${operation.operationId}" were present in the swagger spec, ` +
`however they were not present in x-ms-examples. Please provide them.`;
}
return result;
}

Просмотреть файл

@ -22,6 +22,7 @@ var Constants = {
RequestValidationError: 'REQUEST_VALIDATION_ERROR',
ResponseBodyValidationError: 'RESPONSE_BODY_VALIDATION_ERROR',
ResponseStatusCodeNotInSpec: 'RESPONSE_STATUS_CODE_NOT_IN_SPEC',
ResponseSchemaNotInSpec: 'RESPONSE_SCHEMA_NOT_IN_SPEC',
RequiredParameterNotInExampleError: 'REQUIRED_PARAMETER_NOT_IN_EXAMPLE_ERROR',
BodyParameterValidationError: 'BODY_PARAMETER_VALIDATION_ERROR',
TypeValidationError: 'TYPE_VALIDATION_ERROR',

Просмотреть файл

@ -4,8 +4,17 @@
'use strict';
var fs = require('fs'),
util = require('util'),
path = require('path'),
request = require('request');
/*
* Caches the json docs that were successfully parsed by exports.parseJson(). This avoids, fetching them again.
* key: docPath
* value: parsed doc in JSON format
*/
exports.docCache = {};
/*
* Removes byte order marker. This catches EF BB BF (the UTF-8 BOM)
* because the buffer-to-string conversion in `fs.readFile()`
@ -35,17 +44,23 @@ exports.parseJson = function parseJson(specPath) {
let err = new Error('A (github) url or a local file path to the swagger spec is required and must be of type string.');
return Promise.rject(err);
}
if (exports.docCache[specPath]) {
return Promise.resolve(exports.docCache[specPath]);
}
//url
if (specPath.match(/^http.*/ig) !== null) {
//If the spec path is a url starting with https://github then let us auto convert it to an https://raw.githubusercontent url.
if (specPath.startsWith('https://github')) {
specPath = specPath.replace(/^https:\/\/(github.com)(.*)blob\/(.*)/ig, 'https://raw.githubusercontent.com$2$3');
}
return exports.makeRequest({ url: specPath, errorOnNon200Response: true});
let res = exports.makeRequest({ url: specPath, errorOnNon200Response: true});
exports.docCache[specPath] = res;
return res;
} else {
//local filepath
try {
result = JSON.parse(exports.stripBOM(fs.readFileSync(specPath, 'utf8')));
exports.docCache[specPath] = result;
return Promise.resolve(result);
} catch (err) {
return Promise.reject(err);
@ -147,7 +162,8 @@ exports.getTimeStamp = function getTimeStamp() {
};
/*
* Executes an array of promises sequentially
* Executes an array of promises sequentially. Inspiration of this method is here:
* https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html. An awesome blog on promises!
*
* @param {Array} promiseFactories An array of promise factories(A function that return a promise)
*
@ -185,4 +201,81 @@ exports.generateRandomId = function generateRandomId(prefix, existingIds) {
return randomStr;
};
/*
* Parses a [inline|relative] [model|parameter] reference in the swagger spec.
* This method does not handle parsing paths "/subscriptions/{subscriptionId}/etc.".
*
* @param {string} reference Reference to be parsed.
*
* @return {object} result
* {string} [result.filePath] Filepath present in the reference. Examples are:
* - '../newtwork.json#/definitions/Resource' => '../network.json'
* - '../examples/nic_create.json' => '../examples/nic_create.json'
* {object} [result.localReference] Provides information about the local reference in the json document.
* {string} [result.localReference.value] The json reference value. Examples are:
* - '../newtwork.json#/definitions/Resource' => '#/definitions/Resource'
* - '#/parameters/SubscriptionId' => '#/parameters/SubscriptionId'
* {string} [result.localReference.accessorProperty] The json path expression that can be used by
* eval() to access the desired object. Examples are:
* - '../newtwork.json#/definitions/Resource' => 'definitions.Resource'
* - '#/parameters/SubscriptionId' => 'parameters,SubscriptionId'
*/
exports.parseReferenceInSwagger = function parseReferenceInSwagger(reference) {
if (!reference || (reference && reference.trim().length === 0)) {
throw new Error('reference cannot be null or undefined and it must be a non-empty string.');
}
let result = {};
if (reference.includes('#')) {
//local reference in the doc
if (reference.startsWith('#/')) {
result.localReference = {};
result.localReference.value = reference;
result.localReference.accessorProperty = reference.slice(2).replace('/', '.');
} else {
//filePath+localReference
let segments = reference.split('#');
result.filePath = segments[0];
result.localReference = {};
result.localReference.value = '#' + segments[1];
result.localReference.accessorProperty = segments[1].slice(1).replace('/', '.');
}
} else {
//we are assuming that the string is a relative filePath
result.filePath = reference;
}
return result;
};
/*
* Same as path.join(), however, it converts backward slashes to forward slashes.
* This is required because path.join() joins the paths and converts all the
* forward slashes to backward slashes if executed on a windows system. This can
* be problematic while joining a url. For example:
* path.join('https://github.com/Azure/openapi-validation-tools/blob/master/lib', '../examples/foo.json') returns
* 'https:\\github.com\\Azure\\openapi-validation-tools\\blob\\master\\examples\\foo.json' instead of
* 'https://github.com/Azure/openapi-validation-tools/blob/master/examples/foo.json'
*
* @param variable number of arguments and all the arguments must be of type string. Similar to the API
* provided by path.join() https://nodejs.org/dist/latest-v6.x/docs/api/path.html#path_path_join_paths
* @return {string} resolved path
*/
exports.joinPath = function joinPath() {
let finalPath = '';
for (let arg in arguments) {
finalPath = path.join(finalPath, arguments[arg]);
}
finalPath = finalPath.replace(/\\/gi, '/');
finalPath = finalPath.replace(/^(http|https):\/(.*)/gi, '$1://$2');
log.silly(`The final path is: ${finalPath}.`);
return finalPath;
};
exports.merge = function merge(obj, src) {
Object.keys(src).forEach(function(key) { obj[key] = src[key]; });
return obj;
}
exports = module.exports;

Просмотреть файл

@ -10,14 +10,15 @@
"description": "Validate Azure REST API Specifications",
"license": "MIT",
"dependencies": {
"azure-arm-resource": "^1.6.1-preview",
"moment": "^2.14.1",
"ms-rest": "^1.15.2",
"ms-rest-azure": "^1.15.2",
"request": "^2.79.0",
"swagger-tools": "^0.10.1",
"sway": "^1.0.0",
"winston": "^2.3.0",
"yargs": "^6.6.0",
"ms-rest": "^1.15.2",
"ms-rest-azure": "^1.15.2"
"yargs": "^6.6.0"
},
"homepage": "https://github.com/azure/openapi-validator-tools",
"repository": {

Просмотреть файл

@ -3,9 +3,14 @@
'use strict';
var log = require('./lib/util/logging'),
var msrest = require('ms-rest'),
msrestazure = require('ms-rest-azure'),
ResourceManagementClient = require('azure-arm-resource').ResourceManagementClient,
log = require('./lib/util/logging'),
utils = require('./lib/util/utils'),
Constants = require('./lib/util/constants'),
path = require('path'),
util = require('util'),
SpecValidator = require('./lib/specValidator');
exports.finalValidationResult = { validityStatus: true };
@ -42,7 +47,7 @@ exports.validateSpec = function validateSpec(specPath, json) {
let validator = new SpecValidator(specPath);
exports.finalValidationResult[specPath] = validator.specValidationResult;
validator.initialize().then(function() {
log.info(`\n> Semantically validating ${specPath}:\n`);
log.info(`Semantically validating ${specPath}:\n`);
validator.validateSpec();
exports.updateEndResultOfSingleValidation(validator);
exports.logDetailedInfo(validator, json);
@ -53,20 +58,12 @@ exports.validateSpec = function validateSpec(specPath, json) {
});
};
exports.executeSequentially = function executeSequentially(promiseFactories) {
var result = Promise.resolve();
promiseFactories.forEach(function (promiseFactory) {
result = result.then(promiseFactory);
});
return result;
};
exports.validateCompositeSpec = function validateCompositeSpec(compositeSpecPath, json){
return exports.getDocumentsFromCompositeSwagger(compositeSpecPath).then(function(docs) {
let promiseFactories = docs.map(function(doc) {
return exports.validateSpec(doc, json);
});
return exports.executeSequentially(promiseFactories);
return utils.executePromisesSequentially(promiseFactories);
}).catch(function (err) {
log.error(err);
});
@ -76,7 +73,7 @@ exports.validateExamples = function validateExamples(specPath, operationIds, jso
let validator = new SpecValidator(specPath);
exports.finalValidationResult[specPath] = validator.specValidationResult;
validator.initialize().then(function() {
log.info(`\n> Validating "examples" and "x-ms-examples" in ${specPath}:\n`);
log.info(`Validating "examples" and "x-ms-examples" in ${specPath}:\n`);
validator.validateOperations(operationIds);
exports.updateEndResultOfSingleValidation(validator);
exports.logDetailedInfo(validator, json);
@ -91,7 +88,7 @@ exports.validateExamplesInCompositeSpec = function validateExamplesInCompositeSp
let promiseFactories = docs.map(function(doc) {
return exports.validateExamples(doc, json);
});
return exports.executeSequentially(promiseFactories);
return utils.executePromisesSequentially(promiseFactories);
}).catch(function (err) {
log.error(err);
});
@ -107,7 +104,7 @@ exports.updateEndResultOfSingleValidation = function updateEndResultOfSingleVali
exports.finalValidationResult.validityStatus = validator.specValidationResult.validityStatus;
}
return;
}
};
exports.logDetailedInfo = function logDetailedInfo(validator, json) {
if (json) {
@ -123,4 +120,104 @@ exports.logDetailedInfo = function logDetailedInfo(validator, json) {
}
};
exports.sanitizeParameters = function sanitizeParameters(exampleParameters) {
if (exampleParameters) {
if (exampleParameters.subscriptionId) {
exampleParameters.subscriptionId = process.env['AZURE_SUBSCRIPTION_ID'];
}
if (exampleParameters.resourceGroupName) {
exampleParameters.resourceGroupName = process.env['AZURE_RESOURCE_GROUP'];
}
}
return exampleParameters;
};
exports.liveTest = function liveTest(specPath, operationId) {
exports.validateEnvironmentVariables();
log.transports.console.level = 'info';
let clientId = process.env['CLIENT_ID'];
let domain = process.env['DOMAIN'];
let secret = process.env['APPLICATION_SECRET'];
let subscriptionId = process.env['AZURE_SUBSCRIPTION_ID'];
let location = process.env['AZURE_LOCATION'] || 'westus';
let resourceGroupName = process.env['AZURE_RESOURCE_GROUP'] || utils.generateRandomId('testrg');
let validator = new SpecValidator(specPath);
let operation, xmsExamples;
validator.initialize().then(function() {
log.info(`Running live test using x-ms-examples in ${operationId} in ${specPath}:\n`);
operation = validator.getOperationById(operationId);
xmsExamples = operation[Constants.xmsExamples];
if (xmsExamples) {
for (let scenario in xmsExamples) {
let xmsExample = xmsExamples[scenario];
let parameters = exports.sanitizeParameters(xmsExample.parameters);
let result = validator.validateRequest(operation, xmsExample.parameters);
if (result.validationResult && result.validationResult.errors && result.validationResult.errors.length) {
let msg = `Cannot proceed ahead with the live test for operation: ${operationId}. Found validation errors in the request.\n` +
`${util.inspect(result.validationResult.errors, {depth: null})}`;
throw new Error(msg);
}
let req = result.request;
if (!req) {
throw new Error(`Cannot proceed ahead with the live test for operation: ${operationId}. The request object is undefined.`);
}
if (req.body !== null && req.body !== undefined) {
req.body = JSON.stringify(req.body);
}
msrestazure.loginWithServicePrincipalSecret(clientId, secret, domain, function(err, creds, subscriptions) {
if (err) {
throw err;
}
let resourceClient = new ResourceManagementClient(creds, subscriptionId);
let client = new msrestazure.AzureServiceClient(creds);
exports.createResourceGroup(resourceClient, location, resourceGroupName, function(err, resourceGroup) {
if (err) {
throw err;
}
client.sendRequest(req, function(err, result, request, response) {
log.info(request);
log.info(response);
if (err) {
throw err;
}
log.info(result);
});
});
});
}
}
}).catch(function (err) {
log.error(err);
});
};
exports.createResourceGroup = function createResourceGroup(resourceClient, location, resourceGroupName, callback) {
resourceClient.resourceGroups.get(resourceGroupName, function (err, result, request, response) {
if (err && err.statusCode === 404) {
log.info(`Creating resource group: \'${resourceGroupName}\', if not present.`);
let groupParameters = { location: location, tags: { 'live-test': 'live-test'} };
return resourceClient.resourceGroups.createOrUpdate(resourceGroupName, groupParameters, callback);
} else {
return callback(null, result);
}
});
};
exports.getResourceGroup = function getResourceGroup(resourceClient, resourceGroupName, callback) {
log.info(`Searching ResourceGroup: ${resourceGroupName}.`)
resourceClient.resourceGroups.get(resourceGroupName, callback);
}
exports.validateEnvironmentVariables = function validateEnvironmentVariables() {
var envs = [];
if (!process.env['CLIENT_ID']) envs.push('CLIENT_ID');
if (!process.env['DOMAIN']) envs.push('DOMAIN');
if (!process.env['APPLICATION_SECRET']) envs.push('APPLICATION_SECRET');
if (!process.env['AZURE_SUBSCRIPTION_ID']) envs.push('AZURE_SUBSCRIPTION_ID');
if (envs.length > 0) {
throw new Error(util.format('please set/export the following environment variables: %s', envs.toString()));
}
};
exports = module.exports;