From 41ddb9d05de45cd6a3d3eb834155ed1f42b736ae Mon Sep 17 00:00:00 2001 From: Jonas Lagoni Date: Fri, 16 Feb 2024 17:44:05 +0100 Subject: [PATCH] fix: iterating between operations, channels and messages (#948) * fix traits * update test and implementation --- src/custom-operations/apply-unique-ids.ts | 35 ++++-- src/parse.ts | 5 +- .../custom-operations/apply-traits-v3.spec.ts | 102 ++++++++++++------ .../apply-unique-ids.spec.ts | 36 +++++++ 4 files changed, 130 insertions(+), 48 deletions(-) diff --git a/src/custom-operations/apply-unique-ids.ts b/src/custom-operations/apply-unique-ids.ts index d9804235a..025bc49bc 100644 --- a/src/custom-operations/apply-unique-ids.ts +++ b/src/custom-operations/apply-unique-ids.ts @@ -3,23 +3,36 @@ import { xParserObjectUniqueId } from '../constants'; /** * This function applies unique ids for objects whose key's function as ids, ensuring that the key is part of the value. * - * For v3; Apply unique ids to channel's, and message's + * For v3; Apply unique ids to all channel's, operations, and message's. */ export function applyUniqueIds(structure: any) { const asyncapiVersion = structure.asyncapi.charAt(0); switch (asyncapiVersion) { case '3': - if (structure.channels) { - for (const [channelId, channel] of Object.entries(structure.channels as Record)) { - channel[xParserObjectUniqueId] = channelId; - if (channel.messages) { - for (const [messageId, message] of Object.entries(channel.messages as Record)) { - message[xParserObjectUniqueId] = messageId; - } - } - } + applyUniqueIdToChannels(structure.channels); + applyUniqueIdToObjects(structure.operations); + if (structure.components) { + applyUniqueIdToObjects(structure.components.messages); + applyUniqueIdToObjects(structure.components.operations); + applyUniqueIdToChannels(structure.components.channels); } break; } } - \ No newline at end of file + +function applyUniqueIdToChannels(channels: any) { + for (const [channelId, channel] of Object.entries((channels ?? {}) as Record)) { + if (!channel[xParserObjectUniqueId]) { + channel[xParserObjectUniqueId] = channelId; + } + applyUniqueIdToObjects(channel.messages); + } +} + +function applyUniqueIdToObjects(objects: any) { + for (const [objectKey, object] of Object.entries((objects ?? {}) as Record)) { + if (!object[xParserObjectUniqueId]) { + object[xParserObjectUniqueId] = objectKey; + } + } +} diff --git a/src/parse.ts b/src/parse.ts index 479bd9dfb..9688a4314 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -55,8 +55,6 @@ export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input, } else { loadedObj = asyncapi; } - // Apply unique ids before resolving references - applyUniqueIds(loadedObj); const { validated, diagnostics, extras } = await validate(parser, spectral, loadedObj, { ...options.validateOptions, source: options.source, __unstable: options.__unstable }); if (validated === undefined) { return { @@ -71,6 +69,9 @@ export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input, // unfreeze the object - Spectral makes resolved document "freezed" const validatedDoc = copy(validated as Record); + + // Apply unique ids which are used as part of iterating between channels <-> operations <-> messages + applyUniqueIds(validatedDoc); const detailed = createDetailedAsyncAPI(validatedDoc, loadedObj as DetailedAsyncAPI['input'], options.source); const document = createAsyncAPIDocument(detailed); setExtension(xParserSpecParsed, true, document); diff --git a/test/custom-operations/apply-traits-v3.spec.ts b/test/custom-operations/apply-traits-v3.spec.ts index 1561b783a..50a6268d9 100644 --- a/test/custom-operations/apply-traits-v3.spec.ts +++ b/test/custom-operations/apply-traits-v3.spec.ts @@ -55,13 +55,13 @@ describe('custom operations - apply traits v3', function() { const v3Document = document as AsyncAPIDocumentV3; expect(v3Document).toBeInstanceOf(AsyncAPIDocumentV3); - const someOperation1 = v3Document?.json()?.operations?.someOperation1; + const someOperation1 = v3Document?.json()?.operations?.someOperation1 as v3.OperationObject; delete someOperation1?.traits; - expect(someOperation1).toEqual({ action: 'send', channel: { 'x-parser-unique-object-id': 'channel1' }, description: 'another description' }); + expect(someOperation1).toEqual({ action: 'send', channel: { 'x-parser-unique-object-id': 'channel1' }, description: 'another description', 'x-parser-unique-object-id': 'someOperation1' }); - const someOperation2 = v3Document?.json()?.operations?.someOperation2; + const someOperation2 = v3Document?.json()?.operations?.someOperation2 as v3.OperationObject; delete someOperation2?.traits; - expect(someOperation2).toEqual({ action: 'send', channel: { 'x-parser-unique-object-id': 'channel1' }, description: 'root description' }); + expect(someOperation2).toEqual({ action: 'send', channel: { 'x-parser-unique-object-id': 'channel1' }, description: 'root description', 'x-parser-unique-object-id': 'someOperation2' }); }); it('should apply traits to messages (channels)', async function() { @@ -111,11 +111,11 @@ describe('custom operations - apply traits v3', function() { const v3Document = document as AsyncAPIDocumentV3; expect(v3Document).toBeInstanceOf(AsyncAPIDocumentV3); - const message1 = v3Document?.json()?.channels?.someChannel1?.messages?.someMessage; + const message1 = (v3Document?.json()?.channels?.someChannel1 as v3.ChannelObject).messages?.someMessage; delete (message1 as v3.MessageObject)?.traits; expect(message1).toEqual({ summary: 'some summary', description: 'another description', 'x-parser-message-name': 'someMessage', 'x-parser-unique-object-id': 'someMessage' }); - const message2 = v3Document?.json()?.channels?.someChannel2?.messages?.someMessage; + const message2 = (v3Document?.json()?.channels?.someChannel2 as v3.ChannelObject).messages?.someMessage; delete (message2 as v3.MessageObject)?.traits; expect(message2).toEqual({ summary: 'root summary', description: 'root description', 'x-parser-message-name': 'someMessage', 'x-parser-unique-object-id': 'someMessage' }); }); @@ -163,14 +163,14 @@ describe('custom operations - apply traits v3', function() { const message1 = v3Document?.json()?.components?.messages?.someMessage1; delete (message1 as v3.MessageObject)?.traits; - expect(message1).toEqual({ summary: 'some summary', description: 'another description', 'x-parser-message-name': 'someMessage1' }); + expect(message1).toEqual({ summary: 'some summary', description: 'another description', 'x-parser-message-name': 'someMessage1', 'x-parser-unique-object-id': 'someMessage1' }); const message2 = v3Document?.json()?.components?.messages?.someMessage2; delete (message2 as v3.MessageObject)?.traits; - expect(message2).toEqual({ summary: 'root summary', description: 'root description', 'x-parser-message-name': 'someMessage2' }); + expect(message2).toEqual({ summary: 'root summary', description: 'root description', 'x-parser-message-name': 'someMessage2', 'x-parser-unique-object-id': 'someMessage2' }); }); - it('iterative functions should still work after traits have been applied', async function() { + describe('iterative functions should still work after traits have been applied', function() { const documentRaw = { asyncapi: '3.0.0', info: { @@ -187,7 +187,7 @@ describe('custom operations - apply traits v3', function() { 'smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured': { address: 'smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured', messages: { - 'receiveLightMeasurement.message': { + lightMeasured: { $ref: '#/components/messages/lightMeasured' } }, @@ -213,7 +213,7 @@ describe('custom operations - apply traits v3', function() { ], messages: [ { - $ref: '#/components/messages/lightMeasured' + $ref: '#/channels/smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured/messages/lightMeasured' } ] } @@ -290,32 +290,64 @@ describe('custom operations - apply traits v3', function() { } } }; - const { document } = await parser.parse(documentRaw); - - const v3Document = document as AsyncAPIDocumentV3; - expect(v3Document).toBeInstanceOf(AsyncAPIDocumentV3); + let v3Document: AsyncAPIDocumentV3; const expectedOperationId = 'receiveLightMeasurement'; const expectedChannelId = 'smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured'; - const operations = v3Document.operations(); - expect(operations.length).toEqual(1); - const operation = operations[0]; - expect(operation.id()).toEqual(expectedOperationId); - const operationChannels = operation.channels().all(); - expect(operationChannels.length).toEqual(1); - const lightMeasuredChannel = operationChannels[0]; - expect(lightMeasuredChannel.json()[xParserObjectUniqueId]).toEqual(expectedChannelId); - const channelOperations = lightMeasuredChannel.operations().all(); - expect(channelOperations.length).toEqual(1); - const circularOperation = channelOperations[0]; - expect(circularOperation.id()).toEqual(expectedOperationId); + const expectedMessageId = 'lightMeasured'; + + beforeAll(async () => { + const { document, diagnostics } = await parser.parse(documentRaw); + expect(diagnostics.length).toEqual(0); + v3Document = document as AsyncAPIDocumentV3; + }); + + it('should be able to go from operation -> channel', () => { + const operations = v3Document.operations().all(); + expect(operations.length).toEqual(1); + const operation = operations[0]; + expect(operation.id()).toEqual(expectedOperationId); + const operationChannels = operation.channels().all(); + expect(operationChannels.length).toEqual(1); + const lightMeasuredChannel = operationChannels[0]; + expect(lightMeasuredChannel.json()[xParserObjectUniqueId]).toEqual(expectedChannelId); + const messages = lightMeasuredChannel.messages().all(); + expect(messages.length).toEqual(1); + const message = messages[0]; + expect(message.id()).toEqual(expectedMessageId); + }); + + it('should be able to go from channel -> operation', () => { + const channels = v3Document.channels().all(); + expect(channels.length).toEqual(1); + const channel = channels[0]; + expect(channel.json()[xParserObjectUniqueId]).toEqual(expectedChannelId); + const channelOperations = channel.operations().all(); + expect(channelOperations.length).toEqual(1); + const operation = channelOperations[0]; + expect(operation.id()).toEqual(expectedOperationId); + const messages = operation.messages().all(); + expect(messages.length).toEqual(1); + const message = messages[0]; + expect(message.id()).toEqual(expectedMessageId); + }); - const channels = v3Document.channels(); - expect(channels.length).toEqual(1); - const channel = channels[0]; - expect(channel.json()[xParserObjectUniqueId]).toEqual(expectedChannelId); - const channelOperations2 = channel.operations().all(); - expect(channelOperations2.length).toEqual(1); - const operation2 = channelOperations2[0]; - expect(operation2.id()).toEqual(expectedOperationId); + it('should be able to go in full circle operation -> channel -> operation', () => { + const operations = v3Document.operations().all(); + expect(operations.length).toEqual(1); + const operation = operations[0]; + expect(operation.id()).toEqual(expectedOperationId); + const operationChannels = operation.channels().all(); + expect(operationChannels.length).toEqual(1); + const lightMeasuredChannel = operationChannels[0]; + expect(lightMeasuredChannel.json()[xParserObjectUniqueId]).toEqual(expectedChannelId); + const channelOperations = lightMeasuredChannel.operations().all(); + expect(channelOperations.length).toEqual(1); + const circularOperation = channelOperations[0]; + expect(circularOperation.id()).toEqual(expectedOperationId); + const messages = circularOperation.messages().all(); + expect(messages.length).toEqual(1); + const message = messages[0]; + expect(message.id()).toEqual(expectedMessageId); + }); }); }); diff --git a/test/custom-operations/apply-unique-ids.spec.ts b/test/custom-operations/apply-unique-ids.spec.ts index 2fc32012b..d33cce817 100644 --- a/test/custom-operations/apply-unique-ids.spec.ts +++ b/test/custom-operations/apply-unique-ids.spec.ts @@ -20,11 +20,47 @@ describe('applying unique ids', function() { applyUniqueIds(input); expect(input).toEqual(output); }); + it('should set unique id when input has operations', async function() { + const input = {asyncapi: '3.0.0', operations: {testOperation: {}}}; + const output = {...input, operations: {testOperation: {'x-parser-unique-object-id': 'testOperation'}}}; + applyUniqueIds(input); + expect(input).toEqual(output); + }); it('should set unique id when input has messages in channels', async function() { const input = {asyncapi: '3.0.0', channels: {testChannel: {messages: {testMessage: {}}}}}; const output = {...input, channels: {testChannel: {'x-parser-unique-object-id': 'testChannel', messages: {testMessage: {'x-parser-unique-object-id': 'testMessage'}}}}}; applyUniqueIds(input); expect(input).toEqual(output); }); + it('should set unique id when input has channels under components', async function() { + const input = {asyncapi: '3.0.0', components: {channels: {testChannel: {}}}}; + const output = {...input, components: {channels: {testChannel: {'x-parser-unique-object-id': 'testChannel'}}}}; + applyUniqueIds(input); + expect(input).toEqual(output); + }); + it('should set unique id when input has operations under components', async function() { + const input = {asyncapi: '3.0.0', components: {operations: {testOperation: {}}}}; + const output = {...input, components: {operations: {testOperation: {'x-parser-unique-object-id': 'testOperation'}}}}; + applyUniqueIds(input); + expect(input).toEqual(output); + }); + it('should set unique id when input has messages in channels under components', async function() { + const input = {asyncapi: '3.0.0', components: {channels: {testChannel: {messages: {testMessage: {}}}}}}; + const output = {...input, components: {channels: {testChannel: {'x-parser-unique-object-id': 'testChannel', messages: {testMessage: {'x-parser-unique-object-id': 'testMessage'}}}}}}; + applyUniqueIds(input); + expect(input).toEqual(output); + }); + it('should set unique id when input has messages under components', async function() { + const input = {asyncapi: '3.0.0', components: { messages: {testMessage: {}}}}; + const output = {...input, components: { messages: {testMessage: {'x-parser-unique-object-id': 'testMessage'}}}}; + applyUniqueIds(input); + expect(input).toEqual(output); + }); + it('should not overwrite existing unique ids', async function() { + const input = {asyncapi: '3.0.0', components: { messages: {testMessage: {'x-parser-unique-object-id': 'testMessage2'}}}}; + const output = {...input, components: { messages: {testMessage: {'x-parser-unique-object-id': 'testMessage2'}}}}; + applyUniqueIds(input); + expect(input).toEqual(output); + }); }); });