From 918b5fed214a69705c6858639c82c03b6b699cc0 Mon Sep 17 00:00:00 2001 From: Sergio Moya <1083296+smoya@users.noreply.github.com> Date: Fri, 10 Nov 2023 01:49:45 +0100 Subject: [PATCH] fix: use JSON Pointer last element as ID for reply.channel and reply.messages[] --- src/models/utils.ts | 18 ++++++ src/models/v3/operation-reply.ts | 10 ++-- src/spec-types/v3.ts | 2 +- test/models/v3/operation-reply.spec.ts | 8 +-- test/resolver.spec.ts | 79 +++++++++++++++++++++++++- 5 files changed, 107 insertions(+), 10 deletions(-) diff --git a/src/models/utils.ts b/src/models/utils.ts index 298a4dde0..2a685d5d8 100644 --- a/src/models/utils.ts +++ b/src/models/utils.ts @@ -12,3 +12,21 @@ export type InferModelMetadata = T extends BaseModel ? M : export function createModel(Model: Constructor, value: InferModelData, meta: Omit & { asyncapi?: DetailedAsyncAPI } & InferModelMetadata, parent?: BaseModel): T { return new Model(value, { ...meta, asyncapi: meta.asyncapi || parent?.meta().asyncapi }); } + +export function objectIdFromJSONPointer(path: string, data: any): string { + if (typeof data === 'string') { + data = JSON.parse(data); + } + + if (!path.endsWith('/$ref')) path += '/$ref'; + + path = path.replace(/^\//, ''); // removing the leading slash + path.split('/').forEach((subpath) => { + const key = subpath.replace(/~1/, '/'); // recover escaped slashes + if (data[key] !== undefined) { + data = data[key]; + } + }); + + return data.split('/').pop().replace(/~1/, '/'); +} diff --git a/src/models/v3/operation-reply.ts b/src/models/v3/operation-reply.ts index 93427d388..f1d9eb1d6 100644 --- a/src/models/v3/operation-reply.ts +++ b/src/models/v3/operation-reply.ts @@ -14,6 +14,8 @@ import type { ChannelInterface } from '../channel'; import type { v3 } from '../../spec-types'; +import { objectIdFromJSONPointer } from '../utils'; + export class OperationReply extends BaseModel implements OperationReplyInterface { id(): string | undefined { return this._meta.id; @@ -35,14 +37,14 @@ export class OperationReply extends BaseModel { - return this.createModel(Message, message as v3.MessageObject, { id: messageName, pointer: this.jsonPath(`messages/${messageName}`) }); + return new Messages( + Object.entries(this._json.messages || {}).map(([i, message]) => { + return this.createModel(Message, message as v3.MessageObject, { id: objectIdFromJSONPointer(this.jsonPath(`messages/${i}`), this._meta.asyncapi.input), pointer: this.jsonPath(`messages/${i}`) }); }) ); } diff --git a/src/spec-types/v3.ts b/src/spec-types/v3.ts index e24da3bb6..34774bf18 100644 --- a/src/spec-types/v3.ts +++ b/src/spec-types/v3.ts @@ -143,7 +143,7 @@ export interface OperationTraitObject extends SpecificationExtensions { export interface OperationReplyObject extends SpecificationExtensions { channel?: ChannelObject | ReferenceObject; - messages?: MessagesObject; + messages?: Array; address?: OperationReplyAddressObject | ReferenceObject; } diff --git a/test/models/v3/operation-reply.spec.ts b/test/models/v3/operation-reply.spec.ts index 3f065d5a1..9472060f8 100644 --- a/test/models/v3/operation-reply.spec.ts +++ b/test/models/v3/operation-reply.spec.ts @@ -64,20 +64,20 @@ describe('OperationReply model', function() { describe('.messages()', function() { it('should return collection of messages - single message', function() { - const d = new OperationReply({ messages: { someMessage: {} } }); + const d = new OperationReply({ messages: [{}] }); expect(d.messages()).toBeInstanceOf(Messages); expect(d.messages().all()).toHaveLength(1); expect(d.messages().all()[0]).toBeInstanceOf(Message); }); it('should return collection of messages - more than one messages', function() { - const d = new OperationReply({ messages: { someMessage1: {}, someMessage2: {} } }); + const d = new OperationReply({ messages: [{name: 'someMessage1'}, {name: 'someMessage2'}] }); expect(d.messages()).toBeInstanceOf(Messages); expect(d.messages().all()).toHaveLength(2); expect(d.messages().all()[0]).toBeInstanceOf(Message); - expect(d.messages().all()[0].id()).toEqual('someMessage1'); + expect(d.messages().all()[0].name()).toEqual('someMessage1'); expect(d.messages().all()[1]).toBeInstanceOf(Message); - expect(d.messages().all()[1].id()).toEqual('someMessage2'); + expect(d.messages().all()[1].name()).toEqual('someMessage2'); }); it('should return undefined if address is not present', function() { diff --git a/test/resolver.spec.ts b/test/resolver.spec.ts index 98825ec56..697e2c5b7 100644 --- a/test/resolver.spec.ts +++ b/test/resolver.spec.ts @@ -1,7 +1,84 @@ -import { AsyncAPIDocumentV2 } from '../src/models'; +import { AsyncAPIDocumentV2, AsyncAPIDocumentV3 } from '../src/models'; import { Parser } from '../src/parser'; describe('custom resolver', function() { + it('Reply channel + messages[] Ids come from JSON Pointer', async function() { + const parser = new Parser(); + + const documentRaw = `{ + "asyncapi": "3.0.0", + "info": { + "title": "Account Service", + "version": "1.0.0", + "description": "This service is in charge of processing user signups" + }, + "channels": { + "user/signedup": { + "address": "user/signedup", + "messages": { + "subscribe.message": { + "$ref": "#/components/messages/UserSignedUp" + } + } + } + }, + "operations": { + "user/signedup.subscribe": { + "action": "send", + "channel": { + "$ref": "#/channels/user~1signedup" + }, + "messages": [ + { + "$ref": "#/components/messages/UserSignedUp" + } + ], + "reply": { + "channel": { + "$ref": "#/channels/user~1signedup" + }, + "messages": [ + { + "$ref": "#/components/messages/UserSignedUp" + } + ] + } + } + }, + "components": { + "messages": { + "UserSignedUp": { + "payload": { + "type": "object", + "properties": { + "displayName": { + "type": "string", + "description": "Name of the user" + }, + "email": { + "type": "string", + "format": "email", + "description": "Email of the user" + } + } + } + } + } + } + }`; + const { document } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV3); + + const operation = document?.operations().get('user/signedup.subscribe'); + const replyObj = operation?.reply(); + expect(replyObj?.channel()?.id()).toEqual('user/signedup'); + + const message = replyObj?.messages()?.get('UserSignedUp'); + expect(message).toBeDefined(); + expect(message?.id()).toEqual('UserSignedUp'); + }); + it('should resolve document references', async function() { const parser = new Parser();