@asyncapi/parser
Advanced tools
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 @@ |
180
lib/utils.js
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
280196
75
4709
112
8
+ Added@asyncapi/specs@^2.7.1
+ Addedjson-to-ast@^2.1.0
+ Added@asyncapi/specs@2.14.0(transitive)
+ Added@fmvilas/pseudo-yaml-ast@0.3.1(transitive)
+ Addedcode-error-fragment@0.0.230(transitive)
+ Addedgrapheme-splitter@1.0.4(transitive)
+ Addedjson-to-ast@2.1.0(transitive)
+ Addedyaml-ast-parser@0.0.43(transitive)
- Removedasyncapi@^2.6.1
- Removedasyncapi@2.6.1(transitive)