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

refactor: implement schemas and all* methods in AsyncAPIDocument class #612

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/custom-operations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async function operationsV2(parser: Parser, document: AsyncAPIDocumentInterface,
await parseSchemasV2(parser, detailed);
}

// anonymous naming and checking circular refrences should be done after custom schemas parsing
checkCircularRefs(document);
anonymousNaming(document);
}
Expand Down
5 changes: 5 additions & 0 deletions src/models/asyncapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ export interface AsyncAPIDocumentInterface extends BaseModel<v2.AsyncAPIObject>,
schemas(): SchemasInterface;
securitySchemes(): SecuritySchemesInterface;
components(): ComponentsInterface;
allServers(): ServersInterface;
allChannels(): ChannelsInterface;
allOperations(): OperationsInterface;
allMessages(): MessagesInterface;
allSchemas(): SchemasInterface;
}
66 changes: 62 additions & 4 deletions src/models/v2/asyncapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@ import { SecurityScheme } from './security-scheme';
import { Schemas } from './schemas';

import { extensions } from './mixins';

import { traverseAsyncApiDocument, SchemaTypesToIterate } from '../../iterator';
import { tilde } from '../../utils';

import type { AsyncAPIDocumentInterface } from '../asyncapi';
import type { InfoInterface } from '../info';
import type { ServersInterface } from '../servers';
import type { ServerInterface } from '../server';
import type { ChannelsInterface } from '../channels';
import type { ChannelInterface } from '../channel';
import type { ComponentsInterface } from '../components';
import type { OperationsInterface } from '../operations';
import type { OperationInterface } from '../operation';
import type { MessagesInterface } from '../messages';
import type { MessageInterface } from '../message';
import type { SchemasInterface } from '../schemas';
import type { SchemaInterface } from '../schema';
import type { SecuritySchemesInterface } from '../security-schemes';
import type { ExtensionsInterface } from '../extensions';

Expand Down Expand Up @@ -65,18 +68,20 @@ export class AsyncAPIDocument extends BaseModel<v2.AsyncAPIObject> implements As

operations(): OperationsInterface {
const operations: OperationInterface[] = [];
this.channels().forEach(channel => operations.push(...channel.operations().all()));
this.channels().forEach(channel => operations.push(...channel.operations()));
return new Operations(operations);
}

messages(): MessagesInterface {
const messages: MessageInterface[] = [];
this.operations().forEach(operation => messages.push(...operation.messages().all()));
this.operations().forEach(operation => operation.messages().forEach(message => (
!messages.some(m => m.json() === message.json()) && messages.push(message)
)));
return new Messages(messages);
}

schemas(): SchemasInterface {
return new Schemas([]);
return this.__schemas(false);
}

securitySchemes(): SecuritySchemesInterface {
Expand All @@ -91,7 +96,60 @@ export class AsyncAPIDocument extends BaseModel<v2.AsyncAPIObject> implements As
return this.createModel(Components, this._json.components || {}, { pointer: '/components' });
}

allServers(): ServersInterface {
Copy link
Member

Choose a reason for hiding this comment

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

This comment applies to all new all* methods.

If I understand correctly, the following servers will be considered equal and only one will be returned:

servers:
  myServer: {}
components:
  servers:
    anotherServer: {}

If I'm right, I think the behavior is wrong and the key (stored under metadata.id i think) should be considered as well when comparing.

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 your example you will have two servers, servers.myServer and components.servers.anotherServer. We only compare the data of given object and we base on that filtering of these same "references".

Copy link
Member

Choose a reason for hiding this comment

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

So we compare the references and not the value of the json object?

Copy link
Member Author

Choose a reason for hiding this comment

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

yep, these same references == these same JSON objects

Copy link
Member Author

Choose a reason for hiding this comment

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

You can check tests for allSchemas or allMessages :)

Copy link
Member

Choose a reason for hiding this comment

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

Then, all good! Thanks for the explanation.

const servers: ServerInterface[] = this.servers();
this.components().servers().forEach(server =>
!servers.some(s => s.json() === server.json()) && servers.push(server)
);
return new Servers(servers);
}

allChannels(): ChannelsInterface {
const channels: ChannelInterface[] = this.channels();
this.components().channels().forEach(channel =>
!channels.some(c => c.json() === channel.json()) && channels.push(channel)
);
return new Channels(channels);
}

allOperations(): OperationsInterface {
const operations: OperationInterface[] = [];
this.allChannels().forEach(channel => operations.push(...channel.operations()));
return new Operations(operations);
}

allMessages(): MessagesInterface {
const messages: MessageInterface[] = [];
this.allOperations().forEach(operation => operation.messages().forEach(message => (
!messages.some(m => m.json() === message.json()) && messages.push(message)
)));
this.components().messages().forEach(message => (
!messages.some(m => m.json() === message.json()) && messages.push(message)
));
return new Messages(messages);
}

allSchemas(): SchemasInterface {
return this.__schemas(true);
}

extensions(): ExtensionsInterface {
return extensions(this);
}

private __schemas(withComponents: boolean) {
const schemas: Set<SchemaInterface> = new Set();
function callback(schema: SchemaInterface) {
if (!schemas.has(schema.json())) {
schemas.add(schema);
}
}

let toIterate = Object.values(SchemaTypesToIterate);
if (!withComponents) {
toIterate = toIterate.filter(s => s !== SchemaTypesToIterate.Components);
}
traverseAsyncApiDocument(this, callback, toIterate);
return new Schemas(Array.from(schemas));
}
}
8 changes: 3 additions & 5 deletions src/models/v2/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import type { OperationTraitsInterface } from '../operation-traits';
import type { SecuritySchemesInterface } from '../security-schemes';
import type { MessageTraitsInterface } from '../message-traits';
import type { OperationsInterface } from '../operations';
import type { OperationInterface } from '../operation';
import type { CorrelationIdsInterface } from '../correlation-ids';

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

Expand Down Expand Up @@ -76,9 +76,7 @@ export class Components extends BaseModel<v2.ComponentsObject> implements Compon
}

operations(): OperationsInterface {
const operations: OperationInterface[] = [];
this.channels().forEach(channel => operations.push(...channel.operations().all()));
return new Operations(operations);
return new Operations([]);
Copy link
Member Author

Choose a reason for hiding this comment

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

We don't have operations in components in v2, so we should return empty collection.

}

operationTraits(): OperationTraitsInterface {
Expand All @@ -89,7 +87,7 @@ export class Components extends BaseModel<v2.ComponentsObject> implements Compon
return this.createCollection('messageTraits', MessageTraits, MessageTrait);
}

correlationIds(): CorrelationIds {
correlationIds(): CorrelationIdsInterface {
return this.createCollection('correlationIds', CorrelationIds, CorrelationId);
}

Expand Down
2 changes: 1 addition & 1 deletion src/models/v2/message-trait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type { v2 } from '../../spec-types';

export class MessageTrait<J extends v2.MessageTraitObject = v2.MessageTraitObject> extends BaseModel<J, { id: string }> implements MessageTraitInterface {
id(): string {
return this.messageId() || this._meta.id || this.extensions().get(xParserMessageName)?.value<string>() as string;
return this.messageId() || this._meta.id || this.json(xParserMessageName) as string;
}

schemaFormat(): string {
Expand Down
2 changes: 1 addition & 1 deletion src/models/v2/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class Schema extends BaseModel<v2.AsyncAPISchemaObject, { id?: string, pa
}

uid(): string {
return this._meta.id || this.extensions().get(xParserSchemaId)?.value<string>() as string;
return this._meta.id || this.json(xParserSchemaId as any) as string;
}

$comment(): string | undefined {
Expand Down
144 changes: 143 additions & 1 deletion test/models/v2/asyncapi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Components } from '../../../src/models/v2/components';
import { Info } from '../../../src/models/v2/info';
import { Messages } from '../../../src/models/v2/messages';
import { Operations } from '../../../src/models/v2/operations';
import { Schemas } from '../../../src/models/v2/schemas';
import { SecuritySchemes } from '../../../src/models/v2/security-schemes';
import { Servers } from '../../../src/models/v2/servers';

Expand Down Expand Up @@ -117,6 +118,14 @@ describe('AsyncAPIDocument model', function() {
expect(d.messages()).toHaveLength(4);
});

it('should return a collection of messages without duplication', function() {
const message = {};
const doc = serializeInput<v2.AsyncAPIObject>({ channels: { 'user/signup': { publish: { message }, subscribe: { message: { oneOf: [{}, message] } } }, 'user/logout': { publish: { message } } } });
const d = new AsyncAPIDocument(doc);
expect(d.messages()).toBeInstanceOf(Messages);
expect(d.messages()).toHaveLength(2);
});

it('should return a collection of messages even if messages are not defined', function() {
const doc = serializeInput<v2.AsyncAPIObject>({});
const d = new AsyncAPIDocument(doc);
Expand All @@ -125,7 +134,25 @@ describe('AsyncAPIDocument model', function() {
});

describe('.schemas()', function() {
it.todo('should return a collection of schemas');
it('should return a collection of schemas', function() {
const doc = serializeInput<v2.AsyncAPIObject>({ channels: { 'user/signup': { publish: { message: { payload: {} } }, subscribe: { message: { oneOf: [{ payload: {} }, {}, { payload: {} }] } } }, 'user/logout': { publish: { message: { payload: {} } } } } });
const d = new AsyncAPIDocument(doc);
expect(d.schemas()).toBeInstanceOf(Schemas);
expect(d.schemas()).toHaveLength(4);
});

it('should return only an "used" schemas (without schemas from components)', function() {
const doc = serializeInput<v2.AsyncAPIObject>({ channels: { 'user/signup': { publish: { message: { payload: {} } }, subscribe: { message: { oneOf: [{ payload: {} }, {}] } } }, 'user/logout': { publish: { message: { payload: {} } } } }, components: { schemas: { someSchema1: {}, someSchema2: {} } } });
const d = new AsyncAPIDocument(doc);
expect(d.schemas()).toBeInstanceOf(Schemas);
expect(d.schemas()).toHaveLength(3);
});

it('should return a collection of schemas even if collection is empty', function() {
const doc = serializeInput<v2.AsyncAPIObject>({});
const d = new AsyncAPIDocument(doc);
expect(d.schemas()).toBeInstanceOf(Schemas);
});
});

describe('.securitySchemes()', function() {
Expand Down Expand Up @@ -157,6 +184,121 @@ describe('AsyncAPIDocument model', function() {
});
});

describe('.allServers()', function() {
it('should return a collection of servers', function() {
const doc = serializeInput<v2.AsyncAPIObject>({ servers: { development: {} } });
const d = new AsyncAPIDocument(doc);
expect(d.allServers()).toBeInstanceOf(Servers);
expect(d.allServers()).toHaveLength(1);
expect(d.allServers().all()[0].id()).toEqual('development');
});

it('should return all servers (with servers from components)', function() {
const doc = serializeInput<v2.AsyncAPIObject>({ servers: { production: {} }, components: { servers: { development: {} } } });
const d = new AsyncAPIDocument(doc);
expect(d.allServers()).toBeInstanceOf(Servers);
expect(d.allServers()).toHaveLength(2);
});

it('should return a collection of servers even if servers are not defined', function() {
const doc = serializeInput<v2.AsyncAPIObject>({});
const d = new AsyncAPIDocument(doc);
expect(d.allServers()).toBeInstanceOf(Servers);
});
});

describe('.allChannels()', function() {
it('should return a collection of channels', function() {
const doc = serializeInput<v2.AsyncAPIObject>({ channels: { 'user/signup': {} } });
const d = new AsyncAPIDocument(doc);
expect(d.allChannels()).toBeInstanceOf(Channels);
expect(d.allChannels()).toHaveLength(1);
expect(d.allChannels().all()[0].address()).toEqual('user/signup');
});

it('should return all channels (with channels from components)', function() {
const doc = serializeInput<v2.AsyncAPIObject>({ channels: { 'user/signup': {} }, components: { channels: { someChannel1: {}, someChannel2: {} } } });
const d = new AsyncAPIDocument(doc);
expect(d.allChannels()).toBeInstanceOf(Channels);
expect(d.allChannels()).toHaveLength(3);
});

it('should return a collection of channels even if channels are not defined', function() {
const doc = serializeInput<v2.AsyncAPIObject>({});
const d = new AsyncAPIDocument(doc);
expect(d.allChannels()).toBeInstanceOf(Channels);
});
});

describe('.allOperations()', function() {
it('should return a collection of operations', function() {
const doc = serializeInput<v2.AsyncAPIObject>({ channels: { 'user/signup': { publish: {}, subscribe: {} }, 'user/logout': { publish: {} } } });
const d = new AsyncAPIDocument(doc);
expect(d.allOperations()).toBeInstanceOf(Operations);
expect(d.allOperations()).toHaveLength(3);
});

it('should return all operations (with operations from components)', function() {
const channel = { publish: {} };
const doc = serializeInput<v2.AsyncAPIObject>({ channels: { 'user/signup': { publish: {}, subscribe: {} }, 'user/logout': channel }, components: { channels: { someChannel: { publish: {}, subscribe: {} }, existingOne: channel } } });
const d = new AsyncAPIDocument(doc);
expect(d.allOperations()).toBeInstanceOf(Operations);
expect(d.allOperations()).toHaveLength(5);
});

it('should return a collection of operations even if operations are not defined', function() {
const doc = serializeInput<v2.AsyncAPIObject>({});
const d = new AsyncAPIDocument(doc);
expect(d.allOperations()).toBeInstanceOf(Operations);
});
});

describe('.allMessages()', function() {
it('should return a collection of messages', function() {
const doc = serializeInput<v2.AsyncAPIObject>({ channels: { 'user/signup': { publish: { message: {} }, subscribe: { message: { oneOf: [{}, {}] } } }, 'user/logout': { publish: { message: {} } } } });
const d = new AsyncAPIDocument(doc);
expect(d.allMessages()).toBeInstanceOf(Messages);
expect(d.allMessages()).toHaveLength(4);
});

it('should return all messages (with messages from components)', function() {
const message = {};
const channel = { publish: { message }, subscribe: { message: { oneOf: [{ payload: {} }, message] } } };
const doc = serializeInput<v2.AsyncAPIObject>({ channels: { 'user/signup': channel, 'user/logout': { publish: { message: { payload: {} } } } }, components: { channels: { someChannel: channel, anotherChannel: { publish: { message: {} } } }, messages: { someMessage: message, anotherMessage1: {}, anotherMessage2: {} } } });
const d = new AsyncAPIDocument(doc);
expect(d.allMessages()).toBeInstanceOf(Messages);
expect(d.allMessages()).toHaveLength(6);
});

it('should return a collection of messages even if messages are not defined', function() {
const doc = serializeInput<v2.AsyncAPIObject>({});
const d = new AsyncAPIDocument(doc);
expect(d.allMessages()).toBeInstanceOf(Messages);
});
});

describe('.allSchemas()', function() {
it('should return a collection of schemas', function() {
const doc = serializeInput<v2.AsyncAPIObject>({ channels: { 'user/signup': { publish: { message: { payload: {} } }, subscribe: { message: { oneOf: [{ payload: {} }, {}, { payload: {} }] } } }, 'user/logout': { publish: { message: { payload: {} } } } } });
const d = new AsyncAPIDocument(doc);
expect(d.allSchemas()).toBeInstanceOf(Schemas);
expect(d.allSchemas()).toHaveLength(4);
});

it('should return all schemas (with schemas from components)', function() {
const doc = serializeInput<v2.AsyncAPIObject>({ channels: { 'user/signup': { publish: { message: { payload: {} } }, subscribe: { message: { oneOf: [{ payload: {} }, {}] } } }, 'user/logout': { publish: { message: { payload: {} } } } }, components: { schemas: { someSchema1: {}, someSchema2: {} } } });
const d = new AsyncAPIDocument(doc);
expect(d.allSchemas()).toBeInstanceOf(Schemas);
expect(d.allSchemas()).toHaveLength(5);
});

it('should return a collection of schemas even if collection is empty', function() {
const doc = serializeInput<v2.AsyncAPIObject>({});
const d = new AsyncAPIDocument(doc);
expect(d.allSchemas()).toBeInstanceOf(Schemas);
});
});

describe('mixins', function() {
assertExtensions(AsyncAPIDocument);
});
Expand Down
16 changes: 2 additions & 14 deletions test/models/v2/components.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,20 +181,8 @@ describe('Components model', function() {
});

describe('.operations()', function() {
it('should return Operations with Operation Object', function() {
const doc = { channels: { channel: { publish: {} } } };
const d = new Components(doc);
const expectedOperations: Operation[] = [
new Operation({}, {action: 'publish', id: 'channel_publish', pointer: '/components/channels/channel/publish'} as ModelMetadata & { id: string, action: OperationAction })
];

const operations = d.operations();
expect(operations).toBeInstanceOf(Operations);
expect(operations.all()).toEqual(expectedOperations);
});

it('should return Operations with empty operation objects when operations are not defined in channels', function() {
const doc = { channels: { channel: {} } };
it('should return Operations with empty collection', function() {
const doc = {};
const d = new Components(doc);
const operations = d.operations();
expect(operations).toBeInstanceOf(Operations);
Expand Down