Skip to content

Commit

Permalink
feat: add x-origin property
Browse files Browse the repository at this point in the history
  • Loading branch information
aeworxet committed Apr 6, 2024
1 parent eab6e89 commit 92b449e
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 126 deletions.
9 changes: 4 additions & 5 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ console.log(document.string()); // get JSON string
| files | <code>Array.&lt;string&gt;</code> | <p>Array of stringified AsyncAPI documents in YAML format, that are to be bundled (or array of filepaths, resolved and passed via <code>Array.map()</code> and <code>fs.readFileSync</code>, which is the same, see <code>README.md</code>).</p> |
| [options] | <code>Object</code> | |
| [options.base] | <code>string</code> \| <code>object</code> | <p>Base object whose properties will be retained.</p> |
| [options.referenceIntoComponents] | <code>boolean</code> | <p>Pass <code>true</code> to resolve external references to components.</p> |
| [options.baseDir] | <code>string</code> | <p>Pass folder path to</p> |
| [options.xOrigin] | <code>boolean</code> | <p>Pass <code>true</code> to generate properties <code>x-origin</code> that will contain historical values of dereferenced <code>$ref</code>s.</p> |

**Example**
**TypeScript**
Expand All @@ -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
Expand All @@ -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());
}
Expand All @@ -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());
}
Expand Down
139 changes: 56 additions & 83 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<!-- tocstop -->

## 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:

<details>
<summary>your specification file is divided into different smaller files and is using JSON `$ref` property to reference components </summary>
Expand Down Expand Up @@ -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

<details>
<summary>For example</summary>
`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:

```
</details>
<br />
/#\/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
Expand All @@ -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
Expand All @@ -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());
}
Expand All @@ -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());
}
Expand All @@ -320,6 +291,7 @@ main().catch(e => console.error(e));

```


<a name="bundle"></a>

## bundle(files, [options])
Expand All @@ -330,7 +302,8 @@ main().catch(e => console.error(e));
| files | <code>Array.&lt;string&gt; | 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] | <code>Object</code> | |
| [options.base] | <code>string</code> \| <code>object</code> | Base object whose properties will be retained. |
| [options.referenceIntoComponents] | <code>boolean<code> | Pass `true` to resolve external references to components. |
| [options.xOrigin] | <code>boolean</code> | <p>Pass <code>true</code> to generate properties <code>x-origin</code> that will contain historical values of dereferenced <code>$ref</code>s.</p> |


## Contributors

Expand Down
4 changes: 2 additions & 2 deletions example/bundle-cjs.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
10 changes: 7 additions & 3 deletions example/bundle-cjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
2 changes: 1 addition & 1 deletion example/bundle-esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion example/bundle-esm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion example/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
22 changes: 11 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { toJS, resolve, versionCheck } from './util';
import { Document } from './document';
import { parse } from './parser';

import type { AsyncAPIObject } from './spec-types';

Expand All @@ -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}
*
Expand All @@ -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
Expand All @@ -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());
* }
Expand All @@ -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());
* }
Expand All @@ -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);
Expand Down
8 changes: 5 additions & 3 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,20 @@ 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) {
case 2:
RefParserOptions = {
dereference: {
circular: false, // prettier-ignore
excludedPathMatcher: (path: string): any => { // eslint-disable-line
excludedPathMatcher: (path: string): any => {

Check warning on line 32 in src/parser.ts

View workflow job for this annotation

GitHub Actions / Test NodeJS PR - ubuntu-latest

'path' is defined but never used
// eslint-disable-line
return;
},
onDereference: (path: string, value: AsyncAPIObject) => {
if (options.xOrigin) {
if (options.xOrigin === true) {
value['x-origin'] = path;
}
},
Expand All @@ -59,7 +61,7 @@ export async function parse(
);
},
onDereference: (path: string, value: AsyncAPIObject) => {
if (options.xOrigin) {
if (options.xOrigin === true) {
value['x-origin'] = path;
}
},
Expand Down
2 changes: 1 addition & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object>}
* @private
*/
Expand Down
Loading

0 comments on commit 92b449e

Please sign in to comment.