Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions bin/api-blueprint-validator
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#!/usr/bin/env node
var yargs = require('yargs')
.usage('Usage: $0 apiary.apib ')
.boolean(['validate-requests', 'validate-responses'])
.boolean(['validate-requests', 'validate-responses', 'fail-on-warnings', 'require-name'])
.default('validate-requests', true)
.default('validate-responses', true),
.default('validate-responses', true)
.default('fail-on-warnings', false)
.default('require-name', false),
argv = yargs.argv;

if (argv.help) {
Expand All @@ -18,4 +20,4 @@ if (argv._[0] === undefined) {
}

var validator = require('../src/validator');
validator(argv._[0], argv.validateRequests, argv.validateResponses);
validator(argv._[0], argv.validateRequests, argv.validateResponses, argv.failOnWarnings, argv.requireName);
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
"node": ">=0.11.x"
},
"dependencies": {
"protagonist": ">=0.19.3",
"glob-fs": "^0.1.6",
"is-glob": "^2.0.1",
"jsonlint": "1.6.0",
"yargs": "1.2.1",
"jsonlint": "1.6.0"
"ajv":"^4.9.0",
"protagonist": ">=1.0.0"
},
"license": "MIT"
}
210 changes: 159 additions & 51 deletions src/validator.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,120 @@
var fs = require('fs'),
glob = require('glob-fs')({ gitignore: true }),
isGlob = require('is-glob'),
protagonist = require('protagonist'),
jsonParser = require('jsonlint').parser;
ajv = require('ajv');

function lineNumberFromCharacterIndex(string, index) {
return string.substring(0, index).split("\n").length;
}

function hasImportantWarning(result,parserOptions) {
has_important_warnings = false;
if(result.wanrings) {
result.warnings.forEach(function(warning) {
if ((warning.indexOf('expected API name') !== -1) && ! parserOptions.requireBlueprintName) return;
has_important_warnings = true;
});
}
return has_important_warnings;
}

function lint(file, data, options) {

function shouldSkip(event) {
return (!options.requireBlueprintName && event.message.indexOf('expected API name') !== -1 );
}

var parserOptions = {
requireBlueprintName: options.requireBlueprintName,
type: 'ast'
};

protagonist.parse(data, parserOptions, function (error, result) {

if (error) {
var lineNumber = lineNumberFromCharacterIndex(data, error.location[0].index);
console.error('(' + file + ')' + ' ' + 'Error: ' + error.message + ' on line ' + lineNumber);
process.exit(1);
}

if(result.wanrings) {
result.warnings.forEach(function (warning) {
if (!shouldSkip(warning)) {
var lineNumber = lineNumberFromCharacterIndex(data, warning.location[0].index);
console.error('(' + file + ')' + ' ' + 'Warning: ' + warning.message + ' on line ' + lineNumber);
}
});
}

var errors = [];

if(result.ast) {
examples(result.ast, function (example, action, resource, resourceGroup) {
if (options.validateRequests) {
example.requests.forEach(function (request) {
var valid = isValidRequestOrResponse(request);
if (valid !== true) {
var message = ' ' + valid.message.replace(/\n/g, '\n ');
var position = errorPosition(example, action, resource, resourceGroup);
errors.push('(' + file + ')' + ' ' + 'Error in JSON request ' + position + '\n' + message);
}
});
}

if (options.validateResponses) {
example.responses.forEach(function (response) {
var valid = isValidRequestOrResponse(response);
if (valid !== true) {
var message = ' ' + valid.message.replace(/\n/g, '\n ');
var position = errorPosition(example, action, resource, resourceGroup);
errors.push('(' + file + ')' + ' ' + 'Error in JSON response ' + position + '\n' + message);
}
});
}
});
}
else console.log("Warning: No result.ast.");

if (errors.length > 0) {
console.error(errors.join('\n\n'));
}

if (errors.length > 0 ||
( options.failOnWarnings &&
( result.warnings.length > 0 && hasImportantWarning(result.warnings,parserOptions) )
)
) {
process.exit(1);
}
});
}

function processFile(path, options) {
fs.readFile(path, 'utf8', function (error, data) {
if (error) {
console.error('Could not open ' + path);
process.exit(1);
}
else {
lint(path, data, options);
}
});
}

function processGlob(path, options) {
glob.readdir(path, function (error, files) {
if (error) {
console.error('Unable to read files ' + path);
process.exit(1);
}
files.forEach(function (path) {
processFile(path, options);
});
});
}

function examples(ast, callback) {
ast.resourceGroups.forEach(function (resourceGroup) {
resourceGroup.resources.forEach(function (resource) {
Expand All @@ -20,18 +129,50 @@ function examples(ast, callback) {

function isJsonContentType(headers) {
return headers.some(function (header) {
return header.name === 'Content-Type' && header.value === 'application/json';
});
return header.name === 'Content-Type' && /application\/json/.test(header.value); // header may also contain other info, i.e. encoding:
}); //"application/json; charset=utf-8", so use regexp here.
}

function isValidRequestOrResponse(requestOrResponse) {
if (isJsonContentType(requestOrResponse.headers)) {
// check body
try {
var body = requestOrResponse.body;
jsonParser.parse(body);
} catch (e) {
return e;
}
// schema, if exists, should be a valid json
try {
var schema = requestOrResponse.schema;
if ( schema.length > 0 ) {
jsonParser.parse(schema);
}
} catch (e) {
return e;
}
// also check schema definitions with ajv
try {
if ( schema.length > 0 ) {
schema = JSON.parse(requestOrResponse.schema);
if ( Object.keys(schema).length > 0 ) {
var jsconSchemaParser = ajv({verbose:true, allErrors:true, format:'full',v5:true,unicode:true});
jsconSchemaParser.validateSchema(schema);
if (jsconSchemaParser.errors !== null ) {
exceptionMsg = "Error validating schema:\n";
for (var key in jsconSchemaParser.errors) {
exceptionMsg = exceptionMsg + '\tValue rasing validation error:\t\t\t"' + jsconSchemaParser.errors[key].data + '".\n';
exceptionMsg = exceptionMsg + '\tPath within schema:\t\t\t\t"' + jsconSchemaParser.errors[key].dataPath + '".\n';
exceptionMsg = exceptionMsg + "\tProbably " + jsconSchemaParser.errors[key].message + ': "' +
jsconSchemaParser.errors[key].schema + '"' + '.\n\n';
}
throw new Error(exceptionMsg);
}
} else console.log("schema.length < 0");
}
} catch (e) {
return e;
}
}

return true;
Expand All @@ -57,55 +198,22 @@ function errorPosition(example, action, resource, resourceGroup) {
return 'in ' + output.join(', ');
}

module.exports = function (fileName, validateRequests, validateResponses) {
fs.readFile(fileName, 'utf8', function (error, data) {
if (error) {
console.error('Could not open ' + fileName);
return;
}

protagonist.parse(data, function (error, result) {
if (error) {
var lineNumber = lineNumberFromCharacterIndex(data, error.location[0].index);
console.error('Error: ' + error.message + ' on line ' + lineNumber);
process.exit(1);
}

result.warnings.forEach(function (warning) {
var lineNumber = lineNumberFromCharacterIndex(data, warning.location[0].index);
console.error('Warning: ' + warning.message + ' on line ' + lineNumber);
});

var errors = [];
module.exports = function (fileName, validateRequests, validateResponses, failOnWarnings, requireBlueprintName ) {

var options = {
validateRequests: validateRequests,
validateResponses: validateResponses,
failOnWarnings: failOnWarnings,
requireBlueprintName: requireBlueprintName
};

examples(result.ast, function (example, action, resource, resourceGroup) {
if (validateRequests) {
example.requests.forEach(function (request) {
var valid = isValidRequestOrResponse(request);
if (valid !== true) {
var message = ' ' + valid.message.replace(/\n/g, '\n ');
var position = errorPosition(example, action, resource, resourceGroup);
errors.push('Error in JSON request ' + position + '\n' + message);
}
});
}

if (validateResponses) {
example.responses.forEach(function (response) {
var valid = isValidRequestOrResponse(response);
if (valid !== true) {
var message = ' ' + valid.message.replace(/\n/g, '\n ');
var position = errorPosition(example, action, resource, resourceGroup);
errors.push('Error in JSON response ' + position + '\n' + message);
}
});
}
});
if (isGlob(fileName)) {
// never require the blueprint name for multiple files
options.requireBlueprintName = false;
processGlob(fileName, options);
}
else {
processFile(fileName, options);
}

if (errors.length > 0) {
console.error(errors.join('\n\n'));
process.exit(1);
}
});
});
};