diff --git a/src/custom-operations/index.ts b/src/custom-operations/index.ts index 8303eafc1..ee7b33167 100644 --- a/src/custom-operations/index.ts +++ b/src/custom-operations/index.ts @@ -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); } diff --git a/src/models/asyncapi.ts b/src/models/asyncapi.ts index 4787ad961..d047d5e0d 100644 --- a/src/models/asyncapi.ts +++ b/src/models/asyncapi.ts @@ -23,4 +23,9 @@ export interface AsyncAPIDocumentInterface extends BaseModel, schemas(): SchemasInterface; securitySchemes(): SecuritySchemesInterface; components(): ComponentsInterface; + allServers(): ServersInterface; + allChannels(): ChannelsInterface; + allOperations(): OperationsInterface; + allMessages(): MessagesInterface; + allSchemas(): SchemasInterface; } diff --git a/src/models/v2/asyncapi.ts b/src/models/v2/asyncapi.ts index fc14f4689..5c1359c9e 100644 --- a/src/models/v2/asyncapi.ts +++ b/src/models/v2/asyncapi.ts @@ -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'; @@ -65,18 +68,20 @@ export class AsyncAPIDocument extends BaseModel 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 { @@ -91,7 +96,60 @@ export class AsyncAPIDocument extends BaseModel 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 = 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)); + } } diff --git a/src/models/v2/components.ts b/src/models/v2/components.ts index b6f16f425..f13034c3f 100644 --- a/src/models/v2/components.ts +++ b/src/models/v2/components.ts @@ -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'; @@ -76,9 +76,7 @@ export class Components extends BaseModel 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 { @@ -89,7 +87,7 @@ export class Components extends BaseModel implements Compon return this.createCollection('messageTraits', MessageTraits, MessageTrait); } - correlationIds(): CorrelationIds { + correlationIds(): CorrelationIdsInterface { return this.createCollection('correlationIds', CorrelationIds, CorrelationId); } diff --git a/src/models/v2/message-trait.ts b/src/models/v2/message-trait.ts index 914757e79..299b7856c 100644 --- a/src/models/v2/message-trait.ts +++ b/src/models/v2/message-trait.ts @@ -21,7 +21,7 @@ import type { v2 } from '../../spec-types'; export class MessageTrait extends BaseModel implements MessageTraitInterface { id(): string { - return this.messageId() || this._meta.id || this.extensions().get(xParserMessageName)?.value() as string; + return this.messageId() || this._meta.id || this.json(xParserMessageName) as string; } schemaFormat(): string { diff --git a/src/models/v2/schema.ts b/src/models/v2/schema.ts index ee551ae63..649c543d7 100644 --- a/src/models/v2/schema.ts +++ b/src/models/v2/schema.ts @@ -21,7 +21,7 @@ export class Schema extends BaseModel() as string; + return this._meta.id || this.json(xParserSchemaId as any) as string; } $comment(): string | undefined { diff --git a/test/models/v2/asyncapi.spec.ts b/test/models/v2/asyncapi.spec.ts index 61f04e708..71e4e6ce1 100644 --- a/test/models/v2/asyncapi.spec.ts +++ b/test/models/v2/asyncapi.spec.ts @@ -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'; @@ -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({ 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({}); const d = new AsyncAPIDocument(doc); @@ -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({ 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({ 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({}); + const d = new AsyncAPIDocument(doc); + expect(d.schemas()).toBeInstanceOf(Schemas); + }); }); describe('.securitySchemes()', function() { @@ -157,6 +184,121 @@ describe('AsyncAPIDocument model', function() { }); }); + describe('.allServers()', function() { + it('should return a collection of servers', function() { + const doc = serializeInput({ 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({ 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({}); + const d = new AsyncAPIDocument(doc); + expect(d.allServers()).toBeInstanceOf(Servers); + }); + }); + + describe('.allChannels()', function() { + it('should return a collection of channels', function() { + const doc = serializeInput({ 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({ 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({}); + const d = new AsyncAPIDocument(doc); + expect(d.allChannels()).toBeInstanceOf(Channels); + }); + }); + + describe('.allOperations()', function() { + it('should return a collection of operations', function() { + const doc = serializeInput({ 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({ 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({}); + const d = new AsyncAPIDocument(doc); + expect(d.allOperations()).toBeInstanceOf(Operations); + }); + }); + + describe('.allMessages()', function() { + it('should return a collection of messages', function() { + const doc = serializeInput({ 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({ 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({}); + const d = new AsyncAPIDocument(doc); + expect(d.allMessages()).toBeInstanceOf(Messages); + }); + }); + + describe('.allSchemas()', function() { + it('should return a collection of schemas', function() { + const doc = serializeInput({ 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({ 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({}); + const d = new AsyncAPIDocument(doc); + expect(d.allSchemas()).toBeInstanceOf(Schemas); + }); + }); + describe('mixins', function() { assertExtensions(AsyncAPIDocument); }); diff --git a/test/models/v2/components.spec.ts b/test/models/v2/components.spec.ts index bc1a82b35..b7effc4be 100644 --- a/test/models/v2/components.spec.ts +++ b/test/models/v2/components.spec.ts @@ -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);