Skip to content

Commit

Permalink
feat: add payload validation against asyncapi schema format (#104)
Browse files Browse the repository at this point in the history
Co-authored-by: Fran Méndez <[email protected]>
  • Loading branch information
derberg and fmvilas authored Jul 9, 2020
1 parent d28c53f commit 23d5d3a
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 16 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,15 @@ AsyncAPI doesn't enforce one schema format for messages. You can have payload of
1. Create custom parser module that exports two functions:
```js
module.exports = {
parse: ({ message, defaultSchemaFormat }) => { //custom parsing logic},
/*
* message {Object} is the object containing AsyncAPI Message property
* defaultSchemaFormat {String} information about the default schema format mime type
* schemaFormat {String} information about custom schemaFormat mime type provided in AsyncAPI Document
* fileFormat {String} information if provided AsyncAPI Document was JSON or YAML
* parsedAsyncAPIDocument {Object} Full AsyncAPI Document parsed into Object
* pathToPayload {String} path of the message passed to the parser, relative to the root of AsyncAPI Document
*/
parse: ({ message, defaultSchemaFormat, originalAsyncAPIDocument, schemaFormat, fileFormat, parsedAsyncAPIDocument, pathToPayload }) => { /* custom parsing logic */ },
getMimeTypes: () => [
'//mime types that will be used as the `schemaFormat` property of the message to specify its mime type',
'application/vnd.custom.type;version=1.0.0',
Expand Down Expand Up @@ -118,6 +126,7 @@ This package throws a bunch of different error types. All errors contain a `type
|`unexpected-error`|`parsedJSON`|We have our code covered with try/catch blocks and you should never see this error. If you see it, please open an issue to let us know.
|`validation-errors`|`parsedJSON`, `validationErrors`|The AsyncAPI document contains errors. See `validationErrors` for more information.
|`impossible-to-register-parser`| None | Registration of custom message parser failed.
|`schema-validation-errors`| `parsedJSON`, `validationErrors` | Schema of the payload provided in the AsyncAPI document is not valid with AsyncAPI schema format.
For more information about the `ParserError` class, [check out the documentation](./API.md#new_ParserError_new).
Expand Down
71 changes: 71 additions & 0 deletions lib/asyncapiSchemaFormatParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const Ajv = require('ajv');
const ParserError = require('./errors/parser-error');
const asyncapi = require('@asyncapi/specs');
const { improveAjvErrors } = require('./utils');

module.exports = {
parse,
getMimeTypes
};

async function parse({ message, originalAsyncAPIDocument, fileFormat, parsedAsyncAPIDocument, pathToPayload }) {
const ajv = new Ajv({
jsonPointers: true,
allErrors: true,
schemaId: 'id',
logger: false,
});
const payloadSchema = preparePayloadSchema(asyncapi[parsedAsyncAPIDocument.asyncapi]);
const validate = ajv.compile(payloadSchema);
const valid = validate(message.payload);

if (!valid) throw new ParserError({
type: 'schema-validation-errors',
title: 'This is not a valid AsyncAPI Schema Object.',
parsedJSON: parsedAsyncAPIDocument,
validationErrors: improveAjvErrors(addFullPathToDataPath(validate.errors, pathToPayload), originalAsyncAPIDocument, fileFormat),
});
}

function getMimeTypes() {
return [
'application/vnd.aai.asyncapi;version=2.0.0',
'application/vnd.aai.asyncapi+json;version=2.0.0',
'application/vnd.aai.asyncapi+yaml;version=2.0.0',
'application/schema;version=draft-07',
'application/schema+json;version=draft-07',
'application/schema+yaml;version=draft-07',
];
}

/**
* To validate schema of the payload we just need a small portion of official AsyncAPI spec JSON Schema, the definition of the schema must be
* a main part of the JSON Schema
*
* @param {Object} asyncapiSchema AsyncAPI specification JSON Schema
* @returns {Object} valid JSON Schema document describing format of AsyncAPI-valid schema for message payload
*/
function preparePayloadSchema(asyncapiSchema) {
return {
$ref: '#/definitions/schema',
definitions: asyncapiSchema.definitions
};
}

/**
* Errors from Ajv contain dataPath information about parameter relative to parsed payload message.
* This function enriches dataPath with additional information on where is the parameter located in AsyncAPI document
*
* @param {Array<Object>} errors Ajv errors
* @param {String} path Path to location of the payload schema in AsyncAPI Document
* @returns {Array<Object>} same object as received in input but with modified datePath property so it contain full path relative to AsyncAPI document
*/
function addFullPathToDataPath(errors, path) {
return errors.map((err) => ({
...err,
...{
dataPath: `${path}${err.dataPath}`
}
}));
}

15 changes: 2 additions & 13 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
const parser = require('./parser');
const defaultAsyncAPISchemaParser = require('./asyncapiSchemaFormatParser');

const noop = {
parse: () => {}, // No operation
getMimeTypes: () => [
'application/vnd.aai.asyncapi;version=2.0.0',
'application/vnd.aai.asyncapi+json;version=2.0.0',
'application/vnd.aai.asyncapi+yaml;version=2.0.0',
'application/schema;version=draft-07',
'application/schema+json;version=draft-07',
'application/schema+yaml;version=draft-07',
]
};

parser.registerSchemaParser(noop);
parser.registerSchemaParser(defaultAsyncAPISchemaParser);

module.exports = parser;
9 changes: 7 additions & 2 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,26 +160,31 @@ async function customDocumentOperations(js, asyncapiYAMLorJSON, initialFormat, o
const op = channel[opName];
if (op) {
const messages = op.message ? (op.message.oneOf || [op.message]) : [];
const pathToPayload = `/channels/${ channelName }/${ opName }/message/payload`;
if (options.applyTraits) {
applyTraits(op);
messages.forEach(m => applyTraits(m));
}
for (const m of messages) {
await validateAndConvertMessage(m);
await validateAndConvertMessage(m, asyncapiYAMLorJSON, initialFormat, js, pathToPayload);
}
}
});
await Promise.all(convert);
}
}

async function validateAndConvertMessage(msg) {
async function validateAndConvertMessage(msg, originalAsyncAPIDocument, fileFormat, parsedAsyncAPIDocument, pathToPayload) {
const schemaFormat = msg.schemaFormat || DEFAULT_SCHEMA_FORMAT;

await PARSERS[schemaFormat]({
schemaFormat,
message: msg,
defaultSchemaFormat: DEFAULT_SCHEMA_FORMAT,
originalAsyncAPIDocument,
parsedAsyncAPIDocument,
fileFormat,
pathToPayload
});

msg.schemaFormat = DEFAULT_SCHEMA_FORMAT;
Expand Down
80 changes: 80 additions & 0 deletions test/asyncapiSchemaFormatParser_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const parser = require('../lib');
const chai = require('chai');
const fs = require('fs');
const path = require('path');
const expect = chai.expect;

describe('asyncapiSchemaFormatParser', function() {
it('should throw an error because of invalid schema', async function() {
const invalidAsyncapi = fs.readFileSync(path.resolve(__dirname, './wrong/invalid-payload-asyncapi-format.json'), 'utf8');
try {
await parser.parse(invalidAsyncapi);
} catch (e) {
expect(e.type).to.equal('https://github.com/asyncapi/parser-js/schema-validation-errors');
expect(e.title).to.equal('This is not a valid AsyncAPI Schema Object.');
expect(e.parsedJSON).to.deep.equal(JSON.parse(invalidAsyncapi));
expect(e.validationErrors).to.deep.equal([
{
title: '/channels/mychannel/publish/message/payload/additionalProperties should be object,boolean',
location: {
jsonPointer: '/channels/mychannel/publish/message/payload/additionalProperties',
startLine: 13,
startColumn: 38,
startOffset: 252,
endLine: 15,
endColumn: 15,
endOffset: 297
}
},
{
title: '/channels/mychannel/publish/message/payload/additionalProperties should be object,boolean',
location: {
jsonPointer: '/channels/mychannel/publish/message/payload/additionalProperties',
startLine: 13,
startColumn: 38,
startOffset: 252,
endLine: 15,
endColumn: 15,
endOffset: 297
}
},
{
title: '/channels/mychannel/publish/message/payload/additionalProperties should be object',
location: {
jsonPointer: '/channels/mychannel/publish/message/payload/additionalProperties',
startLine: 13,
startColumn: 38,
startOffset: 252,
endLine: 15,
endColumn: 15,
endOffset: 297
}
},
{
title: '/channels/mychannel/publish/message/payload/additionalProperties should be boolean',
location: {
jsonPointer: '/channels/mychannel/publish/message/payload/additionalProperties',
startLine: 13,
startColumn: 38,
startOffset: 252,
endLine: 15,
endColumn: 15,
endOffset: 297
}
},
{
title: '/channels/mychannel/publish/message/payload/additionalProperties should match some schema in anyOf',
location: {
jsonPointer: '/channels/mychannel/publish/message/payload/additionalProperties',
startLine: 13,
startColumn: 38,
startOffset: 252,
endLine: 15,
endColumn: 15,
endOffset: 297
}
}
]);
}
});
});
21 changes: 21 additions & 0 deletions test/wrong/invalid-payload-asyncapi-format.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"asyncapi": "2.0.0",
"info": {
"title": "My API",
"version": "1.0.0"
},
"channels": {
"mychannel": {
"publish": {
"message": {
"payload": {
"type": "object",
"additionalProperties": [
"invalid_array"
]
}
}
}
}
}
}

0 comments on commit 23d5d3a

Please sign in to comment.