diff --git a/asyncapi-parser-2.0.0.tgz b/asyncapi-parser-2.0.0.tgz new file mode 100644 index 000000000..3e05145f7 Binary files /dev/null and b/asyncapi-parser-2.0.0.tgz differ diff --git a/src/custom-operations/anonymous-naming.ts b/src/custom-operations/anonymous-naming.ts index 42c1b3655..12506cf1f 100644 --- a/src/custom-operations/anonymous-naming.ts +++ b/src/custom-operations/anonymous-naming.ts @@ -29,7 +29,7 @@ function assignNameToAnonymousMessages(document: AsyncAPIDocumentInterface) { let anonymousMessageCounter = 0; document.messages().forEach(message => { if (message.name() === undefined && message.extensions().get(xParserMessageName)?.value() === undefined) { - setExtension(xParserMessageName, ``, message); + setExtension(xParserMessageName, message.id() || ``, message); } }); } diff --git a/src/custom-operations/apply-traits.ts b/src/custom-operations/apply-traits.ts index cc6191857..7ca2730d6 100644 --- a/src/custom-operations/apply-traits.ts +++ b/src/custom-operations/apply-traits.ts @@ -37,17 +37,24 @@ export function applyTraitsV3(asyncapi: v2.AsyncAPIObject) { // TODO: Change typ } function applyAllTraits(asyncapi: Record, paths: string[]) { + const visited: Set = new Set(); paths.forEach(path => { JSONPath({ path, json: asyncapi, resultType: 'value', - callback(value) { applyTraits(value); }, + callback(value) { + if (visited.has(value)) { + return; + } + visited.add(value); + applyTraits(value); + }, }); }); } -function applyTraits(value: Record) { +function applyTraits(value: Record & { traits?: any[] }) { if (Array.isArray(value.traits)) { for (const trait of value.traits) { for (const key in trait) { diff --git a/src/custom-operations/index.ts b/src/custom-operations/index.ts index 5bf91a714..8303eafc1 100644 --- a/src/custom-operations/index.ts +++ b/src/custom-operations/index.ts @@ -16,14 +16,14 @@ export async function customOperations(parser: Parser, document: AsyncAPIDocumen } async function operationsV2(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, options: ParseOptions): Promise { - checkCircularRefs(document); - anonymousNaming(document); - if (options.applyTraits) { applyTraitsV2(detailed.parsed); } if (options.parseSchemas) { await parseSchemasV2(parser, detailed); } + + checkCircularRefs(document); + anonymousNaming(document); } diff --git a/src/utils.ts b/src/utils.ts index 1b218dbb8..4e96780fb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -86,6 +86,10 @@ export function hasRef(value: unknown): value is { $ref: string } { return isObject(value) && '$ref' in value && typeof value.$ref === 'string'; } +export function toJSONPathArray(jsonPath: string): Array { + return jsonPath.split('/').map(untilde); +} + export function tilde(str: string) { return str.replace(/[~/]{1}/g, (sub) => { switch (sub) { diff --git a/test/custom-operations/anonymous-naming.spec.ts b/test/custom-operations/anonymous-naming.spec.ts index cbd92d923..453ebf58a 100644 --- a/test/custom-operations/anonymous-naming.spec.ts +++ b/test/custom-operations/anonymous-naming.spec.ts @@ -57,6 +57,37 @@ describe('custom operations - anonymous naming', function() { expect(document?.components().messages()[0].extensions().get(xParserMessageName)?.value()).toEqual('message'); }); + it('should try use messageId for x-parser-message-name', async function() { + const { document } = await parser.parse({ + asyncapi: '2.4.0', + info: { + title: 'Valid AsyncApi document', + version: '1.0', + }, + channels: { + channel: { + publish: { + operationId: 'operation', + message: { + $ref: '#/components/messages/message' + } + } + } + }, + components: { + messages: { + message: { + messageId: 'someId', + payload: {} + } + } + } + }); + + expect(document?.messages()).toHaveLength(1); + expect(document?.messages()[0].extensions().get(xParserMessageName)?.value()).toEqual('someId'); + }); + it('should not override x-parser-message-name if it exists', async function() { const { document } = await parser.parse({ asyncapi: '2.0.0', diff --git a/test/custom-operations/apply-traits.spec.ts b/test/custom-operations/apply-traits.spec.ts new file mode 100644 index 000000000..ae212a829 --- /dev/null +++ b/test/custom-operations/apply-traits.spec.ts @@ -0,0 +1,186 @@ +import { AsyncAPIDocumentV2 } from '../../src/models'; +import { Parser } from '../../src/parser'; + +import type { v2 } from '../../src/spec-types'; + +describe('custom operations - apply traits', function() { + const parser = new Parser(); + + it('should apply traits to operations', async function() { + const documentRaw = { + asyncapi: '2.0.0', + info: { + title: 'Valid AsyncApi document', + version: '1.0', + }, + channels: { + channel: { + publish: { + operationId: 'publishId', + traits: [ + { + operationId: 'anotherPubId', + description: 'some description' + }, + { + description: 'another description' + } + ] + }, + subscribe: { + operationId: 'subscribeId', + traits: [ + { + operationId: 'anotherSubId', + description: 'some description' + }, + { + description: 'another description' + } + ] + } + } + } + }; + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV2); + expect(diagnostics.length > 0).toEqual(true); + + const publish = document?.json()?.channels?.channel?.publish; + delete publish?.traits; + expect(publish).toEqual({ operationId: 'anotherPubId', description: 'another description' }); + + const subscribe = document?.json()?.channels?.channel?.subscribe; + delete subscribe?.traits; + expect(subscribe).toEqual({ operationId: 'anotherSubId', description: 'another description' }); + }); + + it('should apply traits to messages', async function() { + const documentRaw = { + asyncapi: '2.4.0', + info: { + title: 'Valid AsyncApi document', + version: '1.0', + }, + channels: { + channel: { + publish: { + operationId: 'operationId', + message: { + messageId: 'messageId', + traits: [ + { + messageId: 'anotherMessageId1', + description: 'some description' + }, + { + description: 'another description' + } + ] + } + }, + subscribe: { + message: { + oneOf: [ + { + messageId: 'messageId', + traits: [ + { + messageId: 'anotherMessageId2', + description: 'some description' + }, + { + description: 'another description' + } + ] + }, + { + messageId: 'messageId', + traits: [ + { + messageId: 'anotherId', + description: 'some description' + }, + { + description: 'another description' + }, + { + messageId: 'anotherMessageId3', + description: 'simple description' + } + ] + } + ], + } + } + } + } + }; + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV2); + expect(diagnostics.length > 0).toEqual(true); + + const message = document?.json()?.channels?.channel?.publish?.message; + delete (message as v2.MessageObject)?.traits; + expect(message).toEqual({ messageId: 'anotherMessageId1', description: 'another description', 'x-parser-message-name': 'anotherMessageId1' }); + + const messageOneOf1 = (document?.json()?.channels?.channel?.subscribe?.message as { oneOf: Array }).oneOf[0]; + delete messageOneOf1?.traits; + expect(messageOneOf1).toEqual({ messageId: 'anotherMessageId2', description: 'another description', 'x-parser-message-name': 'anotherMessageId2' }); + + const messageOneOf2 = (document?.json()?.channels?.channel?.subscribe?.message as { oneOf: Array }).oneOf[1]; + delete messageOneOf2?.traits; + expect(messageOneOf2).toEqual({ messageId: 'anotherMessageId3', description: 'simple description', 'x-parser-message-name': 'anotherMessageId3' }); + }); + + it('should preserve this same references', async function() { + const documentRaw = { + asyncapi: '2.4.0', + info: { + title: 'Valid AsyncApi document', + version: '1.0', + }, + channels: { + channel: { + publish: { + operationId: 'publishId', + message: { + $ref: '#/components/messages/message', + } + }, + } + }, + components: { + messages: { + message: { + messageId: 'messageId', + traits: [ + { + messageId: 'anotherId', + description: 'some description' + }, + { + description: 'another description' + }, + { + messageId: 'anotherMessageId3', + description: 'simple description' + } + ] + } + } + } + }; + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV2); + expect(diagnostics.length > 0).toEqual(true); + + const message = document?.json()?.channels?.channel?.publish?.message; + delete (message as v2.MessageObject)?.traits; + expect(message).toEqual({ messageId: 'anotherMessageId3', description: 'simple description', 'x-parser-message-name': 'anotherMessageId3' }); + expect(message === document?.json()?.components?.messages?.message).toEqual(true); + }); +}); diff --git a/test/custom-operations/parse-schema.spec.ts b/test/custom-operations/parse-schema.spec.ts index 5461e338c..dfd71f293 100644 --- a/test/custom-operations/parse-schema.spec.ts +++ b/test/custom-operations/parse-schema.spec.ts @@ -63,6 +63,44 @@ describe('custom operations - parse schemas', function() { expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); }); + it('should preserve this same references', async function() { + const documentRaw = { + asyncapi: '2.0.0', + info: { + title: 'Valid AsyncApi document', + version: '1.0', + }, + channels: { + channel: { + publish: { + operationId: 'operationId', + message: { + $ref: '#/components/messages/message' + } + } + } + }, + components: { + messages: { + message: { + payload: { + type: 'object', + } + } + } + } + }; + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV2); + expect(diagnostics.length > 0).toEqual(true); + + expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + expect((document?.json().components?.messages?.message as v2.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + // check if logic preserves references + expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload === (document?.json().components?.messages?.message as v2.MessageObject)?.payload).toEqual(true); + }); + it('should parse invalid schema format', async function() { const documentRaw = { asyncapi: '2.0.0', diff --git a/test/from.spec.ts b/test/from.spec.ts index 0105deb75..3d53f5184 100644 --- a/test/from.spec.ts +++ b/test/from.spec.ts @@ -8,7 +8,7 @@ describe('fromURL() & fromFile()', function() { describe('fromURL()', function() { it('should operate on existing HTTP source', async function() { - const { document, diagnostics } = await fromURL(parser, 'https://raw.githubusercontent.com/asyncapi/spec/master/examples/simple.yml').parse(); + const { document, diagnostics } = await fromURL(parser, 'https://raw.githubusercontent.com/asyncapi/spec/v2.0.0/examples/2.0.0/gitter-streaming.yml').parse(); expect(document).not.toBeUndefined(); expect(diagnostics.length > 0).toEqual(true); expect(hasWarningDiagnostic(diagnostics)).toEqual(true);