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!: traits, id and reply problems for v3 #910

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
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);
Comment on lines +47 to +59
Copy link
Member Author

Choose a reason for hiding this comment

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

In order to apply unique IDs I had to control the input and force any input (string/yaml/json, and pure objects) to adapt them.

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