diff --git a/example/bundle-cjs.cjs b/example/bundle-cjs.cjs index 550099b..06e7c91 100644 --- a/example/bundle-cjs.cjs +++ b/example/bundle-cjs.cjs @@ -10,9 +10,11 @@ const bundle = require('@asyncapi/bundler'); async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + referenceIntoComponents: false, }); - writeFileSync('asyncapi.yaml', document.yml()); + if (document.yml()) { + writeFileSync('asyncapi.yaml', document.yml()); + } } main().catch(e => console.error(e)); diff --git a/example/bundle-cjs.js b/example/bundle-cjs.js index 40d84e8..325182b 100644 --- a/example/bundle-cjs.js +++ b/example/bundle-cjs.js @@ -12,9 +12,11 @@ const bundle = require('@asyncapi/bundler'); async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + referenceIntoComponents: false, }); - writeFileSync('asyncapi.yaml', document.yml()); + if (document.yml()) { + writeFileSync('asyncapi.yaml', document.yml()); + } } main().catch(e => console.error(e)); diff --git a/example/bundle-esm.js b/example/bundle-esm.js index df97a27..0be8e60 100644 --- a/example/bundle-esm.js +++ b/example/bundle-esm.js @@ -12,9 +12,11 @@ import bundle from '@asyncapi/bundler'; async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + referenceIntoComponents: false, }); - writeFileSync('asyncapi.yaml', document.yml()); + if (document.yml()) { + writeFileSync('asyncapi.yaml', document.yml()); + } } main().catch(e => console.error(e)); diff --git a/example/bundle-esm.mjs b/example/bundle-esm.mjs index 46eb275..a420cc2 100644 --- a/example/bundle-esm.mjs +++ b/example/bundle-esm.mjs @@ -10,9 +10,11 @@ import bundle from '@asyncapi/bundler'; async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + referenceIntoComponents: false, }); - writeFileSync('asyncapi.yaml', document.yml()); + if (document.yml()) { + writeFileSync('asyncapi.yaml', document.yml()); + } } main().catch(e => console.error(e)); diff --git a/example/bundle.ts b/example/bundle.ts index 6abf594..d33ad7e 100644 --- a/example/bundle.ts +++ b/example/bundle.ts @@ -3,9 +3,11 @@ import bundle from '@asyncapi/bundler'; async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + referenceIntoComponents: false, }); - writeFileSync('asyncapi.yaml', document.yml()); + if (document.yml()) { + writeFileSync('asyncapi.yaml', document.yml()); + } } main().catch(e => console.error(e)); diff --git a/example/package.json b/example/package.json index 9eb3af9..921d537 100644 --- a/example/package.json +++ b/example/package.json @@ -14,8 +14,8 @@ "license": "Apache-2.0", "dependencies": { "@asyncapi/bundler": "../", - "@types/node": "^18.7.23", + "@types/node": "^20.11.30", "ts-node": "^10.9.1", - "typescript": "^4.8.4" + "typescript": "^5.4.3" } } diff --git a/package.json b/package.json index 36bc7d9..99143f2 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,10 @@ "/lib" ], "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.9", + "@apidevtools/json-schema-ref-parser": "^9.1.2", + "@asyncapi/parser": "^3.0.10", "@types/json-schema": "^7.0.11", + "axios": "^1.6.8", "js-yaml": "^4.1.0", "jsonpath-plus": "^6.0.1", "lodash": "^4.17.21" diff --git a/src/document.ts b/src/document.ts index 558d203..43d65b9 100644 --- a/src/document.ts +++ b/src/document.ts @@ -37,14 +37,18 @@ export class Document { * @return {Object} */ json() { - return this._doc; + if (Object.keys(this._doc).length) { + return this._doc; + } } /** * @return {string} */ yml() { - return yaml.dump(this._doc); + if (Object.keys(this._doc).length) { + return yaml.dump(this._doc); + } } /** diff --git a/src/util.ts b/src/util.ts index f8b9741..d2bdad3 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,9 +1,11 @@ +import fs from 'fs'; import $RefParser from '@apidevtools/json-schema-ref-parser'; -import { cloneDeep } from 'lodash'; +import axios from 'axios'; +import { cloneDeep, merge } from 'lodash'; import yaml from 'js-yaml'; import { parse } from './parser'; import { ParserError } from './errors'; -import {JSONPath} from 'jsonpath-plus'; +import { JSONPath } from 'jsonpath-plus'; import type { AsyncAPIObject } from './spec-types'; import path from 'path'; @@ -39,7 +41,7 @@ export const toJS = (asyncapiYAMLorJSON: string | object) => { title: 'The provided yaml is not valid.', }); } - + return yaml.load(asyncapiYAMLorJSON); }; @@ -73,6 +75,7 @@ export const resolve = async ( if (options.referenceIntoComponents) { await parse(asyncapiDocument); } + addXOrigins(asyncapiDocument); // eslint-disable-line @typescript-eslint/no-use-before-define const bundledAsyncAPIDocument = await $RefParser.bundle(asyncapiDocument); docs.push(bundledAsyncAPIDocument); } @@ -81,7 +84,7 @@ export const resolve = async ( }; /** - * + * * @param asyncapiDocument {AsyncAPIObject} * @returns {boolean} */ @@ -96,7 +99,9 @@ export function versionCheck(asyncapiDocuments: AsyncAPIObject[]): number { for (const asyncapiDocument of asyncapiDocuments) { const majorVersion = getSpecVersion(asyncapiDocument); if (majorVersion !== currentVersion) { - throw new Error('Unable to bundle specification file of different major versions'); + throw new Error( + 'Unable to bundle specification file of different major versions' + ); } currentVersion = majorVersion; } @@ -118,31 +123,81 @@ export function notAUrl(ref: string): boolean { export function resolveBaseFileDir(file: object, baseFileDir: string) { /** - * Update the local refences in a given file with the - * absolute file path using the baseDir passed by the - * user as an option. + * Update the local refences in a given file with the + * absolute file path using the baseDir passed by the + * user as an option. */ JSONPath({ json: file, resultType: 'all', - path: '$.channels.*.messages.*' - }).forEach(({parent, parentProperty}: {parent: any, parentProperty: string}) => { - const ref = parent[String(parentProperty)]['$ref']; - if (isExternalReference(ref) && notAUrl(ref)) { - parent[String(parentProperty)]['$ref'] = path.resolve(baseFileDir, ref); + path: '$.channels.*.messages.*', + }).forEach( + ({ parent, parentProperty }: { parent: any; parentProperty: string }) => { + const ref = parent[String(parentProperty)]['$ref']; + if (isExternalReference(ref) && notAUrl(ref)) { + parent[String(parentProperty)]['$ref'] = path.resolve(baseFileDir, ref); + } } - }); + ); JSONPath({ json: file, resultType: 'all', - path: '$.operations.*.messages.*' + path: '$.operations.*.messages.*', }).forEach( - ({parent, parentProperty}: {parent: any, parentProperty: string}) => { + ({ parent, parentProperty }: { parent: any; parentProperty: string }) => { const ref = parent[String(parentProperty)]['$ref']; if (isExternalReference(ref) && notAUrl(ref)) { parent[String(parentProperty)]['$ref'] = path.resolve(baseFileDir, ref); } } ); -} \ No newline at end of file +} + +// Moved 'addXOrigins' to the beginning of the scope to avoid an ESLint's error +// `'addXOrigins' was used before it was defined` +export function addXOrigins(asyncapiDocument: AsyncAPIObject) { + // VALUE from 'asyncapiDocument' becomes KEY for the + // underlying and recursive functions + Object.values(asyncapiDocument).forEach(async (key: any) => { + if (key && typeof key === 'object' && key !== '$ref') { + if (Object.keys(key).indexOf('$ref') !== -1) { + if (isExternalReference(key['$ref'])) { + key['x-origin'] = key['$ref']; + + // If an external `$ref` is found, the function goes into + // second-level recursion to see if there are more `$ref`s whose + // values need to be copied to the `x-origin` properties of the + // `$ref`ed file. + // If an external `$ref` is found again, the function goes into the + // third-level recursion, and so on, until it reaches a file that + // contains no external `$ref`s at all. + // Then it exits all the way up in the opposite direction. + + const inlineAsyncapiDocumentURI = key['$ref'].split('#/'); + const inlineAsyncapiDocumentPath = inlineAsyncapiDocumentURI[0]; + const inlineAsyncapiDocumentPointer = inlineAsyncapiDocumentURI[1]; + + let inlineAsyncapiDocument = inlineAsyncapiDocumentPath.startsWith( + 'http' + ) + ? yaml.load(await axios(inlineAsyncapiDocumentPath)) + : (yaml.load( + fs.readFileSync(inlineAsyncapiDocumentPath, 'utf-8') // eslint-disable-line indent + ) as any); // eslint-disable-line indent + + inlineAsyncapiDocument = + inlineAsyncapiDocument[String(inlineAsyncapiDocumentPointer)]; + + if (inlineAsyncapiDocument) { + addXOrigins(inlineAsyncapiDocument as AsyncAPIObject); + merge(key, inlineAsyncapiDocument); + } + } + } else { + addXOrigins(key); + } + } + }); + return asyncapiDocument; +} diff --git a/src/v3/parser.ts b/src/v3/parser.ts index 4152a08..fadc94d 100644 --- a/src/v3/parser.ts +++ b/src/v3/parser.ts @@ -1,105 +1,63 @@ -import $RefParser, {$Refs} from '@apidevtools/json-schema-ref-parser'; -import { JSONPath } from 'jsonpath-plus'; -import { merge } from 'lodash'; +import $RefParser from '@apidevtools/json-schema-ref-parser'; +import { Parser } from '@asyncapi/parser'; +import { addXOrigins } from '../util'; import { AsyncAPIObject } from 'spec-types'; -class ExternalComponents { - ref; - resolvedJSON; - constructor(ref: string, resolvedJSON: string) { - this.ref = ref; - this.resolvedJSON = resolvedJSON; - } +const parser = new Parser(); + +export async function parse(JSONSchema: AsyncAPIObject) { + addXOrigins(JSONSchema); + + const dereferencedJSONSchema = await $RefParser.dereference(JSONSchema, { + dereference: { + circular: false, + excludedPathMatcher: (path: string): boolean => { + return ( + // prettier-ignore + !!(/#\/channels\/[a-zA-Z0-9]*\/servers/).exec(path) || + !!(/#\/operations\/[a-zA-Z0-9]*\/channel/).exec(path) || + !!(/#\/operations\/[a-zA-Z0-9]*\/messages/).exec(path) || + !!(/#\/operations\/[a-zA-Z0-9]*\/reply\/channel/).exec(path) || + !!(/#\/operations\/[a-zA-Z0-9]*\/reply\/messages/).exec(path) || + !!(/#\/components\/channels\/[a-zA-Z0-9]*\/servers/).exec(path) || + !!(/#\/components\/operations\/[a-zA-Z0-9]*\/channel/).exec(path) || + !!(/#\/components\/operations\/[a-zA-Z0-9]*\/messages/).exec(path) || + !!(/#\/components\/operations\/[a-zA-Z0-9]*\/reply\/channel/).exec( + path + ) || + !!(/#\/components\/operations\/[a-zA-Z0-9]*\/reply\/messages/).exec( + path + ) + ); + }, + }, + }); - getKey() { - const keys = this.ref.split('/'); - return keys[keys.length - 1]; - } + const result = await parser.validate( + JSON.parse(JSON.stringify(dereferencedJSONSchema)) + ); - getValue() { - return this.resolvedJSON; + if (result.length !== 0) { + console.log( + 'Validation of the resulting AsyncAPI Document failed.\nList of remarks:\n', + result + ); + throw new Error(); } -} -function crawlChannelPropertiesForRefs(JSONSchema: any) { - return JSONPath({ - json: JSONSchema, - path: '$.channels.*.messages.*.[\'$ref\']', - }); + return result; } -export function isExternalReference(ref: string): boolean { - return typeof ref === 'string' && !ref.startsWith('#'); -} - -async function resolveExternalRefs(parsedJSON: any, $refs: $Refs) { - const componentObj: any = { messages: {} }; - JSONPath({ - json: parsedJSON, - resultType: 'all', - path: '$.channels.*.messages.*', - }).forEach( - ({ parent, parentProperty }: { parent: any; parentProperty: string }) => { - const ref = parent[String(parentProperty)]['$ref']; - if (isExternalReference(ref)) { - const value: any = $refs.get(ref); - const component = new ExternalComponents(ref, value); - if (componentObj.messages) { - componentObj.messages[String(component.getKey())] = - component.getValue() as unknown; - } - parent[String(parentProperty)][ - '$ref' - ] = `#/components/messages/${component.getKey()}`; - } - } - ); - - return componentObj; -} +export async function resolveV3Document(asyncapiDocuments: AsyncAPIObject[]) { + const docs = []; -async function resolveExternalRefsForOperation(parsedJSON: any, $refs: $Refs) { - JSONPath({ - json: parsedJSON, - resultType: 'all', - path: '$.operations.*.messages.*' - }).forEach( - ({parent, parentProperty}: {parent: any, parentProperty: string}) => { - parent.forEach((reference: any) => { - const ref = reference['$ref']; - if (isExternalReference(ref)) { - const value: any = $refs.get(ref); - const component = new ExternalComponents(ref, value); - parent[String(parentProperty)]['$ref'] = `#/components/messages/${component.getKey()}`; - } - }); + try { + for (const asyncapiDocument of asyncapiDocuments) { + await parse(asyncapiDocument); + docs.push(asyncapiDocument); } - ); -} + } catch (e) {} -export async function parse(JSONSchema: any) { - const $ref: any = await $RefParser.resolve(JSONSchema); - const refs = crawlChannelPropertiesForRefs(JSONSchema); - for (const ref of refs) { - if (isExternalReference(ref)) { - const componentObj = await resolveExternalRefs(JSONSchema, $ref); - await resolveExternalRefsForOperation(JSONSchema, $ref); - if (JSONSchema.components) { - merge(JSONSchema.components, componentObj); - } else { - JSONSchema.components = componentObj; - } - } - } + return docs; } - -export async function resolveV3Document(asyncapiDocuments: AsyncAPIObject[]) { - const docs = []; - for (const asyncapiDocument of asyncapiDocuments) { - await parse(asyncapiDocument); - //const bundledAsyncAPIDocument = await $RefParser.bundle(asyncapiDocument) - docs.push(asyncapiDocument); - } - return docs; -} \ No newline at end of file diff --git a/tests/lib/index.spec.ts b/tests/lib/index.spec.ts index 94a808c..592beb1 100644 --- a/tests/lib/index.spec.ts +++ b/tests/lib/index.spec.ts @@ -37,7 +37,7 @@ describe('[integration testing] bundler should ', () => { ); const asyncapiObject = doc.json(); - const message = asyncapiObject.channels?.['user/signedup']?.subscribe?.message as ReferenceObject; + const message = asyncapiObject?.channels?.['user/signedup']?.subscribe?.message as ReferenceObject; expect(message.$ref).toMatch('#/components/messages/UserSignedUp'); });