Skip to content

Commit

Permalink
refactor: implement schemas and all* methods in AsyncAPIDocument class (
Browse files Browse the repository at this point in the history
  • Loading branch information
magicmatatjahu authored Oct 4, 2022
1 parent 73bd4fb commit 562360c
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 26 deletions.
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 {
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([]);
}

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

0 comments on commit 562360c

Please sign in to comment.