Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More

@asyncapi/parser

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@asyncapi/parser - npm Package Compare versions

Comparing version 0.16.2 to 0.17.0

@@ -0,26 +1,62 @@

const ERROR_URL_PREFIX = 'https://github.com/asyncapi/parser-js/';
/**
* Represents an error while trying to parse an AsyncAPI document.
*/
class ParserError extends Error {
constructor(e, json, errors) {
super(e);
/**
* Instantiates an error
* @param {Object} definition
* @param {String} definition.type The type of the error.
* @param {String} definition.title The message of the error.
* @param {String} [definition.detail] A string containing more detailed information about the error.
* @param {Object} [definition.parsedJSON] The resulting JSON after YAML transformation. Or the JSON object if the this was the initial format.
* @param {Object[]} [definition.validationErrors] The errors resulting from the validation. For more information, see https://www.npmjs.com/package/better-ajv-errors.
* @param {String} definition.validationErrors.title A validation error message.
* @param {String} definition.validationErrors.jsonPointer The path to the field that contains the error. Uses JSON Pointer format.
* @param {Number} definition.validationErrors.startLine The line where the error starts in the AsyncAPI document.
* @param {Number} definition.validationErrors.startColumn The column where the error starts in the AsyncAPI document.
* @param {Number} definition.validationErrors.startOffset The offset (starting from the beginning of the document) where the error starts in the AsyncAPI document.
* @param {Number} definition.validationErrors.endLine The line where the error ends in the AsyncAPI document.
* @param {Number} definition.validationErrors.endColumn The column where the error ends in the AsyncAPI document.
* @param {Number} definition.validationErrors.endOffset The offset (starting from the beginning of the document) where the error ends in the AsyncAPI document.
* @param {Object} [definition.location] Error location details after trying to parse an invalid JSON or YAML document.
* @param {Number} definition.location.startLine The line of the YAML/JSON document where the error starts.
* @param {Number} definition.location.startColumn The column of the YAML/JSON document where the error starts.
* @param {Number} definition.location.startOffset The offset (starting from the beginning of the document) where the error starts in the YAML/JSON AsyncAPI document.
* @param {Object[]} [definition.refs] Error details after trying to resolve $ref's.
* @param {String} definition.refs.title A validation error message.
* @param {String} definition.refs.jsonPointer The path to the field that contains the error. Uses JSON Pointer format.
* @param {Number} definition.refs.startLine The line where the error starts in the AsyncAPI document.
* @param {Number} definition.refs.startColumn The column where the error starts in the AsyncAPI document.
* @param {Number} definition.refs.startOffset The offset (starting from the beginning of the document) where the error starts in the AsyncAPI document.
* @param {Number} definition.refs.endLine The line where the error ends in the AsyncAPI document.
* @param {Number} definition.refs.endColumn The column where the error ends in the AsyncAPI document.
* @param {Number} definition.refs.endOffset The offset (starting from the beginning of the document) where the error ends in the AsyncAPI document.
*/
constructor(def) {
super();
buildError(def, this);
this.message = def.title;
}
let msg;
if (typeof e === 'string') {
msg = e;
}
if (typeof e.message === 'string') {
msg = e.message;
}
if (json) {
this.parsedJSON = json;
}
if (errors) {
this.errors = errors;
}
this.message = msg;
/**
* Returns a JS object representation of the error.
*/
toJS() {
return buildError(this, {});
}
}
const buildError = (from, to) => {
to.type = from.type.startsWith(ERROR_URL_PREFIX) ? from.type : `${ERROR_URL_PREFIX}${from.type}`;
to.title = from.title;
if (from.detail) to.detail = from.detail;
if (from.validationErrors) to.validationErrors = from.validationErrors;
if (from.parsedJSON) to.parsedJSON = from.parsedJSON;
if (from.location) to.location = from.location;
if (from.refs) to.refs = from.refs;
return to;
};
module.exports = ParserError;

@@ -0,10 +1,9 @@

const path = require('path');
const Ajv = require('ajv');
const fetch = require('node-fetch');
const asyncapi = require('asyncapi');
const asyncapi = require('@asyncapi/specs');
const $RefParser = require('json-schema-ref-parser');
const mergePatch = require('tiny-merge-patch').apply;
const ParserError = require('./errors/parser-error');
const ParserErrorNoJS = require('./errors/parser-error-no-js');
const ParserErrorUnsupportedVersion = require('./errors/parser-error-unsupported-version');
const { toJS } = require('./utils');
const { toJS, findRefs, getLocationOf, improveAjvErrors } = require('./utils');
const AsyncAPIDocument = require('./models/asyncapi');

@@ -24,4 +23,2 @@

ParserError,
ParserErrorNoJS,
ParserErrorUnsupportedVersion,
AsyncAPIDocument,

@@ -36,3 +33,3 @@ };

* @param {Object} [options] Configuration options.
* @param {String} [options.path] Path to the AsyncAPI document. It will be used to resolve relative references.
* @param {String} [options.path] Path to the AsyncAPI document. It will be used to resolve relative references. Defaults to current working dir.
* @param {Object} [options.parse] Options object to pass to {@link https://apidevtools.org/json-schema-ref-parser/docs/options.html|json-schema-ref-parser}.

@@ -45,19 +42,40 @@ * @param {Object} [options.resolve] Options object to pass to {@link https://apidevtools.org/json-schema-ref-parser/docs/options.html|json-schema-ref-parser}.

async function parse(asyncapiYAMLorJSON, options = {}) {
let js;
let parsedJSON;
let initialFormat;
options.path = options.path || `${process.cwd()}${path.sep}`;
try {
js = toJS(asyncapiYAMLorJSON);
({ initialFormat, parsedJSON } = toJS(asyncapiYAMLorJSON));
if (typeof js !== 'object') {
throw new ParserErrorNoJS('Could not convert AsyncAPI to JSON.');
if (typeof parsedJSON !== 'object') {
throw new ParserError({
type: 'impossible-to-convert-to-json',
title: 'Could not convert AsyncAPI to JSON.',
detail: 'Most probably the AsyncAPI document contains invalid YAML or YAML features not supported in JSON.'
});
}
if (!js.asyncapi || !asyncapi[js.asyncapi]) {
throw new ParserErrorUnsupportedVersion(`AsyncAPI version is missing or unsupported: ${js.asyncapi}.`, js);
if (!parsedJSON.asyncapi) {
throw new ParserError({
type: 'missing-asyncapi-field',
title: 'The `asyncapi` field is missing.',
parsedJSON,
});
}
if (parsedJSON.asyncapi.startsWith('1.') || !asyncapi[parsedJSON.asyncapi]) {
throw new ParserError({
type: 'unsupported-version',
title: `Version ${parsedJSON.asyncapi} is not supported.`,
detail: 'Please use latest version of the specification.',
parsedJSON,
validationErrors: [getLocationOf('/asyncapi', asyncapiYAMLorJSON, initialFormat)],
});
}
if (options.applyTraits === undefined) options.applyTraits = true;
if (options.path) {
js = await $RefParser.dereference(options.path, js, {
try {
parsedJSON = await $RefParser.dereference(options.path, parsedJSON, {
parse: options.parse,

@@ -67,7 +85,8 @@ resolve: options.resolve,

});
} else {
js = await $RefParser.dereference(js, {
parse: options.parse,
resolve: options.resolve,
dereference: options.dereference,
} catch (err) {
throw new ParserError({
type: 'dereference-error',
title: err.message,
parsedJSON,
refs: findRefs(parsedJSON, err.path, options.path, initialFormat, asyncapiYAMLorJSON),
});

@@ -77,8 +96,11 @@ }

if (e instanceof ParserError) throw e;
if (e instanceof ParserErrorNoJS) throw e;
if (e instanceof ParserErrorUnsupportedVersion) throw e;
throw new ParserError(e.message, js);
throw new ParserError({
type: 'unexpected-error',
title: e.message,
parsedJSON,
});
}
const ajv = new Ajv({
jsonPointers: true,
allErrors: true,

@@ -92,12 +114,22 @@ schemaId: 'id',

try {
const validate = ajv.compile(asyncapi[js.asyncapi]);
const valid = validate(js);
if (!valid) throw new ParserError('Invalid AsyncAPI document', js, validate.errors);
const validate = ajv.compile(asyncapi[parsedJSON.asyncapi]);
const valid = validate(parsedJSON);
if (!valid) throw new ParserError({
type: 'validation-errors',
title: 'There were errors validating the AsyncAPI document.',
parsedJSON,
validationErrors: improveAjvErrors(validate.errors, asyncapiYAMLorJSON, initialFormat),
});
await iterateDocument(js, options);
await iterateDocument(parsedJSON, options);
} catch (e) {
throw new ParserError(e, e.parsedJSON, e.errors);
if (e instanceof ParserError) throw e;
throw new ParserError({
type: 'unexpected-error',
title: e.message,
parsedJSON,
});
}
return new AsyncAPIDocument(js);
return new AsyncAPIDocument(parsedJSON);
}

@@ -104,0 +136,0 @@

@@ -0,20 +1,67 @@

const path = require('path');
const YAML = require('js-yaml');
const { yamlAST, loc } = require('@fmvilas/pseudo-yaml-ast');
const jsonAST = require('json-to-ast');
const jsonParseBetterErrors = require('../lib/json-parse');
const ParserError = require('./errors/parser-error');
module.exports.toJS = (asyncapiYAMLorJSON) => {
const utils = module.exports;
utils.toJS = (asyncapiYAMLorJSON) => {
if (!asyncapiYAMLorJSON) {
throw new Error(`Document can't be null, false or empty.`);
throw new ParserError({
type: 'null-or-falsey-document',
title: `Document can't be null or falsey.`,
});
}
if (typeof asyncapiYAMLorJSON === 'object') {
return asyncapiYAMLorJSON;
if (asyncapiYAMLorJSON.constructor && asyncapiYAMLorJSON.constructor.name === 'Object') {
return {
initialFormat: 'js',
parsedJSON: asyncapiYAMLorJSON,
};
}
if (typeof asyncapiYAMLorJSON !== 'string') {
throw new ParserError({
type: 'invalid-document-type',
title: 'The AsyncAPI document has to be either a string or a JS object.',
});
}
try {
return JSON.parse(asyncapiYAMLorJSON);
} catch (e) {
if (asyncapiYAMLorJSON.trimLeft().startsWith('{')) {
try {
return YAML.safeLoad(asyncapiYAMLorJSON);
return {
initialFormat: 'json',
parsedJSON: jsonParseBetterErrors(asyncapiYAMLorJSON),
};
} catch (e) {
throw new ParserError({
type: 'invalid-json',
title: 'The provided JSON is not valid.',
detail: e.message,
location: {
startOffset: e.offset,
startLine: e.startLine,
startColumn: e.startColumn,
},
});
}
} else {
try {
return {
initialFormat: 'yaml',
parsedJSON: YAML.safeLoad(asyncapiYAMLorJSON),
};
} catch (err) {
err.message = `Document has to be either JSON or YAML: ${err.message}`;
throw err;
throw new ParserError({
type: 'invalid-yaml',
title: 'The provided YAML is not valid.',
detail: err.message,
location: {
startOffset: err.mark.position,
startLine: err.mark.line + 1,
startColumn: err.mark.column + 1,
},
});
}

@@ -24,3 +71,3 @@ }

module.exports.createMapOfType = (obj, Type) => {
utils.createMapOfType = (obj, Type) => {
const result = {};

@@ -36,3 +83,3 @@ if (!obj) return result;

module.exports.getMapKeyOfType = (obj, key, Type) => {
utils.getMapKeyOfType = (obj, key, Type) => {
if (!obj) return null;

@@ -43,3 +90,3 @@ if (!obj[key]) return null;

module.exports.addExtensions = (obj) => {
utils.addExtensions = (obj) => {
obj.prototype.extensions = function () {

@@ -63,1 +110,108 @@ const result = {};

};
utils.findRefs = (json, absolutePath, relativeDir, initialFormat, asyncapiYAMLorJSON) => {
const relativePath = path.relative(relativeDir, absolutePath);
let refs = [];
traverse(json, (key, value, scope) => {
if (key === '$ref' && value === relativePath) {
refs.push({ location: [...scope, '$ref'] });
}
});
if (!refs.length) return refs;
if (initialFormat === 'js') {
return refs.map(ref => ({ jsonPointer: `/${ref.location.join('/')}` }));
}
if (initialFormat === 'yaml') {
const pseudoAST = yamlAST(asyncapiYAMLorJSON);
refs = refs.map(ref => findLocationOf(ref.location, pseudoAST, initialFormat));
} else if (initialFormat === 'json') {
const ast = jsonAST(asyncapiYAMLorJSON);
refs = refs.map(ref => findLocationOf(ref.location, ast, initialFormat));
}
return refs;
};
utils.getLocationOf = (jsonPointer, asyncapiYAMLorJSON, initialFormat) => {
const ast = getAST(asyncapiYAMLorJSON, initialFormat);
if (!ast) return { jsonPointer };
return findLocationOf(jsonPointerToArray(jsonPointer), ast, initialFormat);
}
utils.improveAjvErrors = (errors, asyncapiYAMLorJSON, initialFormat) => {
const ast = getAST(asyncapiYAMLorJSON, initialFormat);
return errors.map(error => {
const defaultLocation = { jsonPointer: error.dataPath || '/' };
return {
title: `${error.dataPath || '/'} ${error.message}`,
location: ast ? findLocationOf(jsonPointerToArray(error.dataPath), ast, initialFormat) : defaultLocation,
};
});
};
const jsonPointerToArray = jsonPointer => (jsonPointer || '/').split('/').splice(1);
const getAST = (asyncapiYAMLorJSON, initialFormat) => {
if (initialFormat === 'yaml') {
return yamlAST(asyncapiYAMLorJSON);
} else if (initialFormat === 'json') {
return jsonAST(asyncapiYAMLorJSON);
}
};
const findLocationOf = (keys, ast, initialFormat) => {
let node;
let info;
if (initialFormat === 'js') return { jsonPointer: `/${keys.join('/')}` };
if (initialFormat === 'yaml') {
node = findNode(ast, keys);
if (!node) return { jsonPointer: `/${keys.join('/')}` };
info = node[loc];
} else if (initialFormat === 'json') {
node = findNodeInAST(ast, keys);
if (!node) return { jsonPointer: `/${keys.join('/')}` };
info = node.loc;
}
return {
jsonPointer: `/${keys.join('/')}`,
startLine: info.start.line,
startColumn: info.start.column + 1,
startOffset: info.start.offset,
endLine: info.end ? info.end.line : undefined,
endColumn: info.end ? info.end.column + 1 : undefined,
endOffset: info.end ? info.end.offset : undefined,
};
}
const findNode = (obj, location) => {
for (let key of location) {
obj = obj[key];
}
return obj;
};
const findNodeInAST = (ast, location) => {
let obj = ast;
for (let key of location) {
if (!Array.isArray(obj.children)) return;
const child = obj.children.find(c => c && c.type === 'Property' && c.key && c.key.value === key);
if (!child) return;
obj = child.value;
}
return obj;
};
const traverse = function (o, fn, scope = []) {
for (let i in o) {
fn.apply(this, [i, o[i], scope]);
if (o[i] !== null && typeof o[i] === "object") {
traverse(o[i], fn, scope.concat(i));
}
}
}
{
"name": "@asyncapi/parser",
"version": "0.16.2",
"version": "0.17.0",
"description": "JavaScript AsyncAPI parser.",

@@ -46,6 +46,8 @@ "main": "lib/index.js",

"dependencies": {
"@fmvilas/pseudo-yaml-ast": "^0.3.0",
"ajv": "^6.10.1",
"asyncapi": "^2.6.1",
"@asyncapi/specs": "^2.7.1",
"js-yaml": "^3.13.1",
"json-schema-ref-parser": "^7.1.0",
"json-to-ast": "^2.1.0",
"node-fetch": "^2.6.0",

@@ -52,0 +54,0 @@ "tiny-merge-patch": "^0.1.2"

@@ -82,2 +82,21 @@ <h5 align="center">

### Error types
This package throws a bunch of different error types. All errors contain a `type` (prefixed by this repo URL) and a `title` field. The following table describes all the errors and the extra fields they include:
|Type|Extra Fields|Description|
|---|---|---|
|`null-or-falsey-document`| None | The AsyncAPI document is null or a JS "falsey" value.
|`invalid-document-type`| None | The AsyncAPI document is not a string nor a JS object.
|`invalid-json`| `detail`, `location` | The AsyncAPI document is not valid JSON.
|`invalid-yaml`| `detail`, `location` | The AsyncAPI document is not valid YAML.
|`impossible-to-convert-to-json`|`detail`|Internally, this parser only handles JSON so it tries to immediately convert the YAML to JSON. This error means this process failed.
|`missing-asyncapi-field`|`parsedJSON`|The AsyncAPI document doesn't have the mandatory `asyncapi` field.
|`unsupported-version`|`detail`, `parsedJSON`, `validationErrors`|The version of the `asyncapi` field is not supported. Typically, this means that you're using a version below 2.0.0.
|`dereference-error`|`parsedJSON`, `refs`|This means the parser tried to resolve and dereference $ref's and the process failed. Typically, this means the $ref it's pointing to doesn't exist.
|`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.
For more information about the `ParserError` class, [check out the documentation](./API.md#new_ParserError_new).
### Develop

@@ -93,2 +112,2 @@

Read [CONTRIBUTING](CONTRIBUTING.md) guide.
Read [CONTRIBUTING](CONTRIBUTING.md) guide.

@@ -22,3 +22,24 @@ {

}
}, "components": {
"messages": {
"testMessage": {
"payload": {
"$ref": "#/components/schemas/testSchema"
}
}
},
"schemas": {
"testSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"test": {
"$ref": "refs/refed.yaml"
}
}
}
}
}
}

@@ -13,8 +13,33 @@ const chai = require('chai');

const inputYAML = fs.readFileSync(path.resolve(__dirname, "./asyncapi.yaml"), 'utf8');
const inputJSON = fs.readFileSync(path.resolve(__dirname, "./asyncapi.json"), 'utf8');
const invalidAsyncapiYAML = fs.readFileSync(path.resolve(__dirname, "./invalid-asyncapi.yaml"), 'utf8');
const invalidAsyncpiJSON = fs.readFileSync(path.resolve(__dirname, "./invalid-asyncapi.json"), 'utf8');
const outputJSON = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"mychannel":{"publish":{"externalDocs":{"x-extension":true,"url":"https://company.com/docs"},"message":{"payload":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"},"x-some-extension":"some extension","x-parser-original-traits":[{"x-some-extension":"some extension"}],"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"},"x-parser-original-traits":[{"externalDocs":{"url":"https://company.com/docs"}}]}}},"components":{"messages":{"testMessage":{"payload":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"},"x-some-extension":"some extension","x-parser-original-traits":[{"x-some-extension":"some extension"}],"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"}},"schemas":{"testSchema":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"}},"messageTraits":{"extension":{"x-some-extension":"some extension"}},"operationTraits":{"docs":{"externalDocs":{"url":"https://company.com/docs"}}}}}';
const invalidYamlOutput = '{"asyncapi":"2.0.0","info":{"version":"1.0.0"},"channels":{"mychannel":{"publish":{"traits":[{"externalDocs":{"url":"https://company.com/docs"}}],"externalDocs":{"x-extension":true,"url":"https://irrelevant.com"},"message":{"traits":[{"x-some-extension":"some extension"}],"payload":{"type":"object","properties":{"name":{"type":"string"},"test":{"type":"object","properties":{"testing":{"type":"string"}}}}}}}}},"components":{"messages":{"testMessage":{"traits":[{"x-some-extension":"some extension"}],"payload":{"type":"object","properties":{"name":{"type":"string"},"test":{"type":"object","properties":{"testing":{"type":"string"}}}}}}},"schemas":{"testSchema":{"type":"object","properties":{"name":{"type":"string"},"test":{"type":"object","properties":{"testing":{"type":"string"}}}}}},"messageTraits":{"extension":{"x-some-extension":"some extension"}},"operationTraits":{"docs":{"externalDocs":{"url":"https://company.com/docs"}}}}}'
const invalidJsonOutput = '{"asyncapi":"2.0.0","info":{"version":"1.0.0"},"channels":{"mychannel":{"publish":{"message":{"payload":{"type":"object","properties":{"name":{"type":"string"}}}}}}},"components":{"messages":{"testMessage":{"payload":{"type":"object","properties":{"name":{"type":"string"},"test":{"type":"object","properties":{"testing":{"type":"string"}}}}}}},"schemas":{"testSchema":{"type":"object","properties":{"name":{"type":"string"},"test":{"type":"object","properties":{"testing":{"type":"string"}}}}}}}}'
const outputJsonWithRefs = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"mychannel":{"publish":{"traits":[{"$ref":"#/components/operationTraits/docs"}],"externalDocs":{"x-extension":true,"url":"https://irrelevant.com"},"message":{"$ref":"#/components/messages/testMessage"}}}},"components":{"messages":{"testMessage":{"traits":[{"$ref":"#/components/messageTraits/extension"}],"payload":{"$ref":"#/components/schemas/testSchema"}}},"schemas":{"testSchema":{"type":"object","properties":{"name":{"type":"string"},"test":{"$ref":"refs/refed.yaml"}}}},"messageTraits":{"extension":{"x-some-extension":"some extension"}},"operationTraits":{"docs":{"externalDocs":{"url":"https://company.com/docs"}}}}}';
const outputJsonNoApplyTraits = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"mychannel":{"publish":{"traits":[{"externalDocs":{"url":"https://company.com/docs"}}],"externalDocs":{"x-extension":true,"url":"https://irrelevant.com"},"message":{"traits":[{"x-some-extension":"some extension"}],"payload":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"},"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"}}}},"components":{"messages":{"testMessage":{"traits":[{"x-some-extension":"some extension"}],"payload":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"},"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"}},"schemas":{"testSchema":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"}},"messageTraits":{"extension":{"x-some-extension":"some extension"}},"operationTraits":{"docs":{"externalDocs":{"url":"https://company.com/docs"}}}}}';
const invalidAsyncAPI = { "asyncapi": "2.0.0", "info": {} };
const errorsOfInvalidAsyncAPI = [{ keyword: 'required', dataPath: '.info', schemaPath: '#/required', params: { missingProperty: 'title' }, message: 'should have required property \'title\'' }, { keyword: 'required', dataPath: '.info', schemaPath: '#/required', params: { missingProperty: 'version' }, message: 'should have required property \'version\'' }, { keyword: 'required', dataPath: '', schemaPath: '#/required', params: { missingProperty: 'channels' }, message: 'should have required property \'channels\'' }];
describe('parse()', function () {
const checkErrorTypeAndMessage = async (fn, type, message) => {
try {
await fn();
throw 'should not be reachable';
} catch (e) {
expect(e instanceof ParserError).to.equal(true);
expect(e).to.have.own.property('type', type);
expect(e).to.have.own.property('message', message);
}
}
const checkErrorParsedJSON = async (fn, parsedJSON) => {
try {
await fn();
throw 'should not be reachable';
} catch (e) {
expect(JSON.stringify(e.parsedJSON)).to.equal(parsedJSON);
}
}
describe.only('parse()', function () {
it('should parse YAML', async function () {

@@ -25,25 +50,89 @@ const result = await parser.parse(inputYAML, { path: __filename });

it('should forward ajv errors and AsyncAPI json', async function () {
it('should not apply traits', async function () {
const result = await parser.parse(inputYAML, { path: __filename, applyTraits: false });
await expect(JSON.stringify(result.json())).to.equal(outputJsonNoApplyTraits);
});
it('should fail when asyncapi is not valid', async function () {
try {
await parser.parse(invalidAsyncAPI);
} catch (e) {
await expect(e.errors).to.deep.equal(errorsOfInvalidAsyncAPI);
} catch(e) {
await expect(e.type).to.equal('https://github.com/asyncapi/parser-js/validation-errors');
await expect(e.title).to.equal('There were errors validating the AsyncAPI document.');
await expect(e.validationErrors).to.deep.equal([{
title: '/info should have required property \'title\'',
location: { jsonPointer: '/info' }
},
{
title: '/info should have required property \'version\'',
location: { jsonPointer: '/info' }
},
{
title: '/ should have required property \'channels\'',
location: { jsonPointer: '/' }
}]);
await expect(e.parsedJSON).to.deep.equal(invalidAsyncAPI);
}
});
it('should not forward AsyncAPI json when it is not possible to convert it', async function () {
it('should fail when asyncapi is not valid (yaml)', async function () {
try {
await parser.parse(invalidAsyncapiYAML, { path: __filename });
} catch(e) {
await expect(e.type).to.equal('https://github.com/asyncapi/parser-js/validation-errors');
await expect(e.title).to.equal('There were errors validating the AsyncAPI document.');
await expect(e.validationErrors).to.deep.equal([{
title: '/info should have required property \'title\'',
location: {
jsonPointer: '/info',
startLine: 2,
startColumn: 1,
startOffset: 16,
endLine: 3,
endColumn: 19,
endOffset: 40,
}
}]);
await expect(JSON.stringify(e.parsedJSON)).to.equal(invalidYamlOutput);
}
});
it('should fail when asyncapi is not valid (json)', async function () {
try {
await parser.parse(invalidAsyncpiJSON, { path: __filename });
} catch(e) {
await expect(e.type).to.equal('https://github.com/asyncapi/parser-js/validation-errors');
await expect(e.title).to.equal('There were errors validating the AsyncAPI document.');
await expect(e.validationErrors).to.deep.equal([{
title: '/info should have required property \'title\'',
location: {
jsonPointer: '/info',
startLine: 3,
startColumn: 11,
startOffset: 33,
endLine: 5,
endColumn: 4,
endOffset: 58,
}
}]);
await expect(JSON.stringify(e.parsedJSON)).to.equal(invalidJsonOutput);
}
});
it('should fail when it is not possible to convert asyncapi to json', async function () {
try {
await parser.parse('bad');
} catch (e) {
await expect(e.constructor.name).to.equal('ParserErrorNoJS');
await expect(e.parsedJSON).to.equal(undefined);
} catch(e) {
await expect(e.type).to.equal('https://github.com/asyncapi/parser-js/impossible-to-convert-to-json');
await expect(e.title).to.equal('Could not convert AsyncAPI to JSON.');
await expect(e.detail).to.equal('Most probably the AsyncAPI document contains invalid YAML or YAML features not supported in JSON.');
}
});
it('should forward AsyncAPI json when version is not supported', async function () {
it('should fail when asyncapi is not present', async function () {
try {
await parser.parse('bad: true');
} catch (e) {
await expect(e.constructor.name).to.equal('ParserErrorUnsupportedVersion');
} catch(e) {
await expect(e.type).to.equal('https://github.com/asyncapi/parser-js/missing-asyncapi-field');
await expect(e.title).to.equal('The `asyncapi` field is missing.');
await expect(e.parsedJSON).to.deep.equal({ bad: true });

@@ -53,24 +142,157 @@ }

it('should not apply traits', async function () {
const result = await parser.parse(inputYAML, { path: __filename, applyTraits: false });
await expect(JSON.stringify(result.json())).to.equal(outputJsonNoApplyTraits);
it('should fail when asyncapi version is not supported', async function () {
try {
await parser.parse('asyncapi: 1.2.0');
} catch (e) {
await expect(e.type).to.equal('https://github.com/asyncapi/parser-js/unsupported-version');
await expect(e.title).to.equal('Version 1.2.0 is not supported.');
await expect(e.detail).to.equal('Please use latest version of the specification.');
await expect(e.parsedJSON).to.deep.equal({ asyncapi: '1.2.0' });
await expect(e.validationErrors).to.deep.equal([{
jsonPointer: '/asyncapi',
startLine: 1,
startColumn: 1,
startOffset: 0,
endLine: 1,
endColumn: 16,
endOffset: 15,
}]);
}
});
it('should fail when asyncapi is not yaml nor json', async function () {
try {
await parser.parse('bad:\nbad:');
} catch(e) {
await expect(e.type).to.equal('https://github.com/asyncapi/parser-js/invalid-yaml');
await expect(e.title).to.equal('The provided YAML is not valid.');
await expect(e.detail).to.equal('duplicated mapping key at line 2, column -4:\n bad:\n ^');
await expect(e.location).to.deep.equal({ startOffset: 5, startLine: 2, startColumn: -4 });
}
});
it('should fail to resolve relative files when options.path is not provided', async function () {
const testFn = async () => await parser.parse(inputYAML);
await expect(testFn())
.to.be.rejectedWith(ParserError)
const type = 'https://github.com/asyncapi/parser-js/dereference-error';
const message = `Error opening file "${path.resolve(process.cwd(), 'refs/refed.yaml')}" \nENOENT: no such file or directory, open '${path.resolve(process.cwd(), 'refs/refed.yaml')}'`;
const testFn = async () => { await parser.parse(inputYAML) };
await checkErrorTypeAndMessage(testFn, type, message);
await checkErrorParsedJSON(testFn, outputJsonWithRefs);
});
it('should offer information about YAML line and column where $ref errors are located', async function () {
try {
await parser.parse(inputYAML)
} catch (e) {
expect(e.refs).to.deep.equal([{
jsonPointer: '/components/schemas/testSchema/properties/test/$ref',
startLine: 30,
startColumn: 11,
startOffset: 615,
endLine: 30,
endColumn: 34,
endOffset: 638,
}]);
}
});
it('should offer information about JSON line and column where $ref errors are located', async function () {
try {
await parser.parse(inputJSON)
} catch (e) {
expect(e.refs).to.deep.equal([{
jsonPointer: '/components/schemas/testSchema/properties/test/$ref',
startLine: 38,
startColumn: 21,
startOffset: 599,
endLine: 38,
endColumn: 38,
endOffset: 616,
}]);
}
});
it('should not offer information about JS line and column where $ref errors are located if format is JS', async function () {
try {
await parser.parse(JSON.parse(inputJSON))
} catch (e) {
expect(e.refs).to.deep.equal([{
jsonPointer: '/components/schemas/testSchema/properties/test/$ref',
}]);
}
});
it('should throw error if document is invalid YAML', async function () {
const testFn = async () => await parser.parse(invalidYAML, { path: __filename });
await expect(testFn())
.to.be.rejectedWith(ParserError)
try {
await parser.parse(invalidYAML, { path: __filename })
} catch (e) {
expect(e.type).to.equal('https://github.com/asyncapi/parser-js/invalid-yaml');
expect(e.title).to.equal('The provided YAML is not valid.');
expect(e.detail).to.equal(`bad indentation of a mapping entry at line 19, column 11:\n $ref: "#/components/schemas/sentAt"\n ^`);
expect(e.location).to.deep.equal({ startOffset: 460, startLine: 19, startColumn: 11 });
}
});
it('should throw error if document is invalid JSON', async function () {
try {
await parser.parse(' {"invalid "json" }');
} catch (e) {
expect(e.type).to.equal('https://github.com/asyncapi/parser-js/invalid-json');
expect(e.title).to.equal('The provided JSON is not valid.');
expect(e.detail).to.equal(`Unexpected token j in JSON at position 12 while parsing near ' {"invalid "json" }'`);
expect(e.location).to.deep.equal({ startOffset: 12, startLine: 1, startColumn: 12 });
}
});
it('should throw error if document is empty', async function () {
const testFn = async () => await parser.parse('');
await expect(testFn())
.to.be.rejectedWith(ParserError)
it('should throw error if document is null or falsey', async function () {
const type = 'https://github.com/asyncapi/parser-js/null-or-falsey-document';
const message = `Document can't be null or falsey.`;
await checkErrorTypeAndMessage(async () => {
await parser.parse('');
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse(false);
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse(null);
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse(undefined);
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse(NaN);
}, type, message);
});
it('should throw error if document is not string nor object', async function () {
const type = 'https://github.com/asyncapi/parser-js/invalid-document-type';
const message = 'The AsyncAPI document has to be either a string or a JS object.';
await checkErrorTypeAndMessage(async () => {
await parser.parse(true);
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse([]);
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse(new Map());
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse(new Set());
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse(new WeakMap());
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse(new WeakSet());
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse(1);
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse(Symbol('test'));
}, type, message);
await checkErrorTypeAndMessage(async () => {
await parser.parse(() => {});
}, type, message);
});
});

Sorry, the diff of this file is too big to display