Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add migration function from v2 to v1 API #607

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 54 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ Use this package to validate and parse AsyncAPI documents —either YAML or JSON

![npm](https://img.shields.io/npm/v/@asyncapi/parser?style=for-the-badge) ![npm](https://img.shields.io/npm/dt/@asyncapi/parser?style=for-the-badge)

> :warning: This package doesn't support AsyncAPI 1.x anymore. We recommend to upgrade to the latest AsyncAPI version using the [AsyncAPI converter](https://github.com/asyncapi/converter-js). If you need to convert documents on the fly, you may use the [Node.js](https://github.com/asyncapi/converter-js) or [Go](https://github.com/asyncapi/converter-go) converters.
> **Warning**
> This package doesn't support AsyncAPI 1.x anymore. We recommend to upgrade to the latest AsyncAPI version using the [AsyncAPI converter](https://github.com/asyncapi/converter-js). If you need to convert documents on the fly, you may use the [Node.js](https://github.com/asyncapi/converter-js) or [Go](https://github.com/asyncapi/converter-go) converters.

> **Warning**
> This package has rewrited Model API (old one) to [Intent API](https://github.com/asyncapi/parser-api). If you still need to use the old API, read the [Convert to the old API](#convert-to-the-old-api) section.

<!-- toc is generated with GitHub Actions do not remove toc markers -->

Expand All @@ -19,12 +23,15 @@ Use this package to validate and parse AsyncAPI documents —either YAML or JSON
* [Example using RAML data types](#example-using-raml-data-types)
* [Example with stringify and unstringify parsed document](#example-with-stringify-and-unstringify-parsed-document)
- [API documentation](#api-documentation)
- [Using in the browser](#using-in-the-browser)
- [Using in the browser/SPA applications](#using-in-the-browserspa-applications)
- [Custom schema parsers](#custom-schema-parsers)
* [Official supported custom schema parsers](#official-supported-custom-schema-parsers)
- [Custom extensions](#custom-extensions)
- [Circular references](#circular-references)
- [Stringify](#stringify)
- [Convert to the old API](#convert-to-the-old-api)
- [Bundler configuration](#bundler-configuration)
* [Webpack](#webpack)
- [Develop](#develop)
- [Contributing](#contributing)
- [Contributors](#contributors)
Expand Down Expand Up @@ -176,7 +183,7 @@ Direct access to the parsed JSON document is always available through the `doc.j

See [API documentation](./docs/api.md) for more examples and full API reference information.

## Using in the browser/SPA apps
## Using in the browser/SPA applications

The package contains a built-in version of the parser. To use it, you need to import the parser into the HTML file as below:

Expand All @@ -185,7 +192,7 @@ The package contains a built-in version of the parser. To use it, you need to im

<script>
const parser = new window.AsyncAPIParser();
const { parsed, diagnostics } = parser.parse(...);
const { document, diagnostics } = parser.parse(...);
</script>
```

Expand All @@ -195,19 +202,19 @@ Or, if you want to use the parser in a JS SPA-type application where you have a
import Parser from '@asyncapi/parser/browser';

const parser = new Parser();
const { parsed, diagnostics } = parser.parse(...);
const { document, diagnostics } = parser.parse(...);
```

> **Note**
> Using the above code, we import the entire bundled parser into application. This may result in duplicate code in the final application bundle, only if the application uses the same dependencies what the parser. If, on the other hand, you want to have the smallest bundle possible, we recommend using the following import and properly configure bundler.
> Using the above code, we import the entire bundled parser into application. This may result in a duplicate code in the final application bundle, only if the application uses the same dependencies what the parser. If, on the other hand, you want to have the smallest bundle as possible, we recommend using the following import and properly configure bundler.

Otherwise, if your application is bundled via bundlers like `webpack` and you can configure it, you can import the parser like a regular package:

```js
import { Parser } from '@asyncapi/parser';

const parser = new Parser();
const { parsed, diagnostics } = parser.parse(...);
const { document, diagnostics } = parser.parse(...);
```

> **Note**
Expand Down Expand Up @@ -252,38 +259,55 @@ AsyncAPI doesn't enforce one schema format. The payload of the messages can be d

In AsyncAPI Initiative we support below custom schema parsers. To install them, run below comamnds:

- Avro schema:
- [Avro schema](https://www.github.com/asyncapi/avro-schema-parser):

```bash
npm install @asyncapi/avro-schema-parser
yarn add @asyncapi/avro-schema-parser
```

- OpenAPI (3.0.0) Schema Object:
- [OpenAPI (3.0.0) Schema Object](https://www.github.com/asyncapi/openapi-schema-parser):

```bash
npm install @asyncapi/openapi-schema-parser
yarn add @asyncapi/openapi-schema-parser
```

- RAML data type:
- [RAML data type](https://www.github.com/asyncapi/raml-dt-schema-parser):

```bash
npm install @asyncapi/raml-dt-schema-parser
yarn add @asyncapi/raml-dt-schema-parser
```

> **NOTE**: That custom parser works only in the NodeJS environment. Do not use it in browser applications!
> **Note**
> That custom parser works only in the NodeJS environment. Do not use it in browser applications!

## Custom extensions

TBD
The parser uses custom extensions to define additional information about the spec. Each has a different purpose but all of them are there to make it much easier to work with the AsyncAPI document. These extensions are prefixed with `x-parser-`. The following extensions are used:

- `x-parser-spec-parsed` is used to specify if the AsyncAPI document is already parsed by the parser. Property `x-parser-spec-parsed` is added to the root of the document with the `true` value.
- `x-parser-message-name` is used to specify the name of the message if it is not provided. For messages without names, the parser generates anonymous names. Property `x-parser-message-name` is added to a message object with a value that follows this pattern: `<anonymous-message-${number}>`. This value is returned by `message.id()` (`message.uid()` in the [old API](#convert-to-the-old-api)) when regular `name` property is not present.
- `x-parser-original-payload` holds the original payload of the message. You can use different formats for payloads with the AsyncAPI documents and the parser converts them to. For example, it converts payload described with Avro schema to AsyncAPI schema. The original payload is preserved in the extension.
- [`x-parser-circular`](#circular-references).

In addition, the [`migrateToOldAPI()` function](#convert-to-the-old-api) which converts new API to an old one adds additional extensions:

- `x-parser-message-parsed` is used to specify if the message is already parsed by the message parser. Property `x-parser-message-parsed` is added to the message object with the `true` value.
- `x-parser-schema-id` is used to specify the ID of the schema if it is not provided. For schemas without IDs, the parser generates anonymous names. Property `x-parser-schema-id` is added to every object of a schema with a value that follows this pattern: `<anonymous-schema-${number}>`. This value is returned by `schema.uid()` when regular `$id` property is not present.
- `x-parser-original-traits` is where traits are stored after they are applied on the AsyncAPI document. The reason is because the original `traits` property is removed.
- `x-parser-original-schema-format` holds information about the original schema format of the payload. You can use different schema formats with the AsyncAPI documents and the parser converts them to AsyncAPI schema. This is why different schema format is set, and the original one is preserved in the extension.

> **NOTE**: All extensions added by the parser (including all properties) should be retrieved using special functions. Names of extensions and their location may change, and their eventual changes will not be announced.
> **Warning**
> All extensions added by the parser (including all properties) should be retrieved using special functions. Names of extensions and their location may change, and their eventual changes will not be announced.

## Circular references

TBD
Parser dereferences all circular references by default. In addition, to simplify interactions with the parser, the following is added:

- `x-parser-circular` property is added to the root of the AsyncAPI document to indicate that the document contains circular references. In old API the Parser exposes `hasCircular()` function to check if given AsyncAPI document has circular references.
- `isCircular()` function is added to the [Schema Model](./src/models/schema.ts) to determine if a given schema is circular with respect to previously occurring schemas in the tree.

## Stringify

Expand All @@ -300,11 +324,27 @@ For that, the Parser supports the ability to stringify a parsed AsyncAPI documen
To parse a stringified document into an AsyncAPIDocument instance, you must use the `unstringify` function (also exposed by package). It isn't compatible with the native `JSON.parse()` method. It replaces the given references pointed by the [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) path, with an `$ref:` prefix to the original objects.

A few advantages of this solution:

- The string remains as small as possible due to the use of [JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901).
- All references (also circular) are preserved.

Check [example](#example-with-stringify-and-unstringify-parsed-documentstringify).

## Convert to the old API

Version `2.0.0` of package introduced a lot of breaking changes, including changing the API of the returned parsed document (parser uses [New API](https://github.com/asyncapi/parser-api)). Due to the fact that a large part of the AsyncAPI tooling ecosystem uses a Parser with the old API and rewriting the tool for the new one can be time-consuming and difficult, the package exposes the `migrateToOldAPI()` function to convert new API to old one:

```js
import { Parser, migrateToOldAPI } from '@asyncapi/parser';

const parser = new Parser();
const { document } = parser.parse(...);
const oldAsyncAPIDocument = migrateToOldAPI(document);
magicmatatjahu marked this conversation as resolved.
Show resolved Hide resolved
```

> **Note**
> The old api will be supported only for a certain period of time. The target date for turning off support of the old API is around the end of January 2023.

## Bundler configuration

### Webpack
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const xParserSpecParsed = 'x-parser-spec-parsed';
export const xParserSpecStringified = 'x-parser-spec-stringified';

export const xParserMessageName = 'x-parser-message-name';
export const xParserMessageParsed = 'x-parser-message-parsed';
export const xParserSchemaId = 'x-parser-schema-id';

export const xParserOriginalSchemaFormat = 'x-parser-original-schema-format';
Expand Down
71 changes: 71 additions & 0 deletions src/custom-operations/anonymous-naming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { xParserMessageName, xParserSchemaId } from '../constants';
import { traverseAsyncApiDocument } from '../iterator';
import { setExtension } from '../utils';

import type {
AsyncAPIDocumentInterface,
SchemaInterface
} from '../models';

export function anonymousNaming(document: AsyncAPIDocumentInterface) {
assignNameToComponentMessages(document);
assignNameToAnonymousMessages(document);

assignUidToComponentSchemas(document);
assignUidToComponentParameterSchemas(document);
assignUidToChannelParameterSchemas(document);
assignUidToAnonymousSchemas(document);
}

function assignNameToComponentMessages(document: AsyncAPIDocumentInterface) {
document.components().messages().forEach(message => {
if (message.name() === undefined) {
setExtension(xParserMessageName, message.id(), message);
}
});
}

function assignNameToAnonymousMessages(document: AsyncAPIDocumentInterface) {
let anonymousMessageCounter = 0;
document.messages().forEach(message => {
if (message.name() === undefined && message.extensions().get(xParserMessageName) === undefined) {
setExtension(xParserMessageName, `<anonymous-message-${++anonymousMessageCounter}>`, message);
}
});
}

function assignUidToComponentParameterSchemas(document: AsyncAPIDocumentInterface) {
document.components().channelParameters().forEach(parameter => {
const schema = parameter.schema();
if (schema && !schema.uid()) {
setExtension(xParserSchemaId, parameter.id(), schema);
}
});
}

function assignUidToChannelParameterSchemas(document: AsyncAPIDocumentInterface) {
document.channels().forEach(channel => {
channel.parameters().forEach(parameter => {
const schema = parameter.schema();
if (schema && !schema.uid()) {
setExtension(xParserSchemaId, parameter.id(), schema);
}
});
});
}

function assignUidToComponentSchemas(document: AsyncAPIDocumentInterface) {
document.components().schemas().forEach(schema => {
setExtension(xParserSchemaId, schema.uid(), schema);
});
}

function assignUidToAnonymousSchemas(doc: AsyncAPIDocumentInterface) {
let anonymousSchemaCounter = 0;
function callback(schema: SchemaInterface) {
if (!schema.uid()) {
setExtension(xParserSchemaId, `<anonymous-schema-${++anonymousSchemaCounter}>`, schema);
}
}
traverseAsyncApiDocument(doc, callback);
}
4 changes: 0 additions & 4 deletions src/custom-operations/apply-traits.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { JSONPath } from 'jsonpath-plus';

import { xParserOriginalTraits } from '../constants';
import { mergePatch } from '../utils';

import type { v2 } from '../spec-types';
Expand Down Expand Up @@ -55,8 +54,5 @@ function applyTraits(value: Record<string, unknown>) {
value[String(key)] = mergePatch(value[String(key)], trait[String(key)]);
}
}

value[xParserOriginalTraits] = value.traits;
delete value.traits;
}
}
24 changes: 24 additions & 0 deletions src/custom-operations/check-circular-refs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { setExtension } from '../utils';
import { xParserCircular } from '../constants';

import type { AsyncAPIDocumentInterface } from '../models';

export function checkCircularRefs(document: AsyncAPIDocumentInterface) {
if (hasInlineRef(document.json())) {
setExtension(xParserCircular, true, document);
}
}

function hasInlineRef(data: Record<string, any>): boolean {
if (data && typeof data === 'object' && !Array.isArray(data)) {
if (Object.prototype.hasOwnProperty.call(data, '$ref')) {
return true;
}
for (const p in data) {
if (hasInlineRef(data[p])) {
return true;
}
}
}
return false;
}
21 changes: 11 additions & 10 deletions src/custom-operations/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { applyTraitsV2, applyTraitsV3 } from './apply-traits';
import { applyTraitsV2 } from './apply-traits';
import { checkCircularRefs } from './check-circular-refs';
import { parseSchemasV2 } from './parse-schema';
import { anonymousNaming } from './anonymous-naming';

import type { Parser } from '../parser';
import type { ParseOptions } from '../parse';
import type { AsyncAPIDocumentInterface } from '../models';
import type { DetailedAsyncAPI } from '../types';

export async function customOperations(parser: Parser, detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
export async function customOperations(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
switch (detailed.semver.major) {
case 2: return operationsV2(parser, detailed, options);
case 3: return operationsV3(parser, detailed, options);
case 2: return operationsV2(parser, document, detailed, options);
// case 3: return operationsV3(parser, document, detailed, options);
}
}

async function operationsV2(parser: Parser, detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
async function operationsV2(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
checkCircularRefs(document);
anonymousNaming(document);

if (options.applyTraits) {
applyTraitsV2(detailed.parsed);
}
Expand All @@ -21,8 +27,3 @@ async function operationsV2(parser: Parser, detailed: DetailedAsyncAPI, options:
}
}

async function operationsV3(parser: Parser, detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
if (options.applyTraits) {
applyTraitsV3(detailed.parsed);
}
}
10 changes: 7 additions & 3 deletions src/custom-operations/parse-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,15 @@ export async function parseSchemasV2(parser: Parser, detailed: DetailedAsyncAPI)
}

async function parseSchemaV2(parser: Parser, item: ToParseItem) {
item.value[xParserOriginalPayload] = item.input.data;
item.value.payload = await parseSchema(parser, item.input);
const originalData = item.input.data;
const parsedData = item.value.payload = await parseSchema(parser, item.input);
// save original payload only when data is different (returned by custom parsers)
if (originalData !== parsedData) {
item.value[xParserOriginalPayload] = originalData;
}
}

function splitPath(path: string): string[] {
// remove $[' from beginning and '] at the end and split by ']['
return path.slice(3).slice(0, -2).split('\'][\'');
}
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export { Parser };
export { stringify, unstringify } from './stringify';
export { AsyncAPISchemaParser } from './schema-parser/asyncapi-schema-parser';

export { AsyncAPIDocument as OldAsyncAPIDocument } from './old-api/asyncapi';
export { migrateToOldAPI } from './old-api/migrator';

export type { AsyncAPISemver, Diagnostic, SchemaValidateResult } from './types';
export type { LintOptions, ValidateOptions, ValidateOutput } from './lint';
export type { ParseInput, ParseOptions, ParseOutput } from './parse';
Expand Down
Loading