Skip to content

Commit

Permalink
feat: initial Spectral integration (#195)
Browse files Browse the repository at this point in the history
* feat: initial Spectral integration

* PR feedback, test enhancements, & including spectral in api invocation

* use default yml ruleset file & support yml custom rulesets

* More PR feedback. Improve unit test performance
  • Loading branch information
jorge-ibm committed Sep 23, 2020
1 parent d03dded commit ccbc7a6
Show file tree
Hide file tree
Showing 24 changed files with 4,130 additions and 111 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ dist
bin/
.nyc_output/
coverage/
.spectral.json
.spectral.yaml
.spectral.yml
1,012 changes: 910 additions & 102 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"pkg": "./node_modules/.bin/pkg --out-path=./bin ./package.json; cd bin; rename -f 's/ibm-openapi-validator-(linux|macos|win)/lint-openapi-$1/g' ./ibm-openapi-*"
},
"dependencies": {
"@stoplight/spectral": "^5.5.0",
"chalk": "^2.4.1",
"commander": "^2.17.1",
"deepmerge": "^2.1.1",
Expand Down
18 changes: 15 additions & 3 deletions src/cli-validator/runValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const print = require('./utils/printResults');
const printJson = require('./utils/printJsonResults');
const printError = require('./utils/printError');
const preprocessFile = require('./utils/preprocessFile');

const spectralValidator = require('../spectral/utils/spectral-validator');
const { Spectral } = require('@stoplight/spectral');
// import the init module for creating a .validaterc file
const init = require('./utils/init.js');

Expand Down Expand Up @@ -161,6 +162,15 @@ const processInput = async function(program) {
let originalFile;
let input;

// create an instance of spectral & load the spectral ruleset, either a user's
// or the default ruleset
const spectral = new Spectral();
try {
await spectralValidator.setup(spectral);
} catch (err) {
return Promise.reject(err);
}

for (const validFile of filesToValidate) {
if (filesToValidate.length > 1) {
console.log(
Expand Down Expand Up @@ -232,10 +242,12 @@ const processInput = async function(program) {
process.chdir(originalWorkingDirectory);
}

// run validator, print the results, and determine if validator passed
// run validator & spectral, print the results, and determine if validator passed
let results;
try {
results = validator(swagger, configObject);
// let spectral handle the parsing of the original swagger/oa3 document
const spectralResults = await spectral.run(originalFile);
results = validator(swagger, configObject, spectralResults, debug);
} catch (err) {
printError(chalk, 'There was a problem with a validator.', getError(err));
if (debug) {
Expand Down
30 changes: 30 additions & 0 deletions src/cli-validator/utils/processConfiguration.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,39 @@ const validateConfigOption = function(userOption, defaultOption) {
return result;
};

const getSpectralRuleset = async function(defaultRuleset) {
// List of ruleset files to search for
const ruleSetFilesToFind = [
'.spectral.yaml',
'.spectral.yml',
'.spectral.json'
];
let ruleSetFile;

// search up the file system for the first ruleset file found
try {
for (const file of ruleSetFilesToFind) {
if (!ruleSetFile) {
ruleSetFile = await findUp(file);
}
}
} catch {
// if there's any issue finding a custom ruleset, then
// just use the default
ruleSetFile = defaultRuleset;
}

if (!ruleSetFile) {
ruleSetFile = defaultRuleset;
}

return ruleSetFile;
};

module.exports.get = getConfigObject;
module.exports.validate = validateConfigObject;
module.exports.ignore = getFilesToIgnore;
module.exports.validateOption = validateConfigOption;
module.exports.validateLimits = validateLimits;
module.exports.limits = getLimits;
module.exports.getSpectralRuleset = getSpectralRuleset;
24 changes: 23 additions & 1 deletion src/cli-validator/utils/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const sharedSemanticValidators = require('require-all')(

const circularRefsValidator = require('./circular-references-ibm');

const spectralValidator = require('../../spectral/utils/spectral-validator');

const validators = {
'2': {
semanticValidators: semanticValidators2
Expand All @@ -29,7 +31,12 @@ const validators = {
};

// this function runs the validators on the swagger object
module.exports = function validateSwagger(allSpecs, config) {
module.exports = function validateSwagger(
allSpecs,
config,
spectralResults,
debug
) {
const version = getVersion(allSpecs.jsSpec);
allSpecs.isOAS3 = version === '3';
const { semanticValidators } = validators[version];
Expand All @@ -45,6 +52,21 @@ module.exports = function validateSwagger(allSpecs, config) {
const configSpecToUse = allSpecs.isOAS3 ? 'oas3' : 'swagger2';
config = merge(config.shared, config[configSpecToUse]);

// merge the spectral results
const parsedSpectralResults = spectralValidator.parseResults(
spectralResults,
debug
);
const key = 'spectral';
if (parsedSpectralResults.errors.length) {
validationResults.errors[key] = [...parsedSpectralResults.errors];
validationResults.error = true;
}
if (parsedSpectralResults.warnings.length) {
validationResults.warnings[key] = [...parsedSpectralResults.warnings];
validationResults.warning = true;
}

// run circular reference validator
if (allSpecs.circular) {
const problem = circularRefsValidator.validate(allSpecs, config);
Expand Down
18 changes: 16 additions & 2 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,32 @@ const config = require('../cli-validator/utils/processConfiguration');
const buildSwaggerObject = require('../cli-validator/utils/buildSwaggerObject');
const validator = require('../cli-validator/utils/validator');
const getOutput = require('./utils/printForMachine');
const spectralValidator = require('../spectral/utils/spectral-validator');
const { Spectral } = require('@stoplight/spectral');

module.exports = async function(input, defaultMode = false) {
// process the config file for the validations
// process the config file for the validations &
// create an instance of spectral & load the spectral ruleset, either a user's
// or the default ruleset
let configObject;
let spectralResults;
const spectral = new Spectral();

try {
await spectralValidator.setup(spectral);
configObject = await config.get(defaultMode, chalk);
} catch (err) {
return Promise.reject(err);
}

const swagger = await buildSwaggerObject(input);
const results = validator(swagger, configObject);

try {
spectralResults = await spectral.run(input);
} catch (err) {
return Promise.reject(err);
}
const results = validator(swagger, configObject, spectralResults);

// return a json object containing the errors/warnings
return getOutput(results);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) {
let messageAuth =
'Parameters must not explicitly define `Authorization`.';
messageAuth = isOAS3
? `${messageAuth} Rely on the \`securitySchemas\` and \`security\` fields to specify authorization methods.`
? `${messageAuth} Rely on the \`securitySchemes\` and \`security\` fields to specify authorization methods.`
: `${messageAuth} Rely on the \`securityDefinitions\` and \`security\` fields to specify authorization methods.`;
// temporary message to alert users of pending status change
if (checkStatusAuth === 'warning') {
Expand Down
24 changes: 24 additions & 0 deletions src/spectral/rulesets/.defaultsForSpectral.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
extends: [[spectral:oas, off]]
formats: [oas2, oas3]
functionsDir": ../functions
rules:
no-eval-in-markdown: true
no-script-tags-in-markdown: true
openapi-tags: true
operation-description: true
operation-tags: true
operation-tag-defined: true
path-keys-no-trailing-slash: true
typed-enum: true
oas2-api-host: true
oas2-api-schemes: true
oas2-host-trailing-slash: true
oas2-valid-example: true
oas2-valid-definition-example: true
oas2-anyOf: true
oas2-oneOf: true
oas3-api-servers: true
oas3-examples-value-or-externalValue: true
oas3-server-trailing-slash: true
oas3-valid-example: true
oas3-valid-schema-example: true
74 changes: 74 additions & 0 deletions src/spectral/utils/spectral-validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const MessageCarrier = require('../../plugins/utils/messageCarrier');
const config = require('../../cli-validator/utils/processConfiguration');
const { isOpenApiv2, isOpenApiv3 } = require('@stoplight/spectral');
// default spectral ruleset file
const defaultSpectralRuleset =
__dirname + '/../rulesets/.defaultsForSpectral.yaml';

const parseResults = function(results, debug) {
const messages = new MessageCarrier();

if (results) {
for (const validationResult of results) {
if (validationResult) {
const code = validationResult['code'];
const severity = validationResult['severity'];
const message = validationResult['message'];
const path = validationResult['path'];

if (typeof severity === 'number' && code && message && path) {
if (code === 'parser') {
// Spectral doesn't allow disabling parser rules, so don't include them
// in the output (for now)
continue;
}
// Our validator only supports warning/error level, so only include
// those validation results (for now)
if (severity === 1) {
// warning
messages.addMessage(path, message, 'warning');
} else if (severity === 0) {
// error
messages.addMessage(path, message, 'error');
}
} else {
if (debug) {
console.log(
'There was an error while parsing the spectral results: ',
JSON.stringify(validationResult)
);
}
}
}
}
}
return messages;
};

// setup: registers the oas2/oas3 formats, and attempts to load the ruleset file
const setup = async function(spectral) {
if (!spectral) {
const message =
'Error (spectral-validator): An instance of spectral has not been initialized.';
return Promise.reject(message);
}

spectral.registerFormat('oas2', isOpenApiv2);
spectral.registerFormat('oas3', isOpenApiv3);

// load the spectral ruleset, either a user's or the default ruleset
const spectralRuleset = await config.getSpectralRuleset(
defaultSpectralRuleset
);

try {
return await spectral.loadRuleset(spectralRuleset);
} catch (err) {
return Promise.reject(err);
}
};

module.exports = {
parseResults,
setup
};
10 changes: 10 additions & 0 deletions test/cli-validator/mockFiles/oas3/clean.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
openapi: "3.0.0"
info:
description: Sample API definition that validates cleanly
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
contact:
email: "apiteam@swagger.io"
servers:
- url: http://petstore.swagger.io/v1
tags:
- name: pets
description: A pet
paths:
/pets:
get:
summary: List all pets
description: List all pets
operationId: list_pets
tags:
- pets
Expand Down Expand Up @@ -50,6 +58,7 @@ paths:
$ref: "#/components/schemas/Error"
post:
summary: Create a pet
description: Create a pet
operationId: create_pets
tags:
- pets
Expand All @@ -65,6 +74,7 @@ paths:
/pets/{pet_id}:
get:
summary: Info for a specific pet
description: Get information about a specific pet
operationId: get_pet_by_id
tags:
- pets
Expand Down
2 changes: 1 addition & 1 deletion test/cli-validator/tests/error-handling.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ describe('cli tool - test error handling', function() {
const capturedText = getCapturedText(consoleSpy.mock.calls);

expect(exitCode).toEqual(1);
expect(capturedText.length).toEqual(12);
expect(capturedText.length).toEqual(29);
expect(capturedText[0].trim()).toEqual(
'[Error] Trailing comma on line 36 of file ./test/cli-validator/mockFiles/trailing-comma.json.'
);
Expand Down
2 changes: 1 addition & 1 deletion test/plugins/validation/2and3/parameters-ibm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ describe('validation plugin - semantic - parameters-ibm', () => {
'3'
]);
expect(res.warnings[0].message).toEqual(
'Parameters must not explicitly define `Authorization`. Rely on the `securitySchemas` and `security` fields to specify authorization methods. This check will be converted to an `error` in an upcoming release.'
'Parameters must not explicitly define `Authorization`. Rely on the `securitySchemes` and `security` fields to specify authorization methods. This check will be converted to an `error` in an upcoming release.'
);
});

Expand Down
Loading

0 comments on commit ccbc7a6

Please sign in to comment.