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

fix: use JSON Pointer last element as ID for reply.channel and reply.messages[] #895

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 18 additions & 0 deletions src/models/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,21 @@ export type InferModelMetadata<T> = T extends BaseModel<infer _, infer M> ? M :
export function createModel<T extends BaseModel>(Model: Constructor<T>, value: InferModelData<T>, meta: Omit<ModelMetadata, 'asyncapi'> & { asyncapi?: DetailedAsyncAPI } & InferModelMetadata<T>, 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/, '/');
}
10 changes: 6 additions & 4 deletions src/models/v3/operation-reply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import type { ChannelInterface } from '../channel';

import type { v3 } from '../../spec-types';

import { objectIdFromJSONPointer } from '../utils';

export class OperationReply extends BaseModel<v3.OperationReplyObject, { id?: string }> implements OperationReplyInterface {
id(): string | undefined {
return this._meta.id;
Expand All @@ -35,14 +37,14 @@ export class OperationReply extends BaseModel<v3.OperationReplyObject, { id?: st

channel(): ChannelInterface | undefined {
if (this._json.channel) {
return this.createModel(Channel, this._json.channel as v3.ChannelObject, { id: '', pointer: this.jsonPath('channel') });
return this.createModel(Channel, this._json.channel as v3.ChannelObject, { id: objectIdFromJSONPointer(this.jsonPath('channel'), this._meta.asyncapi.input), pointer: this.jsonPath('channel') });
}
return this._json.channel;
}
messages(): MessagesInterface {
return new Messages(
Object.entries(this._json.messages || {}).map(([messageName, message]) => {
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}`) });
})
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/spec-types/v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export interface OperationTraitObject extends SpecificationExtensions {

export interface OperationReplyObject extends SpecificationExtensions {
channel?: ChannelObject | ReferenceObject;
messages?: MessagesObject;
messages?: Array<MessageObject | ReferenceObject>;
address?: OperationReplyAddressObject | ReferenceObject;
}

Expand Down
8 changes: 4 additions & 4 deletions test/models/v3/operation-reply.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
79 changes: 78 additions & 1 deletion test/resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -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",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc is the one @AceTheCreator shared in #873.
We could remove it or adapt it, but we need something to test this anyway.

"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();

Expand Down
Loading