From e41d6e4ad7e4ba4c94110371931cdb35dda4a06a Mon Sep 17 00:00:00 2001 From: Viacheslav Turovskyi Date: Wed, 7 Aug 2024 10:30:37 +0300 Subject: [PATCH] feat: add resolution of `$ref`s in subdirectories (#181) --- API.md | 25 +- example/bundle-esm.js | 14 +- .../asyncapi/index.yaml | 21 ++ .../receive/lightingMeasured/README.md | 46 ++++ .../receive/lightingMeasured/asyncapi.yaml | 35 +++ .../receive/lightingMeasured/schema.json | 16 ++ .../asyncapi/send/lightTurnOff/README.md | 32 +++ .../asyncapi/send/lightTurnOff/asyncapi.yaml | 25 ++ .../asyncapi/send/lightTurnOff/schema.json | 20 ++ .../asyncapi/send/lightTurnOn/README.md | 32 +++ .../asyncapi/send/lightTurnOn/asyncapi.yaml | 25 ++ .../asyncapi/send/lightTurnOn/schema.json | 19 ++ example/package.json | 1 + src/document.ts | 19 +- src/index.ts | 75 ++++-- src/parser.ts | 37 +-- src/util.ts | 140 +++++++--- tests/base-option/camera.yaml | 2 +- tests/base-option/lights.yaml | 2 +- tests/lib/index.spec.ts | 246 +++++++++++++++++- tests/nested-dirs-mixed/asyncapi/index.yaml | 21 ++ .../receive/lightingMeasured/README.md | 46 ++++ .../receive/lightingMeasured/asyncapi.yaml | 35 +++ .../receive/lightingMeasured/schema.json | 16 ++ .../asyncapi/send/lightTurnOff/README.md | 32 +++ .../asyncapi/send/lightTurnOff/asyncapi.yaml | 25 ++ .../asyncapi/send/lightTurnOff/schema.json | 20 ++ .../asyncapi/send/lightTurnOn/README.md | 32 +++ .../asyncapi/send/lightTurnOn/asyncapi.yaml | 25 ++ .../asyncapi/send/lightTurnOn/schema.json | 19 ++ 30 files changed, 964 insertions(+), 139 deletions(-) create mode 100644 example/example-with-nested-dirs/asyncapi/index.yaml create mode 100644 example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/README.md create mode 100644 example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/asyncapi.yaml create mode 100644 example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/schema.json create mode 100644 example/example-with-nested-dirs/asyncapi/send/lightTurnOff/README.md create mode 100644 example/example-with-nested-dirs/asyncapi/send/lightTurnOff/asyncapi.yaml create mode 100644 example/example-with-nested-dirs/asyncapi/send/lightTurnOff/schema.json create mode 100644 example/example-with-nested-dirs/asyncapi/send/lightTurnOn/README.md create mode 100644 example/example-with-nested-dirs/asyncapi/send/lightTurnOn/asyncapi.yaml create mode 100644 example/example-with-nested-dirs/asyncapi/send/lightTurnOn/schema.json create mode 100644 tests/nested-dirs-mixed/asyncapi/index.yaml create mode 100644 tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/README.md create mode 100644 tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/asyncapi.yaml create mode 100644 tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/schema.json create mode 100644 tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/README.md create mode 100644 tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/asyncapi.yaml create mode 100644 tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/schema.json create mode 100644 tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/README.md create mode 100644 tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/asyncapi.yaml create mode 100644 tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/schema.json diff --git a/API.md b/API.md index 6e8ba90..f77ebe7 100644 --- a/API.md +++ b/API.md @@ -5,13 +5,6 @@
-## Members - -
-
resolveboolean
-
-
- ## Functions
@@ -25,23 +18,22 @@ **Kind**: global class * [Document](#Document) - * [new Document(parsedJSONList, base)](#new_Document_new) + * [new Document(AsyncAPIObject)](#new_Document_new) * [.json()](#Document+json) ⇒ Object * [.yml()](#Document+yml) ⇒ string * [.string()](#Document+string) ⇒ string -### new Document(parsedJSONList, base) +### new Document(AsyncAPIObject) | Param | Type | | --- | --- | -| parsedJSONList | Array.<Object> | -| base | Object | +| AsyncAPIObject | Object | **Example** ```js -const document = new Document(parsedJSONList, base); +const document = new Document(bundledDocument); console.log(document.json()); // get JSON object console.log(document.yml()); // get YAML string @@ -59,15 +51,6 @@ console.log(document.string()); // get JSON string ### document.string() ⇒ string **Kind**: instance method of [Document](#Document) - - -## resolve ⇒ boolean -**Kind**: global variable - -| Param | Type | -| --- | --- | -| asyncapiDocument | AsyncAPIObject | - ## bundle(files, [options]) ⇒ [Document](#Document) diff --git a/example/bundle-esm.js b/example/bundle-esm.js index df285ea..dd92f79 100644 --- a/example/bundle-esm.js +++ b/example/bundle-esm.js @@ -11,11 +11,19 @@ import { writeFileSync } from 'fs'; import bundle from '@asyncapi/bundler'; async function main() { - const document = await bundle(['./main.yaml'], { - xOrigin: true, + const files = [ + 'send/lightTurnOn/asyncapi.yaml', + 'send/lightTurnOff/asyncapi.yaml', + 'receive/lightingMeasured/asyncapi.yaml', + ]; + + const document = await bundle(files, { + base: 'index.yaml', + baseDir: 'example-with-nested-dirs/asyncapi', + xOrigin: false, }); if (document.yml()) { - writeFileSync('asyncapi.yaml', document.yml()); + writeFileSync('bundled.yaml', document.yml()); } } diff --git a/example/example-with-nested-dirs/asyncapi/index.yaml b/example/example-with-nested-dirs/asyncapi/index.yaml new file mode 100644 index 0000000..728830c --- /dev/null +++ b/example/example-with-nested-dirs/asyncapi/index.yaml @@ -0,0 +1,21 @@ +asyncapi: 3.0.0 +info: + title: Streetlights MQTT API + version: 1.0.0 + description: "The Smartylighting Streetlights API allows you to remotely manage the city lights.\n\n### Check out its awesome features:\n\n* Turn a specific streetlight on/off \U0001F303\n* Dim a specific streetlight \U0001F60E\n* Receive real-time information about environmental lighting conditions \U0001F4C8\n" + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' +defaultContentType: application/json +servers: + production: + host: 'test.mosquitto.org:{port}' + protocol: mqtt + description: Test broker + variables: + port: + description: Secure connection (TLS) is available through port 8883. + default: '1883' + enum: + - '1883' + - '8883' diff --git a/example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/README.md b/example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/README.md new file mode 100644 index 0000000..56bb556 --- /dev/null +++ b/example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/README.md @@ -0,0 +1,46 @@ +# Lighting Measured 1.0.0 documentation + + +## Operations + +### RECEIVE `smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured` Operation + +*Inform about environmental lighting conditions of a particular streetlight.* + +* Operation ID: `receiveLightMeasurement` + +The topic on which measured values may be produced and consumed. + +#### Parameters + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| streetlightId | string | The ID of the streetlight. | - | - | **required** | + + +#### Message Light measured `lightMeasured` + +*Inform about environmental lighting conditions of a particular streetlight.* + +* Message ID: `lightMeasured` +* Content type: [application/json](https://www.iana.org/assignments/media-types/application/json) + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| lumens | integer | Light intensity measured in lumens. | - | >= 0 | - | +| sentAt | string | Date and time when the message was sent. | - | format (`date-time`) | - | + +> Examples of payload _(generated)_ + +```json +{ + "lumens": 0, + "sentAt": "2019-08-24T14:15:22Z" +} +``` + + + diff --git a/example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/asyncapi.yaml b/example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/asyncapi.yaml new file mode 100644 index 0000000..b465ad6 --- /dev/null +++ b/example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/asyncapi.yaml @@ -0,0 +1,35 @@ +asyncapi: 3.0.0 +info: + title: Lighting Measured + version: 1.0.0 +channels: + lightingMeasured: + address: 'smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured' + messages: + lightMeasured: + $ref: '#/components/messages/lightMeasured' + description: The topic on which measured values may be produced and consumed. + parameters: + streetlightId: + description: The ID of the streetlight. +operations: + receiveLightMeasurement: + action: receive + channel: + $ref: '#/channels/lightingMeasured' + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + messages: + - $ref: '#/channels/lightingMeasured/messages/lightMeasured' +components: + messages: + lightMeasured: + name: lightMeasured + title: Light measured + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + contentType: application/json + payload: + $ref: ./schema.json diff --git a/example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/schema.json b/example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/schema.json new file mode 100644 index 0000000..6207762 --- /dev/null +++ b/example/example-with-nested-dirs/asyncapi/receive/lightingMeasured/schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "lumens": { + "type": "integer", + "minimum": 0, + "description": "Light intensity measured in lumens." + }, + "sentAt": { + "type": "string", + "format": "date-time", + "description": "Date and time when the message was sent." + } + } +} diff --git a/example/example-with-nested-dirs/asyncapi/send/lightTurnOff/README.md b/example/example-with-nested-dirs/asyncapi/send/lightTurnOff/README.md new file mode 100644 index 0000000..4896f61 --- /dev/null +++ b/example/example-with-nested-dirs/asyncapi/send/lightTurnOff/README.md @@ -0,0 +1,32 @@ +# Light Turn Off 1.0.0 documentation + + +## Operations + +### SEND `smartylighting/streetlights/1/0/action/{streetlightId}/turn/off` Operation + +* Operation ID: `turnOff` + +#### Message Turn on/off `turnOnOff` + +*Command a particular streetlight to turn the lights on or off.* + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| command | string | Whether to turn on or off the light. | allowed (`"on"`, `"off"`) | - | - | +| sentAt | string | Date and time when the message was sent. | - | format (`date-time`) | - | + +> Examples of payload _(generated)_ + +```json +{ + "command": "on", + "sentAt": "2019-08-24T14:15:22Z" +} +``` + + + diff --git a/example/example-with-nested-dirs/asyncapi/send/lightTurnOff/asyncapi.yaml b/example/example-with-nested-dirs/asyncapi/send/lightTurnOff/asyncapi.yaml new file mode 100644 index 0000000..5c8566f --- /dev/null +++ b/example/example-with-nested-dirs/asyncapi/send/lightTurnOff/asyncapi.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Light Turn Off + version: 1.0.0 +channels: + lightTurnOff: + address: 'smartylighting/streetlights/1/0/action/{streetlightId}/turn/off' + messages: + turnOff: + $ref: '#/components/messages/turnOnOff' +operations: + turnOff: + action: send + channel: + $ref: '#/channels/lightTurnOff' + messages: + - $ref: '#/channels/lightTurnOff/messages/turnOff' +components: + messages: + turnOnOff: + name: turnOnOff + title: Turn on/off + summary: Command a particular streetlight to turn the lights on or off. + payload: + $ref: ./schema.json diff --git a/example/example-with-nested-dirs/asyncapi/send/lightTurnOff/schema.json b/example/example-with-nested-dirs/asyncapi/send/lightTurnOff/schema.json new file mode 100644 index 0000000..65804f3 --- /dev/null +++ b/example/example-with-nested-dirs/asyncapi/send/lightTurnOff/schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "command": { + "type": "string", + "enum": [ + "on", + "off" + ], + "description": "Whether to turn on or off the light." + }, + "sentAt": { + "type": "string", + "format": "date-time", + "description": "Date and time when the message was sent." + } + } + } + \ No newline at end of file diff --git a/example/example-with-nested-dirs/asyncapi/send/lightTurnOn/README.md b/example/example-with-nested-dirs/asyncapi/send/lightTurnOn/README.md new file mode 100644 index 0000000..5b0c2a1 --- /dev/null +++ b/example/example-with-nested-dirs/asyncapi/send/lightTurnOn/README.md @@ -0,0 +1,32 @@ +# Light Turn On 1.0.0 documentation + + +## Operations + +### SEND `smartylighting/streetlights/1/0/action/{streetlightId}/turn/on` Operation + +* Operation ID: `turnOn` + +#### Message Turn on/off `turnOnOff` + +*Command a particular streetlight to turn the lights on or off.* + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| command | string | Whether to turn on or off the light. | allowed (`"on"`, `"off"`) | - | - | +| sentAt | string | Date and time when the message was sent. | - | format (`date-time`) | - | + +> Examples of payload _(generated)_ + +```json +{ + "command": "on", + "sentAt": "2019-08-24T14:15:22Z" +} +``` + + + diff --git a/example/example-with-nested-dirs/asyncapi/send/lightTurnOn/asyncapi.yaml b/example/example-with-nested-dirs/asyncapi/send/lightTurnOn/asyncapi.yaml new file mode 100644 index 0000000..8d0061c --- /dev/null +++ b/example/example-with-nested-dirs/asyncapi/send/lightTurnOn/asyncapi.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Light Turn On + version: 1.0.0 +channels: + lightTurnOn: + address: 'smartylighting/streetlights/1/0/action/{streetlightId}/turn/on' + messages: + turnOn: + $ref: '#/components/messages/turnOnOff' +operations: + turnOn: + action: send + channel: + $ref: '#/channels/lightTurnOn' + messages: + - $ref: '#/channels/lightTurnOn/messages/turnOn' +components: + messages: + turnOnOff: + name: turnOnOff + title: Turn on/off + summary: Command a particular streetlight to turn the lights on or off. + payload: + $ref: ./schema.json diff --git a/example/example-with-nested-dirs/asyncapi/send/lightTurnOn/schema.json b/example/example-with-nested-dirs/asyncapi/send/lightTurnOn/schema.json new file mode 100644 index 0000000..8128dfc --- /dev/null +++ b/example/example-with-nested-dirs/asyncapi/send/lightTurnOn/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "command": { + "type": "string", + "enum": [ + "on", + "off" + ], + "description": "Whether to turn on or off the light." + }, + "sentAt": { + "type": "string", + "format": "date-time", + "description": "Date and time when the message was sent." + } + } +} diff --git a/example/package.json b/example/package.json index 921d537..cf94b16 100644 --- a/example/package.json +++ b/example/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "", "main": "index.js", + "type": "module", "scripts": { "start": "npm run start:cjs && npm run start:esm && npm run start:ts", "start:cjs": "node bundle-cjs.cjs", diff --git a/src/document.ts b/src/document.ts index 5ecb5d2..2d48f6c 100644 --- a/src/document.ts +++ b/src/document.ts @@ -1,4 +1,3 @@ -import { merge } from 'lodash'; import yaml from 'js-yaml'; import type { AsyncAPIObject } from './spec-types'; @@ -8,7 +7,7 @@ import type { AsyncAPIObject } from './spec-types'; * * @example * - * const document = new Document(parsedJSONList, base); + * const document = new Document(bundledDocument); * * console.log(document.json()); // get JSON object * console.log(document.yml()); // get YAML string @@ -16,21 +15,13 @@ import type { AsyncAPIObject } from './spec-types'; */ export class Document { - private _doc: AsyncAPIObject = {} as any; + private _doc: AsyncAPIObject; /** - * - * @param {Object[]} parsedJSONList - * @param {Object} base + * @param {Object} AsyncAPIObject */ - constructor(parsedJSONList: AsyncAPIObject[], base: AsyncAPIObject) { - for (const resolvedJSON of parsedJSONList) { - this._doc = merge(this._doc, resolvedJSON); - } - - if (typeof base !== 'undefined') { - this._doc = merge(this._doc, base); - } + constructor(bundledDocument: AsyncAPIObject) { + this._doc = bundledDocument; } /** diff --git a/src/index.ts b/src/index.ts index 7b8e480..81df8ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,14 @@ -import { readFileSync } from 'fs'; import path from 'path'; -import { toJS, resolve, versionCheck } from './util'; +import { merge } from 'lodash'; +import { Parser } from '@asyncapi/parser'; +import { + resolve, + versionCheck, + orderPropsAccToAsyncAPISpec, + mergeIntoBaseFile, +} from './util'; + import { Document } from './document'; -import { parse } from './parser'; import type { AsyncAPIObject } from './spec-types'; @@ -85,6 +91,11 @@ export default async function bundle( files: string[] | string, options: any = {} ) { + let bundledDocument: any = {}; + let validationResult: any = []; + + const parser = new Parser(); + // if one string was passed, convert it to an array if (typeof files === 'string') { files = Array.from(files.split(' ')); @@ -96,34 +107,58 @@ export default async function bundle( process.chdir(path.resolve(originDir, String(options.baseDir[0]))); // guard against passing an array } - const readFiles = files.map(file => readFileSync(file, 'utf-8')); // eslint-disable-line - - const parsedJsons = readFiles.map(file => toJS(file)) as AsyncAPIObject[]; + const parsedJsons: AsyncAPIObject[] = await resolve(files, options); const majorVersion = versionCheck(parsedJsons); - if (typeof options.base !== 'undefined') { - if (typeof options.base === 'string') { - options.base = readFileSync(options.base, 'utf-8'); // eslint-disable-line - } else if (Array.isArray(options.base)) { - options.base = readFileSync(String(options.base[0]), 'utf-8'); // eslint-disable-line - } - options.base = toJS(options.base); - await parse(options.base, majorVersion, options); + for (const parsedJson of parsedJsons) { + bundledDocument = merge(bundledDocument, parsedJson); + } + + if (options.base) { + bundledDocument = await mergeIntoBaseFile( + options.base, + bundledDocument, + majorVersion, + options + ); } - const resolvedJsons: AsyncAPIObject[] = await resolve( - parsedJsons, - majorVersion, - options - ); + // Purely decorative stuff, just to bring the order of the AsyncAPI Document's + // properties into a familiar form. + bundledDocument = orderPropsAccToAsyncAPISpec(bundledDocument); + + // Option `noValidation: true` is used by the testing system, which + // intentionally feeds Bundler wrong AsyncAPI Documents, thus it is not + // documented. + if (!options.noValidation) { + validationResult = await parser.validate( + JSON.parse(JSON.stringify(bundledDocument)) + ); + } + + // If Parser's `validate()` function returns a non-empty array with at least + // one `severity: 0`, that means there was at least one error during + // validation, not a `warning: 1`, `info: 2`, or `hint: 3`. Thus, array's + // elements with `severity: 0` are outputted as a list of remarks, and the + // program throws. + if ( + validationResult.length !== 0 && + validationResult.map((element: any) => element.severity).includes(0) + ) { + console.log( + 'Validation of the resulting AsyncAPI Document failed.\nList of remarks:\n', + validationResult.filter((element: any) => element.severity === 0) + ); + throw new Error(); + } // return to the starting directory before finishing the execution if (options.baseDir) { process.chdir(originDir); } - return new Document(resolvedJsons, options.base); + return new Document(bundledDocument as AsyncAPIObject); } // 'module.exports' is added to maintain backward compatibility with Node.js diff --git a/src/parser.ts b/src/parser.ts index fcfcabe..b139ba9 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,11 +1,8 @@ import $RefParser from '@apidevtools/json-schema-ref-parser'; -import { Parser } from '@asyncapi/parser'; import type { ParserOptions as $RefParserOptions } from '@apidevtools/json-schema-ref-parser'; import type { AsyncAPIObject } from 'spec-types'; -const parser = new Parser(); - let RefParserOptions: $RefParserOptions; /** @@ -20,8 +17,6 @@ export async function parse( specVersion: number, options: any = {} ) { - let validationResult: any[] = []; - /* eslint-disable indent */ // It is assumed that there will be major Spec versions 4, 5 and on. switch (specVersion) { @@ -74,35 +69,5 @@ export async function parse( ); } - const dereferencedJSONSchema = await $RefParser.dereference( - JSONSchema, - RefParserOptions - ); - - // Option `noValidation: true` is used by the testing system, which - // intentionally feeds Bundler wrong AsyncAPI Documents, thus it is not - // documented. - if (!options.noValidation) { - validationResult = await parser.validate( - JSON.parse(JSON.stringify(dereferencedJSONSchema)) - ); - } - - // If Parser's `validate()` function returns a non-empty array with at least - // one `severity: 0`, that means there was at least one error during - // validation, not a `warning: 1`, `info: 2`, or `hint: 3`. Thus, array's - // elements with `severity: 0` are outputted as a list of remarks, and the - // program exits without doing anything further. - if ( - validationResult.length !== 0 && - validationResult.map(element => element.severity).includes(0) - ) { - console.log( - 'Validation of the resulting AsyncAPI Document failed.\nList of remarks:\n', - validationResult.filter(element => element.severity === 0) - ); - throw new Error(); - } - - return dereferencedJSONSchema; + return await $RefParser.dereference(JSONSchema, RefParserOptions) as AsyncAPIObject; } diff --git a/src/util.ts b/src/util.ts index 2f136f3..0651d67 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,7 @@ +import { readFileSync } from 'fs'; +import path from 'path'; import yaml from 'js-yaml'; +import { merge } from 'lodash'; import { parse } from './parser'; import { ParserError } from './errors'; @@ -19,7 +22,7 @@ export const toJS = (asyncapiYAMLorJSON: string | object) => { asyncapiYAMLorJSON.constructor && asyncapiYAMLorJSON.constructor.name === 'Object' ) { - return asyncapiYAMLorJSON; + return asyncapiYAMLorJSON as AsyncAPIObject; } if (typeof asyncapiYAMLorJSON !== 'string') { @@ -36,50 +39,27 @@ export const toJS = (asyncapiYAMLorJSON: string | object) => { }); } - return yaml.load(asyncapiYAMLorJSON); + return yaml.load(asyncapiYAMLorJSON) as AsyncAPIObject; }; -/** - * - * @param {Object} asyncapiDocuments - * @param {Object} options - * @param {boolean} options.xOrigin - * @returns {Array} - * @private - */ -export const resolve = async ( - asyncapiDocuments: AsyncAPIObject[], - specVersion: number, - options: any -) => { - const docs = []; - - for (const asyncapiDocument of asyncapiDocuments) { - await parse(asyncapiDocument, specVersion, options); - docs.push(asyncapiDocument); - } - - return docs; -}; - -/** - * - * @param asyncapiDocument {AsyncAPIObject} - * @returns {boolean} - */ - export function getSpecVersion(asyncapiDocument: AsyncAPIObject): number { const versionString = asyncapiDocument.asyncapi; return parseInt(versionString, 10); } +/** + * @private + * + * @param asyncapiDocuments {AsyncAPIObject[]} + * @returns {boolean} + */ export function versionCheck(asyncapiDocuments: AsyncAPIObject[]): number { let currentVersion = getSpecVersion(asyncapiDocuments[0]); for (const asyncapiDocument of asyncapiDocuments) { const majorVersion = getSpecVersion(asyncapiDocument); if (majorVersion !== currentVersion) { throw new Error( - 'Unable to bundle specification file of different major versions' + 'Unable to bundle specification files of different major versions' ); } currentVersion = majorVersion; @@ -90,3 +70,99 @@ export function versionCheck(asyncapiDocuments: AsyncAPIObject[]): number { export function isExternalReference(ref: string): boolean { return typeof ref === 'string' && !ref.startsWith('#'); } + +/** + * + * @param {string | string[]} files + * @param {Object} options + * @returns {Array} + * @private + */ +export const resolve = async (files: string | string[], options: any) => { + const parsedJsons: AsyncAPIObject[] = []; + + for (const file of files) { + const prevDir = process.cwd(); + + let filePath: any = file.split('/'); + filePath.pop(); + filePath = filePath.join('/'); + + let readFile: any = readFileSync(file, 'utf-8'); // eslint-disable-line + readFile = toJS(readFile); + + if (filePath) { + process.chdir(path.resolve(prevDir, filePath)); + } + + readFile = await parse(readFile, getSpecVersion(readFile), options); + + parsedJsons.push(readFile); + + if (prevDir) { + process.chdir(prevDir); + } + } + + return parsedJsons; +}; + +export async function mergeIntoBaseFile( + baseFilePath: string | string[], + bundledDocument: AsyncAPIObject, + majorVersion: number, + options: any = {} +) { + // The base file's path must be an array of exactly one element to be properly + // iterated in `resolve()`. Even if it was passed to the main script as a + // string or an array of several elements. + const baseFilePathAsArray: string[] = []; + + if (typeof baseFilePath === 'string') { + baseFilePathAsArray.push(baseFilePath); + } else if (Array.isArray(baseFilePath) && baseFilePath.length >= 1) { + baseFilePathAsArray.push(baseFilePath[0]); + } + + const parsedBaseFile: AsyncAPIObject[] = await resolve( + baseFilePathAsArray, + options + ); + + if (majorVersion !== getSpecVersion(parsedBaseFile[0])) { + throw new Error( + 'Base file has different major version than other specification files' + ); + } + + return merge(bundledDocument, parsedBaseFile[0]) as AsyncAPIObject; +} + +// Purely decorative stuff, just to bring the order of the AsyncAPI Document's +// properties into a familiar form. +export function orderPropsAccToAsyncAPISpec( + inputAsyncAPIObject: any +): AsyncAPIObject { + const orderOfPropsAccToAsyncAPISpec = [ + 'asyncapi', + 'id', + 'info', + 'defaultContentType', + 'servers', + 'channels', + 'operations', + 'components', + ]; + + const outputAsyncAPIObject: any = {}; + + for (const prop of orderOfPropsAccToAsyncAPISpec) { + if (inputAsyncAPIObject[`${prop}`]) { + outputAsyncAPIObject[`${prop}`] = structuredClone( + inputAsyncAPIObject[`${prop}`] + ); + } + } + + return outputAsyncAPIObject as AsyncAPIObject; +} diff --git a/tests/base-option/camera.yaml b/tests/base-option/camera.yaml index d298118..8fdf590 100644 --- a/tests/base-option/camera.yaml +++ b/tests/base-option/camera.yaml @@ -7,4 +7,4 @@ channels: camera/click: subcribe: message: - $ref: './tests/base-option/messages.yaml#/messages/clickPhoto' \ No newline at end of file + $ref: './messages.yaml#/messages/clickPhoto' \ No newline at end of file diff --git a/tests/base-option/lights.yaml b/tests/base-option/lights.yaml index 1fb1d09..26ce6d1 100644 --- a/tests/base-option/lights.yaml +++ b/tests/base-option/lights.yaml @@ -7,4 +7,4 @@ channels: lights/On: subcribe: message: - $ref: './tests/base-option/messages.yaml#/messages/LightsOn' \ No newline at end of file + $ref: './messages.yaml#/messages/LightsOn' \ No newline at end of file diff --git a/tests/lib/index.spec.ts b/tests/lib/index.spec.ts index 94257ef..29deb7e 100644 --- a/tests/lib/index.spec.ts +++ b/tests/lib/index.spec.ts @@ -51,16 +51,14 @@ describe('[integration testing] bundler should ', () => { test('should throw if external `$ref` cannot be resolved', async () => { const files = ['wrong-external-ref.yaml']; - await expect( - async () => { - await bundle(files, { - xOrigin: true, - base: 'base.yml', - baseDir: path.resolve(process.cwd(), './tests'), - noValidation: true, - }) - } - ).rejects.toThrow(JSONParserError); + await expect(async () => { + await bundle(files, { + xOrigin: true, + base: 'base.yml', + baseDir: path.resolve(process.cwd(), './tests'), + noValidation: true, + }); + }).rejects.toThrow(JSONParserError); }); test('should be able to bundle base file', async () => { @@ -71,8 +69,8 @@ describe('[integration testing] bundler should ', () => { expect( await bundle(files, { - xOrigin: true, base: path.resolve(process.cwd(), './tests/base-option/base.yaml'), + xOrigin: true, noValidation: true, }) ).resolves; @@ -84,6 +82,232 @@ describe('[integration testing] bundler should ', () => { await bundle(files, { baseDir: './tests/specfiles', noValidation: true }) ).resolves; }); + + test('should be able to bundle specification files in subdirectories and merge them into the base file', async () => { + const object = { + asyncapi: '3.0.0', + info: { + title: 'Streetlights MQTT API', + version: '1.0.0', + description: + 'The Smartylighting Streetlights API allows you to remotely manage the city lights.\n\n### Check out its awesome features:\n\n* Turn a specific streetlight on/off 🌃\n* Dim a specific streetlight 😎\n* Receive real-time information about environmental lighting conditions 📈\n', + license: { + name: 'Apache 2.0', + url: 'https://www.apache.org/licenses/LICENSE-2.0', + }, + }, + defaultContentType: 'application/json', + servers: { + production: { + host: 'test.mosquitto.org:{port}', + protocol: 'mqtt', + description: 'Test broker', + variables: { + port: { + description: + 'Secure connection (TLS) is available through port 8883.', + default: '1883', + enum: ['1883', '8883'], + }, + }, + }, + }, + channels: { + lightTurnOn: { + address: + 'smartylighting/streetlights/1/0/action/{streetlightId}/turn/on', + messages: { + turnOn: { + name: 'turnOnOff', + title: 'Turn on/off', + summary: + 'Command a particular streetlight to turn the lights on or off.', + payload: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + command: { + type: 'string', + enum: ['on', 'off'], + description: 'Whether to turn on or off the light.', + }, + sentAt: { + type: 'string', + format: 'date-time', + description: 'Date and time when the message was sent.', + }, + }, + }, + }, + }, + }, + lightTurnOff: { + address: + 'smartylighting/streetlights/1/0/action/{streetlightId}/turn/off', + messages: { + turnOff: { + name: 'turnOnOff', + title: 'Turn on/off', + summary: + 'Command a particular streetlight to turn the lights on or off.', + payload: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + command: { + type: 'string', + enum: ['on', 'off'], + description: 'Whether to turn on or off the light.', + }, + sentAt: { + type: 'string', + format: 'date-time', + description: 'Date and time when the message was sent.', + }, + }, + }, + }, + }, + }, + lightingMeasured: { + address: + 'smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured', + messages: { + lightMeasured: { + name: 'lightMeasured', + title: 'Light measured', + summary: + 'Inform about environmental lighting conditions of a particular streetlight.', + contentType: 'application/json', + payload: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + lumens: { + type: 'integer', + minimum: 0, + description: 'Light intensity measured in lumens.', + }, + sentAt: { + type: 'string', + format: 'date-time', + description: 'Date and time when the message was sent.', + }, + }, + }, + }, + }, + description: + 'The topic on which measured values may be produced and consumed.', + parameters: { + streetlightId: { + description: 'The ID of the streetlight.', + }, + }, + }, + }, + operations: { + turnOn: { + action: 'send', + channel: { + $ref: '#/channels/lightTurnOn', + }, + messages: [ + { + $ref: '#/channels/lightTurnOn/messages/turnOn', + }, + ], + }, + turnOff: { + action: 'send', + channel: { + $ref: '#/channels/lightTurnOff', + }, + messages: [ + { + $ref: '#/channels/lightTurnOff/messages/turnOff', + }, + ], + }, + receiveLightMeasurement: { + action: 'receive', + channel: { + $ref: '#/channels/lightingMeasured', + }, + summary: + 'Inform about environmental lighting conditions of a particular streetlight.', + messages: [ + { + $ref: '#/channels/lightingMeasured/messages/lightMeasured', + }, + ], + }, + }, + components: { + messages: { + turnOnOff: { + name: 'turnOnOff', + title: 'Turn on/off', + summary: + 'Command a particular streetlight to turn the lights on or off.', + payload: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + command: { + type: 'string', + enum: ['on', 'off'], + description: 'Whether to turn on or off the light.', + }, + sentAt: { + type: 'string', + format: 'date-time', + description: 'Date and time when the message was sent.', + }, + }, + }, + }, + lightMeasured: { + name: 'lightMeasured', + title: 'Light measured', + summary: + 'Inform about environmental lighting conditions of a particular streetlight.', + contentType: 'application/json', + payload: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + lumens: { + type: 'integer', + minimum: 0, + description: 'Light intensity measured in lumens.', + }, + sentAt: { + type: 'string', + format: 'date-time', + description: 'Date and time when the message was sent.', + }, + }, + }, + }, + }, + }, + }; + + const files = [ + 'asyncapi/send/lightTurnOn/asyncapi.yaml', + 'asyncapi/send/lightTurnOff/asyncapi.yaml', + 'asyncapi/receive/lightingMeasured/asyncapi.yaml', + ]; + + const document = await bundle(files, { + base: 'asyncapi/index.yaml', + baseDir: path.resolve(process.cwd(), 'tests/nested-dirs-mixed'), + noValidation: true, + }); + + expect(document.json()).toMatchObject(object); + }); }); describe('[unit testing]', () => { diff --git a/tests/nested-dirs-mixed/asyncapi/index.yaml b/tests/nested-dirs-mixed/asyncapi/index.yaml new file mode 100644 index 0000000..728830c --- /dev/null +++ b/tests/nested-dirs-mixed/asyncapi/index.yaml @@ -0,0 +1,21 @@ +asyncapi: 3.0.0 +info: + title: Streetlights MQTT API + version: 1.0.0 + description: "The Smartylighting Streetlights API allows you to remotely manage the city lights.\n\n### Check out its awesome features:\n\n* Turn a specific streetlight on/off \U0001F303\n* Dim a specific streetlight \U0001F60E\n* Receive real-time information about environmental lighting conditions \U0001F4C8\n" + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' +defaultContentType: application/json +servers: + production: + host: 'test.mosquitto.org:{port}' + protocol: mqtt + description: Test broker + variables: + port: + description: Secure connection (TLS) is available through port 8883. + default: '1883' + enum: + - '1883' + - '8883' diff --git a/tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/README.md b/tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/README.md new file mode 100644 index 0000000..56bb556 --- /dev/null +++ b/tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/README.md @@ -0,0 +1,46 @@ +# Lighting Measured 1.0.0 documentation + + +## Operations + +### RECEIVE `smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured` Operation + +*Inform about environmental lighting conditions of a particular streetlight.* + +* Operation ID: `receiveLightMeasurement` + +The topic on which measured values may be produced and consumed. + +#### Parameters + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| streetlightId | string | The ID of the streetlight. | - | - | **required** | + + +#### Message Light measured `lightMeasured` + +*Inform about environmental lighting conditions of a particular streetlight.* + +* Message ID: `lightMeasured` +* Content type: [application/json](https://www.iana.org/assignments/media-types/application/json) + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| lumens | integer | Light intensity measured in lumens. | - | >= 0 | - | +| sentAt | string | Date and time when the message was sent. | - | format (`date-time`) | - | + +> Examples of payload _(generated)_ + +```json +{ + "lumens": 0, + "sentAt": "2019-08-24T14:15:22Z" +} +``` + + + diff --git a/tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/asyncapi.yaml b/tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/asyncapi.yaml new file mode 100644 index 0000000..b465ad6 --- /dev/null +++ b/tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/asyncapi.yaml @@ -0,0 +1,35 @@ +asyncapi: 3.0.0 +info: + title: Lighting Measured + version: 1.0.0 +channels: + lightingMeasured: + address: 'smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured' + messages: + lightMeasured: + $ref: '#/components/messages/lightMeasured' + description: The topic on which measured values may be produced and consumed. + parameters: + streetlightId: + description: The ID of the streetlight. +operations: + receiveLightMeasurement: + action: receive + channel: + $ref: '#/channels/lightingMeasured' + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + messages: + - $ref: '#/channels/lightingMeasured/messages/lightMeasured' +components: + messages: + lightMeasured: + name: lightMeasured + title: Light measured + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + contentType: application/json + payload: + $ref: ./schema.json diff --git a/tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/schema.json b/tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/schema.json new file mode 100644 index 0000000..6207762 --- /dev/null +++ b/tests/nested-dirs-mixed/asyncapi/receive/lightingMeasured/schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "lumens": { + "type": "integer", + "minimum": 0, + "description": "Light intensity measured in lumens." + }, + "sentAt": { + "type": "string", + "format": "date-time", + "description": "Date and time when the message was sent." + } + } +} diff --git a/tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/README.md b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/README.md new file mode 100644 index 0000000..4896f61 --- /dev/null +++ b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/README.md @@ -0,0 +1,32 @@ +# Light Turn Off 1.0.0 documentation + + +## Operations + +### SEND `smartylighting/streetlights/1/0/action/{streetlightId}/turn/off` Operation + +* Operation ID: `turnOff` + +#### Message Turn on/off `turnOnOff` + +*Command a particular streetlight to turn the lights on or off.* + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| command | string | Whether to turn on or off the light. | allowed (`"on"`, `"off"`) | - | - | +| sentAt | string | Date and time when the message was sent. | - | format (`date-time`) | - | + +> Examples of payload _(generated)_ + +```json +{ + "command": "on", + "sentAt": "2019-08-24T14:15:22Z" +} +``` + + + diff --git a/tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/asyncapi.yaml b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/asyncapi.yaml new file mode 100644 index 0000000..5c8566f --- /dev/null +++ b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/asyncapi.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Light Turn Off + version: 1.0.0 +channels: + lightTurnOff: + address: 'smartylighting/streetlights/1/0/action/{streetlightId}/turn/off' + messages: + turnOff: + $ref: '#/components/messages/turnOnOff' +operations: + turnOff: + action: send + channel: + $ref: '#/channels/lightTurnOff' + messages: + - $ref: '#/channels/lightTurnOff/messages/turnOff' +components: + messages: + turnOnOff: + name: turnOnOff + title: Turn on/off + summary: Command a particular streetlight to turn the lights on or off. + payload: + $ref: ./schema.json diff --git a/tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/schema.json b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/schema.json new file mode 100644 index 0000000..65804f3 --- /dev/null +++ b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOff/schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "command": { + "type": "string", + "enum": [ + "on", + "off" + ], + "description": "Whether to turn on or off the light." + }, + "sentAt": { + "type": "string", + "format": "date-time", + "description": "Date and time when the message was sent." + } + } + } + \ No newline at end of file diff --git a/tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/README.md b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/README.md new file mode 100644 index 0000000..5b0c2a1 --- /dev/null +++ b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/README.md @@ -0,0 +1,32 @@ +# Light Turn On 1.0.0 documentation + + +## Operations + +### SEND `smartylighting/streetlights/1/0/action/{streetlightId}/turn/on` Operation + +* Operation ID: `turnOn` + +#### Message Turn on/off `turnOnOff` + +*Command a particular streetlight to turn the lights on or off.* + +##### Payload + +| Name | Type | Description | Value | Constraints | Notes | +|---|---|---|---|---|---| +| (root) | object | - | - | - | **additional properties are allowed** | +| command | string | Whether to turn on or off the light. | allowed (`"on"`, `"off"`) | - | - | +| sentAt | string | Date and time when the message was sent. | - | format (`date-time`) | - | + +> Examples of payload _(generated)_ + +```json +{ + "command": "on", + "sentAt": "2019-08-24T14:15:22Z" +} +``` + + + diff --git a/tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/asyncapi.yaml b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/asyncapi.yaml new file mode 100644 index 0000000..8d0061c --- /dev/null +++ b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/asyncapi.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Light Turn On + version: 1.0.0 +channels: + lightTurnOn: + address: 'smartylighting/streetlights/1/0/action/{streetlightId}/turn/on' + messages: + turnOn: + $ref: '#/components/messages/turnOnOff' +operations: + turnOn: + action: send + channel: + $ref: '#/channels/lightTurnOn' + messages: + - $ref: '#/channels/lightTurnOn/messages/turnOn' +components: + messages: + turnOnOff: + name: turnOnOff + title: Turn on/off + summary: Command a particular streetlight to turn the lights on or off. + payload: + $ref: ./schema.json diff --git a/tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/schema.json b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/schema.json new file mode 100644 index 0000000..8128dfc --- /dev/null +++ b/tests/nested-dirs-mixed/asyncapi/send/lightTurnOn/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "command": { + "type": "string", + "enum": [ + "on", + "off" + ], + "description": "Whether to turn on or off the light." + }, + "sentAt": { + "type": "string", + "format": "date-time", + "description": "Date and time when the message was sent." + } + } +}