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
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
Prev Previous commit
Next Next commit
fixed implementation and test
jonaslagoni committed Nov 28, 2023
commit 83f6c8a0f58ddc257ba5bc64996cf850f4ea1fe6
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -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.\-_]+$/;

13 changes: 9 additions & 4 deletions src/custom-operations/apply-traits.ts
Original file line number Diff line number Diff line change
@@ -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.*',
];

@@ -100,4 +105,4 @@ function applyTraitsToObjectV3(value: Record<string, unknown>) {
value[String(key)] = mergePatch(value[String(key)], trait[String(key)]);
}
}
}
}
3 changes: 2 additions & 1 deletion src/custom-operations/index.ts
Original file line number Diff line number Diff line change
@@ -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);
18 changes: 9 additions & 9 deletions src/models/v3/channel.ts
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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}` }));
}
@@ -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}` }),
);
@@ -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}`),
16 changes: 12 additions & 4 deletions src/models/v3/message.ts
Original file line number Diff line number Diff line change
@@ -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';
@@ -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 {
@@ -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 => {
@@ -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);
}
@@ -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);
}
});
12 changes: 6 additions & 6 deletions src/models/v3/operation-reply.ts
Original file line number Diff line number Diff line change
@@ -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 {
@@ -35,14 +33,16 @@ 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.entries(this._json.messages ?? {}).map(([messageId, message]) => {
return this.createModel(Message, message as v3.MessageObject, { id: messageId, pointer: this.jsonPath(`messages/${messageId}`) });
})
);
}
17 changes: 8 additions & 9 deletions src/models/v3/operation.ts
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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);
24 changes: 18 additions & 6 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';
@@ -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,
@@ -58,13 +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);
detailed.parsed['x-test'] = 'test';
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,
Loading