Repository: asyncapi/parser-js
Pull request #822 (2023-08-01 09:54:01)

refactor: match Spectral formats based on schemas found in @asyncapi/specs pkg
**Description**

This PR removes the need for creating new Spectral ruleset formats matching new AsyncAPI versions.
This translates to the removal of the requirement of making changes every time we add a new version into @asyncapi/specs package. It might be some exceptions in the case we, for example, add a new major spec version, where rules won't apply, and manual changes (and expected I would say) will need to be done, but will be close to as simple as including that new version in the allowed formats in ruleset.

This PR enables future support for v3 of the spec in a very easy way (will come in a new PR).

**Related issue(s)**

https://github.com/asyncapi/parser-js/issues/817


cc @jonaslagoni @magicmatatjahu @Amzani @fmvilas


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=asyncapi_parser-js&pullRequest=822)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=822) No Coverage information  
[![1.2%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '1.2%')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=822&metric=new_duplicated_lines_density&view=list) [1.2% Duplication](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=822&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=asyncapi_parser-js&pullRequest=822)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=822&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=822) No Coverage information  
[![1.2%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '1.2%')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=822&metric=new_duplicated_lines_density&view=list) [1.2% Duplication](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=822&metric=new_duplicated_lines_density&view=list)


jonaslagoni: /rtm

Patch:
diff --git a/src/ruleset/formats.ts b/src/ruleset/formats.ts
index 7abda41..a0d0693 100644
--- a/src/ruleset/formats.ts
+++ b/src/ruleset/formats.ts
@@ -1,51 +1,57 @@
-/* eslint-disable security/detect-unsafe-regex */
-
-import { isObject } from '../utils';
+import { getSemver, isObject } from '../utils';
+import { schemas } from '@asyncapi/specs';
 
 import type { Format } from '@stoplight/spectral-core';
 import type { MaybeAsyncAPI } from '../types';
 
-const aas2Regex = /^2\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
-const aas2_0Regex = /^2\.0(?:\.[0-9]*)?$/;
-const aas2_1Regex = /^2\.1(?:\.[0-9]*)?$/;
-const aas2_2Regex = /^2\.2(?:\.[0-9]*)?$/;
-const aas2_3Regex = /^2\.3(?:\.[0-9]*)?$/;
-const aas2_4Regex = /^2\.4(?:\.[0-9]*)?$/;
-const aas2_5Regex = /^2\.5(?:\.[0-9]*)?$/;
-const aas2_6Regex = /^2\.6(?:\.[0-9]*)?$/;
-
-const isAas2 = (document: unknown): document is { asyncapi: string } & Record<string, unknown> =>
-  isObject(document) && 'asyncapi' in document && aas2Regex.test(String((document as MaybeAsyncAPI).asyncapi));
-
-export const aas2: Format = isAas2;
-aas2.displayName = 'AsyncAPI 2.x';
-
-export const aas2_0: Format = (document: unknown): boolean =>
-  isAas2(document) && aas2_0Regex.test(String((document as MaybeAsyncAPI).asyncapi));
-aas2_0.displayName = 'AsyncAPI 2.0.x';
-
-export const aas2_1: Format = (document: unknown): boolean =>
-  isAas2(document) && aas2_1Regex.test(String((document as MaybeAsyncAPI).asyncapi));
-aas2_1.displayName = 'AsyncAPI 2.1.x';
-
-export const aas2_2: Format = (document: unknown): boolean =>
-  isAas2(document) && aas2_2Regex.test(String((document as MaybeAsyncAPI).asyncapi));
-aas2_2.displayName = 'AsyncAPI 2.2.x';
-
-export const aas2_3: Format = (document: unknown): boolean =>
-  isAas2(document) && aas2_3Regex.test(String((document as MaybeAsyncAPI).asyncapi));
-aas2_3.displayName = 'AsyncAPI 2.3.x';
-
-export const aas2_4: Format = (document: unknown): boolean =>
-  isAas2(document) && aas2_4Regex.test(String((document as MaybeAsyncAPI).asyncapi));
-aas2_4.displayName = 'AsyncAPI 2.4.x';
-
-export const aas2_5: Format = (document: unknown): boolean =>
-  isAas2(document) && aas2_5Regex.test(String((document as MaybeAsyncAPI).asyncapi));
-aas2_5.displayName = 'AsyncAPI 2.5.x';
-
-export const aas2_6: Format = (document: unknown): boolean =>
-  isAas2(document) && aas2_6Regex.test(String((document as MaybeAsyncAPI).asyncapi));
-aas2_6.displayName = 'AsyncAPI 2.6.x';
+export class Formats extends Map<string, Format> {
+  filterByMajorVersions(majorsToInclude: string[]): Formats {
+    return new Formats([...this.entries()].filter(element => {return majorsToInclude.includes(element[0].split('.')[0]);}));
+  }
+
+  excludeByVersions(versionsToExclude: string[]): Formats {
+    return new Formats([...this.entries()].filter(element => {return !versionsToExclude.includes(element[0]);}));
+  }
+
+  find(version: string): Format | undefined {
+    return this.get(formatVersion(version));
+  }
+
+  formats(): Format[] {
+    return [...this.values()];
+  } 
+}
+
+export const AsyncAPIFormats = new Formats(Object.entries(schemas).reverse().map(([version]) => [version, createFormat(version)])); // reverse is used for giving newer versions a higher priority when matching
+
+function isAsyncAPIVersion(versionToMatch: string, document: unknown): document is { asyncapi: string } & Record<string, unknown> {
+  const asyncAPIDoc = document as MaybeAsyncAPI;
+  if (!asyncAPIDoc) return false;
+
+  const documentVersion = String(asyncAPIDoc.asyncapi);
+  return isObject(document) && 'asyncapi' in document 
+    && assertValidAsyncAPIVersion(documentVersion)
+    && versionToMatch === formatVersion(documentVersion);
+}
+  
+function assertValidAsyncAPIVersion(documentVersion: string): boolean {
+  const semver = getSemver(documentVersion);
+  const regexp = new RegExp(`^(${semver.major})\\.(${semver.minor})\\.(0|[1-9][0-9]*)$`); // eslint-disable-line security/detect-non-literal-regexp
+  return regexp.test(documentVersion);
+}
+
+function createFormat(version: string): Format {
+  const format: Format = (document: unknown): boolean =>
+    isAsyncAPIVersion(version, document);
+  
+  const semver = getSemver(version);
+  format.displayName = `AsyncAPI ${semver.major}.${semver.minor}.x`;
+
+  return format; 
+}
+
+const formatVersion = function (version: string): string {
+  const versionSemver = getSemver(version);
+  return `${versionSemver.major}.${versionSemver.minor}.0`;
+};
 
-export const aas2All = [aas2_0, aas2_1, aas2_2, aas2_3, aas2_4, aas2_5, aas2_6];
diff --git a/src/ruleset/functions/documentStructure.ts b/src/ruleset/functions/documentStructure.ts
index e6f6c08..a00268d 100644
--- a/src/ruleset/functions/documentStructure.ts
+++ b/src/ruleset/functions/documentStructure.ts
@@ -1,10 +1,10 @@
 import specs from '@asyncapi/specs';
 import { createRulesetFunction } from '@stoplight/spectral-core';
 import { schema as schemaFn } from '@stoplight/spectral-functions';
-import { aas2_0, aas2_1, aas2_2, aas2_3, aas2_4, aas2_5, aas2_6 } from '../formats';
 
 import type { ErrorObject } from 'ajv';
 import type { IFunctionResult, Format } from '@stoplight/spectral-core';
+import { AsyncAPIFormats } from '../formats';
 
 type AsyncAPIVersions = keyof typeof specs.schemas;
 
@@ -80,24 +80,11 @@ function filterRefErrors(errors: IFunctionResult[], resolved: boolean) {
     });
 }
 
-function getSchema(formats: Set<Format>): Record<string, any> | void {
-  switch (true) {
-  case formats.has(aas2_6):
-    return getSerializedSchema('2.6.0');
-  case formats.has(aas2_5):
-    return getSerializedSchema('2.5.0');
-  case formats.has(aas2_4):
-    return getSerializedSchema('2.4.0');
-  case formats.has(aas2_3):
-    return getSerializedSchema('2.3.0');
-  case formats.has(aas2_2):
-    return getSerializedSchema('2.2.0');
-  case formats.has(aas2_1):
-    return getSerializedSchema('2.1.0');
-  case formats.has(aas2_0):
-    return getSerializedSchema('2.0.0');
-  default:
-    return;
+export function getSchema(docFormats: Set<Format>): Record<string, any> | void {
+  for (const [version, format] of AsyncAPIFormats) {
+    if (docFormats.has(format)) {
+      return getSerializedSchema(version as AsyncAPIVersions);
+    }
   }
 }
 
diff --git a/src/ruleset/functions/unusedComponent.ts b/src/ruleset/functions/unusedComponent.ts
index 0be382d..0040204 100644
--- a/src/ruleset/functions/unusedComponent.ts
+++ b/src/ruleset/functions/unusedComponent.ts
@@ -1,6 +1,5 @@
 import { unreferencedReusableObject } from '@stoplight/spectral-functions';
 import { createRulesetFunction } from '@stoplight/spectral-core';
-import { aas2 } from '../formats';
 import { isObject } from '../../utils';
 
 import type { IFunctionResult } from '@stoplight/spectral-core';
@@ -23,9 +22,9 @@ export const unusedComponent = createRulesetFunction<{ components: Record<string
 
     const results: IFunctionResult[] = [];
     Object.keys(components).forEach(componentType => {
-      // if component type is `securitySchemes` and we operate on AsyncAPI 2.x.x skip validation
-      // security schemes in 2.x.x are referenced by keys, not by object ref - for this case we have a separate `asyncapi2-unused-securityScheme` rule
-      if (componentType === 'securitySchemes' && aas2(targetVal, null)) {
+      // if component type is `securitySchemes` we skip the validation
+      // security schemes in >=2.x.x are referenced by keys, not by object ref - for this case we have a separate `asyncapi2-unused-securityScheme` rule
+      if (componentType === 'securitySchemes') {
         return;
       }
 
diff --git a/src/ruleset/ruleset.ts b/src/ruleset/ruleset.ts
index 0f87c3d..c24ebfd 100644
--- a/src/ruleset/ruleset.ts
+++ b/src/ruleset/ruleset.ts
@@ -1,4 +1,4 @@
-import { aas2All as aas2AllFormats } from './formats';
+
 import { lastVersion } from '../constants';
 import { truthy, schema } from '@stoplight/spectral-functions';
 
@@ -6,10 +6,11 @@ import { documentStructure } from './functions/documentStructure';
 import { internal } from './functions/internal';
 import { isAsyncAPIDocument } from './functions/isAsyncAPIDocument';
 import { unusedComponent } from './functions/unusedComponent';
+import { AsyncAPIFormats } from './formats';
 
 export const coreRuleset = {
   description: 'Core AsyncAPI x.x.x ruleset.',
-  formats: [...aas2AllFormats],
+  formats: AsyncAPIFormats.filterByMajorVersions(['2']).formats(), // Validation for AsyncAPI v3 is still WIP.
   rules: {
     /**
      * Root Object rules
@@ -80,7 +81,7 @@ export const coreRuleset = {
 
 export const recommendedRuleset = {
   description: 'Recommended AsyncAPI x.x.x ruleset.',
-  formats: [...aas2AllFormats],
+  formats: AsyncAPIFormats.filterByMajorVersions(['2']).formats(), // Validation for AsyncAPI v3 is still WIP.
   rules: {
     /**
      * Root Object rules
@@ -188,6 +189,7 @@ export const recommendedRuleset = {
      */
     'asyncapi-unused-component': {
       description: 'Potentially unused component has been detected in AsyncAPI document.',
+      formats: AsyncAPIFormats.filterByMajorVersions(['2']).formats(), // Validation for AsyncAPI v3 is still WIP.
       recommended: true,
       resolved: false,
       severity: 'info',
diff --git a/src/ruleset/v2/ruleset.ts b/src/ruleset/v2/ruleset.ts
index 419a6a0..a9c6096 100644
--- a/src/ruleset/v2/ruleset.ts
+++ b/src/ruleset/v2/ruleset.ts
@@ -1,6 +1,6 @@
 /* eslint-disable sonarjs/no-duplicate-string */
 
-import { aas2All as aas2AllFormats } from '../formats';
+import { AsyncAPIFormats } from '../formats';
 import { truthy, pattern } from '@stoplight/spectral-functions';
 
 import { channelParameters } from './functions/channelParameters';
@@ -20,7 +20,7 @@ import type { Parser } from '../../parser';
 
 export const v2CoreRuleset = {
   description: 'Core AsyncAPI 2.x.x ruleset.',
-  formats: [...aas2AllFormats],
+  formats: AsyncAPIFormats.filterByMajorVersions(['2']).formats(),
   rules: {
     /**
      * Server Object rules
@@ -191,7 +191,6 @@ export const v2CoreRuleset = {
 export const v2SchemasRuleset = (parser: Parser) => {
   return {
     description: 'Schemas AsyncAPI 2.x.x ruleset.',
-    formats: [...aas2AllFormats],
     rules: {
       'asyncapi2-schemas': asyncApi2SchemaParserRule(parser),
       'asyncapi2-schema-default': {
@@ -244,7 +243,7 @@ export const v2SchemasRuleset = (parser: Parser) => {
 
 export const v2RecommendedRuleset = {
   description: 'Recommended AsyncAPI 2.x.x ruleset.',
-  formats: [...aas2AllFormats],
+  formats: AsyncAPIFormats.filterByMajorVersions(['2']).formats(),
   rules: {
     /**
      * Root Object rules
@@ -334,7 +333,7 @@ export const v2RecommendedRuleset = {
     'asyncapi2-message-messageId': {
       description: 'Message should have a "messageId" field defined.',
       recommended: true,
-      formats: aas2AllFormats.slice(4), // from 2.4.0
+      formats: AsyncAPIFormats.filterByMajorVersions(['2']).excludeByVersions(['2.0.0', '2.1.0', '2.2.0', '2.3.0']).formats(), // message.messageId is available starting from v2.4.      
       given: [
         '$.channels.*.[publish,subscribe][?(@property === "message" && @.oneOf == void 0)]',
         '$.channels.*.[publish,subscribe].message.oneOf.*',


Test patch:
diff --git a/test/parse.spec.ts b/test/parse.spec.ts
index cdca94a..265ffb3 100644
--- a/test/parse.spec.ts
+++ b/test/parse.spec.ts
@@ -1,13 +1,13 @@
 import { Document } from '@stoplight/spectral-core';
 
-import { AsyncAPIDocumentV2 } from '../src/models';
+import { AsyncAPIDocumentV2, AsyncAPIDocumentV3 } from '../src/models';
 import { Parser } from '../src/parser';
 import { xParserApiVersion } from '../src/constants';
 
 describe('parse()', function() {
   const parser = new Parser();
 
-  it('should parse valid document', async function() {
+  it('should parse valid document', async function() { 
     const documentRaw = {
       asyncapi: '2.0.0',
       info: {
@@ -22,6 +22,21 @@ describe('parse()', function() {
     expect(diagnostics.length > 0).toEqual(true);
   });
 
+  it('should not parse valid v3 document', async function() { 
+    const documentRaw = {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Valid AsyncApi document',
+        version: '1.0',
+      },
+      channels: {}
+    };
+    const { document, diagnostics } = await parser.parse(documentRaw);
+    expect(document).toEqual(undefined);
+    expect(diagnostics.length > 0).toEqual(true);
+    expect(diagnostics[0].message).toContain('Version "3.0.0" is not supported');
+  });
+
   it('should parse invalid document', async function() {
     const documentRaw = {
       asyncapi: '2.0.0',
diff --git a/test/ruleset/formats.spec.ts b/test/ruleset/formats.spec.ts
index 5de1168..b2639a0 100644
--- a/test/ruleset/formats.spec.ts
+++ b/test/ruleset/formats.spec.ts
@@ -1,117 +1,82 @@
-import { aas2, aas2_0, aas2_1, aas2_2, aas2_3, aas2_4, aas2_5, aas2_6 } from '../../src/ruleset/formats';
+import { schemas } from '@asyncapi/specs';
+import { AsyncAPIFormats, Formats } from '../../src/ruleset/formats';
+import { getSemver } from '../../src/utils';
 
-describe('AsyncAPI format', () => {
-  describe('AsyncAPI 2.x', () => {
-    it.each(['2.0.0', '2.1.0', '2.2.0', '2.3.0', '2.0.17', '2.1.37', '2.9.0', '2.9.3'])(
-      'recognizes %s version correctly',
-      version => {
-        expect(aas2({ asyncapi: version }, null)).toBe(true);
-      },
-    );
+import type { Format } from '@stoplight/spectral-core';
 
+describe('AsyncAPI format', () => {
+  describe('Recognizes versions', () => {
     const testCases = [
-      { asyncapi: '3.0' },
-      { asyncapi: '3.0.0' },
-      { asyncapi: '2' },
-      { asyncapi: '2.0' },
-      { asyncapi: '2.0.' },
-      { asyncapi: '2.0.01' },
-      { asyncapi: '1.0' },
-      { asyncapi: 2 },
-      { asyncapi: null },
-      { openapi: '4.0' },
-      { openapi: '2.0' },
-      { openapi: null },
-      { swagger: null },
-      { swagger: '3.0' },
-      {},
-      null,
+      { formatVersion: '2.0.0', document: {asyncapi: '2.0.0'}, existsFormat: true, result: true },
+      { formatVersion: '2.0.0', document: {asyncapi: '2.1.8'}, existsFormat: true, result: false },
+      { formatVersion: '2.1.8', document: {asyncapi: '2.0.0'}, existsFormat: true, result: false },
+      { formatVersion: '2.1.3', document: {asyncapi: '2.1.3'}, existsFormat: true, result: true },
+      { formatVersion: '2.1.3', document: {asyncapi: '2.0.0'}, existsFormat: true, result: false },
+      { formatVersion: '2.0.0', document: {asyncapi: '2.1.3'}, existsFormat: true, result: false },
+      { formatVersion: '2.2.9', document: {asyncapi: '2.2.9'}, existsFormat: true, result: true },
+      { formatVersion: '2.2.9', document: {asyncapi: '2.0.0'}, existsFormat: true, result: false },
+      { formatVersion: '2.0.0', document: {asyncapi: '2.2.9'}, existsFormat: true, result: false },
+      { formatVersion: '2.6.5', document: {asyncapi: '2.6.5'}, existsFormat: true, result: true },
+      { formatVersion: '2.6.5', document: {asyncapi: '2.0.0'}, existsFormat: true, result: false },
+      { formatVersion: '2.0.0', document: {asyncapi: '2.6.5'}, existsFormat: true, result: false },
+      { formatVersion: '3.0.10', document: {asyncapi: '3.0.10'}, existsFormat: false, result: false },
+      { formatVersion: '3.0.0', document: {openapi: '3.0.0'}, existsFormat: false, result: false },
+      { formatVersion: '3.0.0', document: null, existsFormat: false, result: false },
+      { formatVersion: '999.999.0', document: {}, existsFormat: false, result: false },
+      { formatVersion: '19923.1.0', document: {}, existsFormat: false, result: false },
+      { formatVersion: '2.99.0', document: {}, existsFormat: false, result: false },
     ];
-
-    it.each(testCases)('does not recognize invalid document %o', document => {
-      expect(aas2(document, null)).toBe(false);
+      
+    it.each(testCases)('format formatVersion recognizes version %p correctly', testCase => {
+      const format = AsyncAPIFormats.find(testCase.formatVersion);
+      expect(format !== undefined).toEqual(testCase.existsFormat);
+      if (format !== undefined) {
+        expect(format(testCase.document, null)).toEqual(testCase.result);
+      }
     });
   });
+});
 
-  describe('AsyncAPI 2.0', () => {
-    it.each(['2.0.0', '2.0.3'])('recognizes %s version correctly', version => {
-      expect(aas2_0({ asyncapi: version }, null)).toBe(true);
-    });
-
-    it.each(['2', '2.0', '2.1.0', '2.1.3'])('does not recognize %s version', version => {
-      expect(aas2_0({ asyncapi: version }, null)).toBe(false);
-    });
+describe('AsyncAPIFormats collection', () => {
+  it('Is a Formats collection', () => {
+    expect(AsyncAPIFormats).toBeInstanceOf(Formats);
   });
 
-  describe('AsyncAPI 2.1', () => {
-    it.each(['2.1.0', '2.1.37'])('recognizes %s version correctly', version => {
-      expect(aas2_1({ asyncapi: version }, null)).toBe(true);
-    });
-
-    it.each(['2', '2.1', '2.0.0', '2.2.0', '2.2.3'])('does not recognize %s version', version => {
-      expect(aas2_1({ asyncapi: version }, null)).toBe(false);
-    });
+  it('Returns all formats as array', () => {
+    const formats = AsyncAPIFormats.formats();
+    expect(formats).toHaveLength(Object.keys(schemas).length);    
   });
 
-  describe('AsyncAPI 2.2', () => {
-    it.each(['2.2.0', '2.2.3'])('recognizes %s version correctly', version => {
-      expect(aas2_2({ asyncapi: version }, null)).toBe(true);
-    });
-
-    it.each(['2', '2.2', '2.0.0', '2.1.0', '2.1.37', '2.3.0', '2.3.3'])('does not recognize %s version', version => {
-      expect(aas2_2({ asyncapi: version }, null)).toBe(false);
-    });
+  it('Finds existing version', () => {
+    expect(AsyncAPIFormats.find('2.0.0') !== undefined).toBeTruthy();
   });
 
-  describe('AsyncAPI 2.3', () => {
-    it.each(['2.3.0', '2.3.3'])('recognizes %s version correctly', version => {
-      expect(aas2_3({ asyncapi: version }, null)).toBe(true);
-    });
-
-    it.each(['2', '2.3', '2.0.0', '2.1.0', '2.1.37', '2.2.0', '2.4.0', '2.4.3'])(
-      'does not recognize %s version',
-      version => {
-        expect(aas2_3({ asyncapi: version }, null)).toBe(false);
-      },
-    );
+  it('Finds non-existing version', () => {
+    expect(AsyncAPIFormats.find('9999.9999.99999-rc') === undefined).toBeTruthy();
   });
 
-  describe('AsyncAPI 2.4', () => {
-    it.each(['2.4.0', '2.4.3'])('recognizes %s version correctly', version => {
-      expect(aas2_4({ asyncapi: version }, null)).toBe(true);
-    });
+  it('Filters by major version', () => {
+    const formats = AsyncAPIFormats;
+    formats.set('999.0.0', (_: unknown): boolean => true);
 
-    it.each(['2', '2.3', '2.0.0', '2.1.0', '2.1.37', '2.2.0', '2.3.0', '2.5.0', '2.5.3'])(
-      'does not recognize %s version',
-      version => {
-        expect(aas2_4({ asyncapi: version }, null)).toBe(false);
-      },
-    );
-  });
+    const filteredMajorVersion = '2';
+    const previousLenght = AsyncAPIFormats.formats().length;
+    const filteredFormats = AsyncAPIFormats.filterByMajorVersions([filteredMajorVersion]);
 
-  describe('AsyncAPI 2.5', () => {
-    it.each(['2.5.0', '2.5.2'])('recognizes %s version correctly', version => {
-      expect(aas2_5({ asyncapi: version }, null)).toBe(true);
+    expect(filteredFormats.size).toBeLessThan(previousLenght);
+    filteredFormats.forEach((_, version) => {
+      expect(String(getSemver(version).major)).toEqual(filteredMajorVersion);
     });
-
-    it.each(['2', '2.3', '2.0.0', '2.1.0', '2.1.37', '2.2.0', '2.3.0', '2.4.0', '2.4.3', '2.6.0', '2.6.4'])(
-      'does not recognize %s version',
-      version => {
-        expect(aas2_5({ asyncapi: version }, null)).toBe(false);
-      },
-    );
   });
 
-  describe('AsyncAPI 2.6', () => {
-    it.each(['2.6.0', '2.6.2'])('recognizes %s version correctly', version => {
-      expect(aas2_6({ asyncapi: version }, null)).toBe(true);
-    });
+  it('Excludes by version', () => {
+    const excludedVersions = ['2.0.0', '2.1.0', '2.6.0'];
+    const previousLenght = AsyncAPIFormats.formats().length;
+    const filteredFormats = AsyncAPIFormats.excludeByVersions(excludedVersions);
 
-    it.each(['2', '2.3', '2.0.0', '2.1.0', '2.1.37', '2.2.0', '2.3.0', '2.4.0', '2.4.3', '2.5.0', '2.5.3', '2.7.0', '2.7.4'])(
-      'does not recognize %s version',
-      version => {
-        expect(aas2_6({ asyncapi: version }, null)).toBe(false);
-      },
-    );
+    expect(filteredFormats.size).toEqual(previousLenght - excludedVersions.length);
+    excludedVersions.forEach((version) => {
+      expect(filteredFormats.find(version)).toBeFalsy();
+    });
   });
 });