Skip to content

Commit 18ee6fe

Browse files
authored
fix!: traits, id and reply problems for v3 (#910)
1 parent 62c58da commit 18ee6fe

14 files changed

+283
-47
lines changed

src/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const xParserOriginalTraits = 'x-parser-original-traits';
1414

1515
export const xParserCircular = 'x-parser-circular';
1616
export const xParserCircularProps = 'x-parser-circular-props';
17+
export const xParserObjectUniqueId = 'x-parser-unique-object-id';
1718

1819
export const EXTENSION_REGEX = /^x-[\w\d.\-_]+$/;
1920

src/custom-operations/apply-traits.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,17 @@ function applyTraitsToObjectV2(value: Record<string, unknown>) {
5151
const v3TraitPaths = [
5252
// operations
5353
'$.operations.*',
54+
'$.operations.*.channel.*',
55+
'$.operations.*.channel.messages.*',
56+
'$.operations.*.messages.*',
5457
'$.components.operations.*',
55-
// messages
58+
'$.components.operations.*.channel.*',
59+
'$.components.operations.*.channel.messages.*',
60+
'$.components.operations.*.messages.*',
61+
// Channels
5662
'$.channels.*.messages.*',
57-
'$.operations.*.messages.*',
5863
'$.components.channels.*.messages.*',
59-
'$.components.operations.*.messages.*',
64+
// messages
6065
'$.components.messages.*',
6166
];
6267

@@ -100,4 +105,4 @@ function applyTraitsToObjectV3(value: Record<string, unknown>) {
100105
value[String(key)] = mergePatch(value[String(key)], trait[String(key)]);
101106
}
102107
}
103-
}
108+
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { xParserObjectUniqueId } from '../constants';
2+
3+
/**
4+
* This function applies unique ids for objects whose key's function as ids, ensuring that the key is part of the value.
5+
*
6+
* For v3; Apply unique ids to channel's, and message's
7+
*/
8+
export function applyUniqueIds(structure: any) {
9+
const asyncapiVersion = structure.asyncapi.charAt(0);
10+
switch (asyncapiVersion) {
11+
case '3':
12+
if (structure.channels) {
13+
for (const [channelId, channel] of Object.entries(structure.channels as Record<string, any>)) {
14+
channel[xParserObjectUniqueId] = channelId;
15+
if (channel.messages) {
16+
for (const [messageId, message] of Object.entries(channel.messages as Record<string, any>)) {
17+
message[xParserObjectUniqueId] = messageId;
18+
}
19+
}
20+
}
21+
}
22+
break;
23+
}
24+
}
25+

src/custom-operations/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import { applyTraitsV2, applyTraitsV3 } from './apply-traits';
22
import { resolveCircularRefs } from './resolve-circular-refs';
33
import { parseSchemasV2, parseSchemasV3 } from './parse-schema';
44
import { anonymousNaming } from './anonymous-naming';
5+
import { checkCircularRefs } from './check-circular-refs';
56

67
import type { RulesetFunctionContext } from '@stoplight/spectral-core';
78
import type { Parser } from '../parser';
89
import type { ParseOptions } from '../parse';
910
import type { AsyncAPIDocumentInterface } from '../models';
1011
import type { DetailedAsyncAPI } from '../types';
1112
import type { v2, v3 } from '../spec-types';
12-
import { checkCircularRefs } from './check-circular-refs';
1313

14+
export {applyUniqueIds} from './apply-unique-ids';
1415
export async function customOperations(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, inventory: RulesetFunctionContext['documentInventory'], options: ParseOptions): Promise<void> {
1516
switch (detailed.semver.major) {
1617
case 2: return operationsV2(parser, document, detailed, inventory, options);

src/models/v3/channel.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,15 @@ import { Operations } from './operations';
66
import { Operation } from './operation';
77
import { Servers } from './servers';
88
import { Server } from './server';
9-
9+
import { xParserObjectUniqueId } from '../../constants';
1010
import { CoreModel } from './mixins';
11-
1211
import type { ChannelInterface } from '../channel';
1312
import type { ChannelParametersInterface } from '../channel-parameters';
1413
import type { MessagesInterface } from '../messages';
1514
import type { OperationsInterface } from '../operations';
1615
import type { OperationInterface } from '../operation';
1716
import type { ServersInterface } from '../servers';
1817
import type { ServerInterface } from '../server';
19-
2018
import type { v3 } from '../../spec-types';
2119

2220
export class Channel extends CoreModel<v3.ChannelObject, { id: string }> implements ChannelInterface {
@@ -30,8 +28,8 @@ export class Channel extends CoreModel<v3.ChannelObject, { id: string }> impleme
3028

3129
servers(): ServersInterface {
3230
const servers: ServerInterface[] = [];
33-
const allowedServers = this._json.servers || [];
34-
Object.entries(this._meta.asyncapi?.parsed.servers || {}).forEach(([serverName, server]) => {
31+
const allowedServers = this._json.servers ?? [];
32+
Object.entries(this._meta.asyncapi?.parsed.servers ?? {}).forEach(([serverName, server]) => {
3533
if (allowedServers.length === 0 || allowedServers.includes(server)) {
3634
servers.push(this.createModel(Server, server, { id: serverName, pointer: `/servers/${serverName}` }));
3735
}
@@ -41,8 +39,10 @@ export class Channel extends CoreModel<v3.ChannelObject, { id: string }> impleme
4139

4240
operations(): OperationsInterface {
4341
const operations: OperationInterface[] = [];
44-
Object.entries(((this._meta.asyncapi?.parsed as v3.AsyncAPIObject)?.operations || {})).forEach(([operationId, operation]) => {
45-
if ((operation as v3.OperationObject).channel === this._json) {
42+
Object.entries(((this._meta.asyncapi?.parsed as v3.AsyncAPIObject)?.operations ?? {} as v3.OperationsObject)).forEach(([operationId, operation]) => {
43+
const operationChannelId = ((operation as v3.OperationObject).channel as any)[xParserObjectUniqueId];
44+
const channelId = (this._json as any)[xParserObjectUniqueId];
45+
if (operationChannelId === channelId) {
4646
operations.push(
4747
this.createModel(Operation, operation as v3.OperationObject, { id: operationId, pointer: `/operations/${operationId}` }),
4848
);
@@ -53,15 +53,15 @@ export class Channel extends CoreModel<v3.ChannelObject, { id: string }> impleme
5353

5454
messages(): MessagesInterface {
5555
return new Messages(
56-
Object.entries(this._json.messages || {}).map(([messageName, message]) => {
56+
Object.entries(this._json.messages ?? {}).map(([messageName, message]) => {
5757
return this.createModel(Message, message as v3.MessageObject, { id: messageName, pointer: this.jsonPath(`messages/${messageName}`) });
5858
})
5959
);
6060
}
6161

6262
parameters(): ChannelParametersInterface {
6363
return new ChannelParameters(
64-
Object.entries(this._json.parameters || {}).map(([channelParameterName, channelParameter]) => {
64+
Object.entries(this._json.parameters ?? {}).map(([channelParameterName, channelParameter]) => {
6565
return this.createModel(ChannelParameter, channelParameter as v3.ParameterObject, {
6666
id: channelParameterName,
6767
pointer: this.jsonPath(`parameters/${channelParameterName}`),

src/models/v3/message.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { MessageTraits } from './message-traits';
66
import { MessageTrait } from './message-trait';
77
import { Servers } from './servers';
88
import { Schema } from './schema';
9-
9+
import { xParserObjectUniqueId } from '../../constants';
1010
import type { ChannelsInterface } from '../channels';
1111
import type { ChannelInterface } from '../channel';
1212
import type { MessageInterface } from '../message';
@@ -16,7 +16,6 @@ import type { OperationInterface } from '../operation';
1616
import type { ServersInterface } from '../servers';
1717
import type { ServerInterface } from '../server';
1818
import type { SchemaInterface } from '../schema';
19-
2019
import type { v3 } from '../../spec-types';
2120

2221
export class Message extends MessageTrait<v3.MessageObject> implements MessageInterface {
@@ -58,6 +57,7 @@ export class Message extends MessageTrait<v3.MessageObject> implements MessageIn
5857
}
5958

6059
channels(): ChannelsInterface {
60+
const thisMessageId = (this._json)[xParserObjectUniqueId];
6161
const channels: ChannelInterface[] = [];
6262
const channelsData: any[] = [];
6363
this.operations().forEach(operation => {
@@ -73,7 +73,10 @@ export class Message extends MessageTrait<v3.MessageObject> implements MessageIn
7373

7474
Object.entries((this._meta.asyncapi?.parsed as v3.AsyncAPIObject)?.channels || {}).forEach(([channelId, channelData]) => {
7575
const channelModel = this.createModel(Channel, channelData as v3.ChannelObject, { id: channelId, pointer: `/channels/${channelId}` });
76-
if (!channelsData.includes(channelData) && channelModel.messages().some(m => m.json() === this._json)) {
76+
if (!channelsData.includes(channelData) && channelModel.messages().some(m => {
77+
const messageId = (m as any)[xParserObjectUniqueId];
78+
return messageId === thisMessageId;
79+
})) {
7780
channelsData.push(channelData);
7881
channels.push(channelModel);
7982
}
@@ -83,10 +86,15 @@ export class Message extends MessageTrait<v3.MessageObject> implements MessageIn
8386
}
8487

8588
operations(): OperationsInterface {
89+
const thisMessageId = (this._json)[xParserObjectUniqueId];
8690
const operations: OperationInterface[] = [];
8791
Object.entries((this._meta.asyncapi?.parsed as v3.AsyncAPIObject)?.operations || {}).forEach(([operationId, operation]) => {
8892
const operationModel = this.createModel(Operation, operation as v3.OperationObject, { id: operationId, pointer: `/operations/${operationId}` });
89-
if (operationModel.messages().some(m => m.json() === this._json)) {
93+
const operationHasMessage = operationModel.messages().some(m => {
94+
const messageId = (m as any)[xParserObjectUniqueId];
95+
return messageId === thisMessageId;
96+
});
97+
if (operationHasMessage) {
9098
operations.push(operationModel);
9199
}
92100
});

src/models/v3/operation-reply.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@ import { Message } from './message';
44
import { Messages } from './messages';
55
import { MessagesInterface } from '../messages';
66
import { OperationReplyAddress } from './operation-reply-address';
7-
87
import { extensions } from './mixins';
9-
8+
import { xParserObjectUniqueId } from '../../constants';
109
import type { ExtensionsInterface } from '../extensions';
1110
import type { OperationReplyInterface } from '../operation-reply';
1211
import type { OperationReplyAddressInterface } from '../operation-reply-address';
1312
import type { ChannelInterface } from '../channel';
14-
1513
import type { v3 } from '../../spec-types';
1614

1715
export class OperationReply extends BaseModel<v3.OperationReplyObject, { id?: string }> implements OperationReplyInterface {
@@ -35,14 +33,17 @@ export class OperationReply extends BaseModel<v3.OperationReplyObject, { id?: st
3533

3634
channel(): ChannelInterface | undefined {
3735
if (this._json.channel) {
38-
return this.createModel(Channel, this._json.channel as v3.ChannelObject, { id: '', pointer: this.jsonPath('channel') });
36+
const channelId = (this._json.channel as any)[xParserObjectUniqueId];
37+
return this.createModel(Channel, this._json.channel as v3.ChannelObject, { id: channelId, pointer: this.jsonPath('channel') });
3938
}
4039
return this._json.channel;
4140
}
41+
4242
messages(): MessagesInterface {
4343
return new Messages(
44-
Object.entries(this._json.messages || {}).map(([messageName, message]) => {
45-
return this.createModel(Message, message as v3.MessageObject, { id: messageName, pointer: this.jsonPath(`messages/${messageName}`) });
44+
Object.values(this._json.messages ?? {}).map((message) => {
45+
const messageId = (message as any)[xParserObjectUniqueId];
46+
return this.createModel(Message, message as v3.MessageObject, { id: messageId, pointer: this.jsonPath(`messages/${messageId}`) });
4647
})
4748
);
4849
}

src/models/v3/operation.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { ServersInterface } from '../servers';
1717
import type { ServerInterface } from '../server';
1818

1919
import type { v3 } from '../../spec-types';
20+
import { xParserObjectUniqueId } from '../../constants';
2021

2122
export class Operation extends OperationTrait<v3.OperationObject> implements OperationInterface {
2223
action(): OperationAction {
@@ -48,23 +49,21 @@ export class Operation extends OperationTrait<v3.OperationObject> implements Ope
4849

4950
channels(): ChannelsInterface {
5051
if (this._json.channel) {
51-
for (const [channelName, channel] of Object.entries(this._meta.asyncapi?.parsed.channels || {})) {
52-
if (channel === this._json.channel) {
53-
return new Channels([
54-
this.createModel(Channel, this._json.channel as v3.ChannelObject, { id: channelName, pointer: `/channels/${channelName}` })
55-
]);
56-
}
57-
}
52+
const operationChannelId = (this._json.channel as any)[xParserObjectUniqueId];
53+
return new Channels([
54+
this.createModel(Channel, this._json.channel as v3.ChannelObject, { id: operationChannelId, pointer: `/channels/${operationChannelId}` })
55+
]);
5856
}
5957
return new Channels([]);
6058
}
61-
59+
6260
messages(): MessagesInterface {
6361
const messages: MessageInterface[] = [];
6462
if (Array.isArray(this._json.messages)) {
6563
this._json.messages.forEach((message, index) => {
64+
const messageId = (message as any)[xParserObjectUniqueId];
6665
messages.push(
67-
this.createModel(Message, message as v3.MessageObject, { id: '', pointer: this.jsonPath(`messages/${index}`) })
66+
this.createModel(Message, message as v3.MessageObject, { id: messageId, pointer: this.jsonPath(`messages/${index}`) })
6867
);
6968
});
7069
return new Messages(messages);

src/parse.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AsyncAPIDocumentInterface, ParserAPIVersion } from './models';
22

3-
import { customOperations } from './custom-operations';
3+
import { applyUniqueIds, customOperations } from './custom-operations';
44
import { validate } from './validate';
55
import { copy } from './stringify';
66
import { createAsyncAPIDocument } from './document';
@@ -38,13 +38,26 @@ const defaultOptions: ParseOptions = {
3838
validateOptions: {},
3939
__unstable: {},
4040
};
41-
41+
import yaml from 'js-yaml';
4242
export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input, options: ParseOptions = {}): Promise<ParseOutput> {
4343
let spectralDocument: Document | undefined;
4444

4545
try {
4646
options = mergePatch<ParseOptions>(defaultOptions, options);
47-
const { validated, diagnostics, extras } = await validate(parser, spectral, asyncapi, { ...options.validateOptions, source: options.source, __unstable: options.__unstable });
47+
// Normalize input to always be JSON
48+
let loadedObj;
49+
if (typeof asyncapi === 'string') {
50+
try {
51+
loadedObj = yaml.load(asyncapi);
52+
} catch (e) {
53+
loadedObj = JSON.parse(asyncapi);
54+
}
55+
} else {
56+
loadedObj = asyncapi;
57+
}
58+
// Apply unique ids before resolving references
59+
applyUniqueIds(loadedObj);
60+
const { validated, diagnostics, extras } = await validate(parser, spectral, loadedObj, { ...options.validateOptions, source: options.source, __unstable: options.__unstable });
4861
if (validated === undefined) {
4962
return {
5063
document: undefined,
@@ -58,12 +71,12 @@ export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input,
5871

5972
// unfreeze the object - Spectral makes resolved document "freezed"
6073
const validatedDoc = copy(validated as Record<string, any>);
61-
const detailed = createDetailedAsyncAPI(validatedDoc, asyncapi as DetailedAsyncAPI['input'], options.source);
74+
const detailed = createDetailedAsyncAPI(validatedDoc, loadedObj as DetailedAsyncAPI['input'], options.source);
6275
const document = createAsyncAPIDocument(detailed);
6376
setExtension(xParserSpecParsed, true, document);
6477
setExtension(xParserApiVersion, ParserAPIVersion, document);
6578
await customOperations(parser, document, detailed, inventory, options);
66-
79+
6780
return {
6881
document,
6982
diagnostics,

src/spec-types/v3.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export interface OperationTraitObject extends SpecificationExtensions {
143143

144144
export interface OperationReplyObject extends SpecificationExtensions {
145145
channel?: ChannelObject | ReferenceObject;
146-
messages?: MessagesObject;
146+
messages?: (MessageObject | ReferenceObject)[];
147147
address?: OperationReplyAddressObject | ReferenceObject;
148148
}
149149

0 commit comments

Comments
 (0)