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 d0cc16e
Show file tree
Hide file tree
Showing 12 changed files with 183 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
62 changes: 62 additions & 0 deletions example/bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var fs_1 = require("fs");
var bundler_1 = __importDefault(require("@asyncapi/bundler"));
function main() {
return __awaiter(this, void 0, void 0, function () {
var document;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, (0, bundler_1.default)([(0, fs_1.readFileSync)('./main.yaml', 'utf-8')], {
referenceIntoComponents: false,
})];
case 1:
document = _a.sent();
if (document.yml()) {
(0, fs_1.writeFileSync)('asyncapi.yaml', document.yml());
}
return [2 /*return*/];
}
});
});
}
main().catch(function (e) { return console.error(e); });
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
Loading

0 comments on commit d0cc16e

Please sign in to comment.