From a54dc11c495a880504b3a9842b20b0a8bb50383b Mon Sep 17 00:00:00 2001 From: Viacheslav Turovskyi Date: Sat, 6 Apr 2024 11:05:29 +0000 Subject: [PATCH] feat: add `x-origin` property --- API.md | 9 ++- README.md | 139 ++++++++++++++++------------------------ example/bundle-cjs.cjs | 4 +- example/bundle-cjs.js | 10 ++- example/bundle-esm.js | 2 +- example/bundle-esm.mjs | 2 +- example/bundle.ts | 2 +- src/index.ts | 22 +++---- src/parser.ts | 5 +- src/util.ts | 2 +- tests/lib/index.spec.ts | 47 +++++++++----- 11 files changed, 119 insertions(+), 125 deletions(-) diff --git a/API.md b/API.md index e22e0b6..ce8e00c 100644 --- a/API.md +++ b/API.md @@ -78,8 +78,7 @@ console.log(document.string()); // get JSON string | files | Array.<string> |

Array of stringified AsyncAPI documents in YAML format, that are to be bundled (or array of filepaths, resolved and passed via Array.map() and fs.readFileSync, which is the same, see README.md).

| | [options] | Object | | | [options.base] | string \| object |

Base object whose properties will be retained.

| -| [options.referenceIntoComponents] | boolean |

Pass true to resolve external references to components.

| -| [options.baseDir] | string |

Pass folder path to

| +| [options.xOrigin] | boolean |

Pass true to generate properties x-origin that will contain historical values of dereferenced $refs.

| **Example** **TypeScript** @@ -89,7 +88,7 @@ import bundle from '@asyncapi/bundler'; async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + xOrigin: true, }); console.log(document.yml()); // the complete bundled AsyncAPI document @@ -108,7 +107,7 @@ const bundle = require('@asyncapi/bundler'); async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + xOrigin: true, }); writeFileSync('asyncapi.yaml', document.yml()); } @@ -125,7 +124,7 @@ import bundle from '@asyncapi/bundler'; async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + xOrigin: true, }); writeFileSync('asyncapi.yaml', document.yml()); } diff --git a/README.md b/README.md index 1d59d78..21a6c25 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,19 @@ - [Overview](#overview) - [Installation](#installation) - [Usage](#usage) - * [Resolving external references into components](#resolving-external-references-into-components) + * [Dereference of the external references](#dereference-of-the-external-references) + * [Property `x-origin`](#property-x-origin) + * [Movement of components to `components`](#movement-of-components-to-components) + * [Code examples](#code-examples) - [bundle(files, [options])](#bundlefiles-options) - [Contributors](#contributors) ## Overview -An official library that lets you bundle/merge your specification files into one. AsyncAPI Bundler can help you if: +An official library that lets you bundle/dereference or merge into one your AsyncAPI Documents. + +AsyncAPI Bundler can help you if:
your specification file is divided into different smaller files and is using JSON `$ref` property to reference components @@ -184,89 +189,55 @@ async function main() { main().catch(e => console.error(e)); ``` -### Resolving external references into components -You can resolve external references by moving them to Messages Object, under `components/messages`. +### Dereference of the external references -
-For example +`Bundler` dereferences the provided AsyncAPI Document to the maximum possible extent, leaving intact only those internal references that MUST be `Reference Object`s according to the AsyncAPI Specification (thus, should never be dereferenced): -```yml -# main.yaml -asyncapi: 2.5.0 -info: - title: Account Service - version: 1.0.0 - description: This service is in charge of processing user signups -channels: - user/signedup: - subscribe: - message: - $ref: './messages.yaml#/messages/UserSignedUp' - test: - subscribe: - message: - $ref: '#/components/messages/TestMessage' -components: - messages: - TestMessage: - payload: - type: string +- AsyncAPI Specification v2.6.0 + +There are no internal references that MUST be `Reference Object`s. -# messages.yaml -messages: - UserSignedUp: - payload: - type: object - properties: - displayName: - type: string - description: Name of the user - email: - type: string - format: email - description: Email of the user - UserLoggedIn: - payload: - type: object - properties: - id: string +- AsyncAPI Specification v3.0.0 -# After combining -# asyncapi.yaml -asyncapi: 2.5.0 -info: - title: Account Service - version: 1.0.0 - description: This service is in charge of processing user signups -channels: - user/signedup: - subscribe: - message: - $ref: '#/components/messages/UserSignedUp' - test: - subscribe: - message: - $ref: '#/components/messages/TestMessage' -components: - messages: - TestMessage: - payload: - type: string - UserSignedUp: - payload: - type: object - properties: - displayName: - type: string - description: Name of the user - email: - type: string - format: email - description: Email of the user +Regexes of internal references that MUST be `Reference Object`s: ``` -
-
+/#\/channels\/[a-zA-Z0-9]*\/servers/ +/#\/operations\/[a-zA-Z0-9]*\/channel/ +/#\/operations\/[a-zA-Z0-9]*\/messages/ +/#\/operations\/[a-zA-Z0-9]*\/reply\/channel/ +/#\/operations\/[a-zA-Z0-9]*\/reply\/messages/ +/#\/components\/channels\/[a-zA-Z0-9]*\/servers/ +/#\/components\/operations\/[a-zA-Z0-9]*\/channel/ +/#\/components\/operations\/[a-zA-Z0-9]*\/messages/ +/#\/components\/operations\/[a-zA-Z0-9]*\/reply\/channel/ +/#\/components\/operations\/[a-zA-Z0-9]*\/reply\/messages/ +``` + + +### Property `x-origin` + +Property `x-origin` is used for origin tracing in `Bundler` and component naming in `Optimizer`. + +It originated from [this comment](https://github.com/asyncapi/bundler/issues/97#issuecomment-1330501758) in a year-long discussion: + +> The $ref usually also carries a semantical meaning to understand easier what it is (example "$ref : financial-system.yaml#/components/schemas/bankAccountIdentifier"). If the bundling just resolves this ref inline, the semantical meaning of the $ref pointer gets lost and cannot be recovered in later steps. The optimizer would need to invent an artificial component name for the "bankAccountIdentifier" when moving it to the components section. + +Thus, property `x-origin` contains historical values of dereferenced `$ref`s, which are also used by `Optimizer` to give meaningful names to components it moves through the AsyncAPI Document. + +However, if a user doesn't need / doesn't want `x-origin` properties to be present in the structure of the AsyncAPI Document (values of the `x-origin` property may leak internal details about how the system described by the AsyncAPI Document is structured,) they can pass `{ xOrigin: false }` (or omit passing `xOrigin` at all) to the `Bundler` in the options object. + + +### Movement of components to `components` + +The movement of all AsyncAPI Specification-valid components to the `components` section of the AsyncAPI Document is done by the [`Optimizer`](https://github.com/asyncapi/optimizer) v1.0.0+. + +To get in CI/code an AsyncAPI Document, that is dereferenced [to its maximum possible extent](#dereference-of-the-external-references) with all of its components moved to the `components` section, the original AsyncAPI Document must be run through chain `Bundler -> Optimizer`. + +If `Optimizer` is not able to find `x-origin` properties during optimization of the provided AsyncAPI Document, the existing names of components are used as a fallback mechanism, but keep in mind that components' names may lack semantic meaning in this case. + + +### Code examples **TypeScript** ```ts @@ -275,7 +246,7 @@ import bundle from '@asyncapi/bundler'; async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + xOrigin: true, }); console.log(document.yml()); // the complete bundled AsyncAPI document @@ -294,7 +265,7 @@ const bundle = require('@asyncapi/bundler'); async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + xOrigin: true, }); writeFileSync('asyncapi.yaml', document.yml()); } @@ -311,7 +282,7 @@ import bundle from '@asyncapi/bundler'; async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + xOrigin: true, }); writeFileSync('asyncapi.yaml', document.yml()); } @@ -320,6 +291,7 @@ main().catch(e => console.error(e)); ``` + ## bundle(files, [options]) @@ -330,7 +302,8 @@ main().catch(e => console.error(e)); | files | Array.<string> | Array of stringified AsyncAPI documents in YAML format, that are to be bundled (or array of filepaths, resolved and passed via `Array.map()` and `fs.readFileSync`, which is the same). | | [options] | Object | | | [options.base] | string \| object | Base object whose properties will be retained. | -| [options.referenceIntoComponents] | boolean | Pass `true` to resolve external references to components. | +| [options.xOrigin] | boolean |

Pass true to generate properties x-origin that will contain historical values of dereferenced $refs.

| + ## Contributors diff --git a/example/bundle-cjs.cjs b/example/bundle-cjs.cjs index 44d37ae..f3f35c2 100644 --- a/example/bundle-cjs.cjs +++ b/example/bundle-cjs.cjs @@ -9,8 +9,8 @@ const { readFileSync, writeFileSync } = require('fs'); const bundle = require('@asyncapi/bundler'); async function main() { - const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - xOrigin: true, + const document = await bundle(['./main151.yaml', './main153.yaml'].map( f => readFileSync(f, 'utf-8')), { + xOrigin: false, }); if (document.yml()) { writeFileSync('asyncapi.yaml', document.yml()); diff --git a/example/bundle-cjs.js b/example/bundle-cjs.js index 325182b..720c3f7 100644 --- a/example/bundle-cjs.js +++ b/example/bundle-cjs.js @@ -11,9 +11,13 @@ const { readFileSync, writeFileSync } = require('fs'); const bundle = require('@asyncapi/bundler'); async function main() { - const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: false, - }); + const filePaths = ['./camera.yml','./audio.yml']; + const document = await bundle( + filePaths.map(filePath => readFileSync(filePath, 'utf-8')), { + // base: readFileSync('./base.yml', 'utf-8'), + xOrigin: true + } + ); if (document.yml()) { writeFileSync('asyncapi.yaml', document.yml()); } diff --git a/example/bundle-esm.js b/example/bundle-esm.js index 0be8e60..25899df 100644 --- a/example/bundle-esm.js +++ b/example/bundle-esm.js @@ -12,7 +12,7 @@ import bundle from '@asyncapi/bundler'; async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: false, + xOrigin: true, }); if (document.yml()) { writeFileSync('asyncapi.yaml', document.yml()); diff --git a/example/bundle-esm.mjs b/example/bundle-esm.mjs index a420cc2..89b3b2b 100644 --- a/example/bundle-esm.mjs +++ b/example/bundle-esm.mjs @@ -10,7 +10,7 @@ import bundle from '@asyncapi/bundler'; async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: false, + xOrigin: true, }); if (document.yml()) { writeFileSync('asyncapi.yaml', document.yml()); diff --git a/example/bundle.ts b/example/bundle.ts index 6abf594..aa98d4f 100644 --- a/example/bundle.ts +++ b/example/bundle.ts @@ -3,7 +3,7 @@ import bundle from '@asyncapi/bundler'; async function main() { const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - referenceIntoComponents: true, + xOrigin: true, }); writeFileSync('asyncapi.yaml', document.yml()); } diff --git a/src/index.ts b/src/index.ts index 2d983f6..ae00d9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { toJS, resolve, versionCheck } from './util'; import { Document } from './document'; +import { parse } from './parser'; import type { AsyncAPIObject } from './spec-types'; @@ -11,9 +12,8 @@ import type { AsyncAPIObject } from './spec-types'; * @param {Object} [options] * @param {string | object} [options.base] Base object whose properties will be * retained. - * @param {boolean} [options.referenceIntoComponents] Pass `true` to resolve - * external references to components. - * @param {string} [options.baseDir] Pass folder path to + * @param {boolean} [options.xOrigin] Pass `true` to generate properties + * `x-origin` that will contain historical values of dereferenced `$ref`s. * * @return {Document} * @@ -26,7 +26,7 @@ import type { AsyncAPIObject } from './spec-types'; * * async function main() { * const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - * referenceIntoComponents: true, + * xOrigin: true, * }); * * console.log(document.yml()); // the complete bundled AsyncAPI document @@ -45,7 +45,7 @@ import type { AsyncAPIObject } from './spec-types'; * * async function main() { * const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - * referenceIntoComponents: true, + * xOrigin: true, * }); * writeFileSync('asyncapi.yaml', document.yml()); * } @@ -62,7 +62,7 @@ import type { AsyncAPIObject } from './spec-types'; * * async function main() { * const document = await bundle([readFileSync('./main.yaml', 'utf-8')], { - * referenceIntoComponents: true, + * xOrigin: true, * }); * writeFileSync('asyncapi.yaml', document.yml()); * } @@ -72,15 +72,15 @@ import type { AsyncAPIObject } from './spec-types'; * */ export default async function bundle(files: string[], options: any = {}) { - // if (typeof options.base !== 'undefined') { - // options.base = toJS(options.base); - // await parse(options.base, options); - // } - const parsedJsons = files.map(file => toJS(file)) as AsyncAPIObject[]; const majorVersion = versionCheck(parsedJsons); + if (typeof options.base !== 'undefined') { + options.base = toJS(options.base); + await parse(options.base, majorVersion, options); + } + const resolvedJsons: AsyncAPIObject[] = await resolve(parsedJsons, majorVersion, options); return new Document(resolvedJsons, options.base); diff --git a/src/parser.ts b/src/parser.ts index 9120500..872a2c1 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -21,6 +21,7 @@ export async function parse( options: any = {} ) { let validationResult: any[] = []; + /* eslint-disable indent */ // It is assumed that there will be major Spec versions 4, 5 and on. switch (specVersion) { @@ -32,7 +33,7 @@ export async function parse( return; }, onDereference: (path: string, value: AsyncAPIObject) => { - if (options.xOrigin) { + if (options.xOrigin === true) { value['x-origin'] = path; } }, @@ -59,7 +60,7 @@ export async function parse( ); }, onDereference: (path: string, value: AsyncAPIObject) => { - if (options.xOrigin) { + if (options.xOrigin === true) { value['x-origin'] = path; } }, diff --git a/src/util.ts b/src/util.ts index 9d4a5d5..79b441d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -43,7 +43,7 @@ export const toJS = (asyncapiYAMLorJSON: string | object) => { * * @param {Object} asyncapiDocuments * @param {Object} options - * @param {boolean} options.referenceIntoComponents + * @param {boolean} options.xOrigin * @returns {Array} * @private */ diff --git a/tests/lib/index.spec.ts b/tests/lib/index.spec.ts index 7ceb64e..7c44dc0 100644 --- a/tests/lib/index.spec.ts +++ b/tests/lib/index.spec.ts @@ -19,7 +19,7 @@ describe('[integration testing] bundler should ', () => { noValidation: true, } ); - + console.log(response.yml()) expect(response).toBeDefined(); }); @@ -35,7 +35,7 @@ describe('[integration testing] bundler should ', () => { fs.readFileSync(path.resolve(process.cwd(), file), 'utf-8') ), { - referenceIntoComponents: false, + xOrigin: true, noValidation: true, } ) @@ -54,7 +54,7 @@ describe('[integration testing] bundler should ', () => { fs.readFileSync(path.resolve(process.cwd(), file), 'utf-8') ), { - referenceIntoComponents: false, + xOrigin: true, noValidation: true, } ) @@ -62,33 +62,50 @@ describe('[integration testing] bundler should ', () => { }); test('should be able to bundle base file', async () => { - const files = ['./tests/base-option/lights.yaml', './tests/base-option/camera.yaml'] + const files = [ + './tests/base-option/lights.yaml', + './tests/base-option/camera.yaml', + ]; expect( await bundle( - files.map(file => fs.readFileSync(path.resolve(process.cwd(), file), 'utf-8')), - { referenceIntoComponents: false, base: fs.readFileSync(path.resolve(process.cwd(), './tests/base-option/base.yaml'), 'utf-8'), noValidation: true, } + files.map(file => + fs.readFileSync(path.resolve(process.cwd(), file), 'utf-8') + ), + { + xOrigin: true, + base: fs.readFileSync( + path.resolve(process.cwd(), './tests/base-option/base.yaml'), + 'utf-8' + ), + noValidation: true, + } ) ).resolves; - - }) + }); test('should be able to change the baseDir folder', async () => { - const files = ['./tests/specfiles/main.yaml'] + const files = ['./tests/specfiles/main.yaml']; expect( await bundle( - files.map(file => fs.readFileSync(path.resolve(process.cwd(), file), 'utf-8')), - {baseDir: './tests/specfiles', noValidation: true} + files.map(file => + fs.readFileSync(path.resolve(process.cwd(), file), 'utf-8') + ), + { baseDir: './tests/specfiles', noValidation: true } ) - ).resolves - }) + ).resolves; + }); }); describe('[unit testing]', () => { test('`isExternalReference()` should return `true` on external reference', () => { - expect(isExternalReference('./components/messages/UserSignedUp')).toBeTruthy(); + expect( + isExternalReference('./components/messages/UserSignedUp') + ).toBeTruthy(); }); test('`isExternalReference()` should return `false` on local reference', () => { - expect(isExternalReference('#/components/messages/UserSignedUp')).toBeFalsy(); + expect( + isExternalReference('#/components/messages/UserSignedUp') + ).toBeFalsy(); }); });