Skip to content

Commit

Permalink
fix!: traits, id and reply problems for v3 (#910)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonaslagoni authored Nov 29, 2023
1 parent 62c58da commit 18ee6fe
Show file tree
Hide file tree
Showing 14 changed files with 283 additions and 47 deletions.
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const xParserOriginalTraits = 'x-parser-original-traits';

export const xParserCircular = 'x-parser-circular';
export const xParserCircularProps = 'x-parser-circular-props';
export const xParserObjectUniqueId = 'x-parser-unique-object-id';

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

Expand Down
13 changes: 9 additions & 4 deletions src/custom-operations/apply-traits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,17 @@ function applyTraitsToObjectV2(value: Record<string, unknown>) {
const v3TraitPaths = [
// operations
'$.operations.*',
'$.operations.*.channel.*',
'$.operations.*.channel.messages.*',
'$.operations.*.messages.*',
'$.components.operations.*',
// messages
'$.components.operations.*.channel.*',
'$.components.operations.*.channel.messages.*',
'$.components.operations.*.messages.*',
// Channels
'$.channels.*.messages.*',
'$.operations.*.messages.*',
'$.components.channels.*.messages.*',
'$.components.operations.*.messages.*',
// messages
'$.components.messages.*',
];

Expand Down Expand Up @@ -100,4 +105,4 @@ function applyTraitsToObjectV3(value: Record<string, unknown>) {
value[String(key)] = mergePatch(value[String(key)], trait[String(key)]);
}
}
}
}
25 changes: 25 additions & 0 deletions src/custom-operations/apply-unique-ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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
*/
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<string, any>)) {
channel[xParserObjectUniqueId] = channelId;
if (channel.messages) {
for (const [messageId, message] of Object.entries(channel.messages as Record<string, any>)) {
message[xParserObjectUniqueId] = messageId;
}
}
}
}
break;
}
}

3 changes: 2 additions & 1 deletion src/custom-operations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { applyTraitsV2, applyTraitsV3 } from './apply-traits';
import { resolveCircularRefs } from './resolve-circular-refs';
import { parseSchemasV2, parseSchemasV3 } from './parse-schema';
import { anonymousNaming } from './anonymous-naming';
import { checkCircularRefs } from './check-circular-refs';

import type { RulesetFunctionContext } from '@stoplight/spectral-core';
import type { Parser } from '../parser';
import type { ParseOptions } from '../parse';
import type { AsyncAPIDocumentInterface } from '../models';
import type { DetailedAsyncAPI } from '../types';
import type { v2, v3 } from '../spec-types';
import { checkCircularRefs } from './check-circular-refs';

export {applyUniqueIds} from './apply-unique-ids';
export async function customOperations(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, inventory: RulesetFunctionContext['documentInventory'], options: ParseOptions): Promise<void> {
switch (detailed.semver.major) {
case 2: return operationsV2(parser, document, detailed, inventory, options);
Expand Down
18 changes: 9 additions & 9 deletions src/models/v3/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@ import { Operations } from './operations';
import { Operation } from './operation';
import { Servers } from './servers';
import { Server } from './server';

import { xParserObjectUniqueId } from '../../constants';
import { CoreModel } from './mixins';

import type { ChannelInterface } from '../channel';
import type { ChannelParametersInterface } from '../channel-parameters';
import type { MessagesInterface } from '../messages';
import type { OperationsInterface } from '../operations';
import type { OperationInterface } from '../operation';
import type { ServersInterface } from '../servers';
import type { ServerInterface } from '../server';

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

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

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

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

messages(): MessagesInterface {
return new Messages(
Object.entries(this._json.messages || {}).map(([messageName, message]) => {
Object.entries(this._json.messages ?? {}).map(([messageName, message]) => {
return this.createModel(Message, message as v3.MessageObject, { id: messageName, pointer: this.jsonPath(`messages/${messageName}`) });
})
);
}

parameters(): ChannelParametersInterface {
return new ChannelParameters(
Object.entries(this._json.parameters || {}).map(([channelParameterName, channelParameter]) => {
Object.entries(this._json.parameters ?? {}).map(([channelParameterName, channelParameter]) => {
return this.createModel(ChannelParameter, channelParameter as v3.ParameterObject, {
id: channelParameterName,
pointer: this.jsonPath(`parameters/${channelParameterName}`),
Expand Down
16 changes: 12 additions & 4 deletions src/models/v3/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { MessageTraits } from './message-traits';
import { MessageTrait } from './message-trait';
import { Servers } from './servers';
import { Schema } from './schema';

import { xParserObjectUniqueId } from '../../constants';
import type { ChannelsInterface } from '../channels';
import type { ChannelInterface } from '../channel';
import type { MessageInterface } from '../message';
Expand All @@ -16,7 +16,6 @@ import type { OperationInterface } from '../operation';
import type { ServersInterface } from '../servers';
import type { ServerInterface } from '../server';
import type { SchemaInterface } from '../schema';

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

export class Message extends MessageTrait<v3.MessageObject> implements MessageInterface {
Expand Down Expand Up @@ -58,6 +57,7 @@ export class Message extends MessageTrait<v3.MessageObject> implements MessageIn
}

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

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

operations(): OperationsInterface {
const thisMessageId = (this._json)[xParserObjectUniqueId];
const operations: OperationInterface[] = [];
Object.entries((this._meta.asyncapi?.parsed as v3.AsyncAPIObject)?.operations || {}).forEach(([operationId, operation]) => {
const operationModel = this.createModel(Operation, operation as v3.OperationObject, { id: operationId, pointer: `/operations/${operationId}` });
if (operationModel.messages().some(m => m.json() === this._json)) {
const operationHasMessage = operationModel.messages().some(m => {
const messageId = (m as any)[xParserObjectUniqueId];
return messageId === thisMessageId;
});
if (operationHasMessage) {
operations.push(operationModel);
}
});
Expand Down
13 changes: 7 additions & 6 deletions src/models/v3/operation-reply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import { Message } from './message';
import { Messages } from './messages';
import { MessagesInterface } from '../messages';
import { OperationReplyAddress } from './operation-reply-address';

import { extensions } from './mixins';

import { xParserObjectUniqueId } from '../../constants';
import type { ExtensionsInterface } from '../extensions';
import type { OperationReplyInterface } from '../operation-reply';
import type { OperationReplyAddressInterface } from '../operation-reply-address';
import type { ChannelInterface } from '../channel';

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

export class OperationReply extends BaseModel<v3.OperationReplyObject, { id?: string }> implements OperationReplyInterface {
Expand All @@ -35,14 +33,17 @@ 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') });
const channelId = (this._json.channel as any)[xParserObjectUniqueId];
return this.createModel(Channel, this._json.channel as v3.ChannelObject, { id: channelId, 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}`) });
Object.values(this._json.messages ?? {}).map((message) => {
const messageId = (message as any)[xParserObjectUniqueId];
return this.createModel(Message, message as v3.MessageObject, { id: messageId, pointer: this.jsonPath(`messages/${messageId}`) });
})
);
}
Expand Down
17 changes: 8 additions & 9 deletions src/models/v3/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { ServersInterface } from '../servers';
import type { ServerInterface } from '../server';

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

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

channels(): ChannelsInterface {
if (this._json.channel) {
for (const [channelName, channel] of Object.entries(this._meta.asyncapi?.parsed.channels || {})) {
if (channel === this._json.channel) {
return new Channels([
this.createModel(Channel, this._json.channel as v3.ChannelObject, { id: channelName, pointer: `/channels/${channelName}` })
]);
}
}
const operationChannelId = (this._json.channel as any)[xParserObjectUniqueId];
return new Channels([
this.createModel(Channel, this._json.channel as v3.ChannelObject, { id: operationChannelId, pointer: `/channels/${operationChannelId}` })
]);
}
return new Channels([]);
}

messages(): MessagesInterface {
const messages: MessageInterface[] = [];
if (Array.isArray(this._json.messages)) {
this._json.messages.forEach((message, index) => {
const messageId = (message as any)[xParserObjectUniqueId];
messages.push(
this.createModel(Message, message as v3.MessageObject, { id: '', pointer: this.jsonPath(`messages/${index}`) })
this.createModel(Message, message as v3.MessageObject, { id: messageId, pointer: this.jsonPath(`messages/${index}`) })
);
});
return new Messages(messages);
Expand Down
23 changes: 18 additions & 5 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AsyncAPIDocumentInterface, ParserAPIVersion } from './models';

import { customOperations } from './custom-operations';
import { applyUniqueIds, customOperations } from './custom-operations';
import { validate } from './validate';
import { copy } from './stringify';
import { createAsyncAPIDocument } from './document';
Expand Down Expand Up @@ -38,13 +38,26 @@ const defaultOptions: ParseOptions = {
validateOptions: {},
__unstable: {},
};

import yaml from 'js-yaml';
export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input, options: ParseOptions = {}): Promise<ParseOutput> {
let spectralDocument: Document | undefined;

try {
options = mergePatch<ParseOptions>(defaultOptions, options);
const { validated, diagnostics, extras } = await validate(parser, spectral, asyncapi, { ...options.validateOptions, source: options.source, __unstable: options.__unstable });
// Normalize input to always be JSON
let loadedObj;
if (typeof asyncapi === 'string') {
try {
loadedObj = yaml.load(asyncapi);
} catch (e) {
loadedObj = JSON.parse(asyncapi);
}
} 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 {
document: undefined,
Expand All @@ -58,12 +71,12 @@ export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input,

// unfreeze the object - Spectral makes resolved document "freezed"
const validatedDoc = copy(validated as Record<string, any>);
const detailed = createDetailedAsyncAPI(validatedDoc, asyncapi as DetailedAsyncAPI['input'], options.source);
const detailed = createDetailedAsyncAPI(validatedDoc, loadedObj as DetailedAsyncAPI['input'], options.source);
const document = createAsyncAPIDocument(detailed);
setExtension(xParserSpecParsed, true, document);
setExtension(xParserApiVersion, ParserAPIVersion, document);
await customOperations(parser, document, detailed, inventory, options);

return {
document,
diagnostics,
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?: (MessageObject | ReferenceObject)[];
address?: OperationReplyAddressObject | ReferenceObject;
}

Expand Down
Loading

0 comments on commit 18ee6fe

Please sign in to comment.