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

chore: merge master into next-major-spec + adapt tests for v3 #824

Merged
merged 4 commits into from
Aug 1, 2023
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
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
# For more details, read the following article on GitHub: https://help.github.com/articles/about-codeowners/.

# The default owners are automatically added as reviewers when you open a pull request unless different owners are specified in the file.
* @fmvilas @magicmatatjahu @jonaslagoni @derberg @smoya @asyncapi-bot-eve
* @fmvilas @magicmatatjahu @jonaslagoni @smoya @asyncapi-bot-eve
98 changes: 52 additions & 46 deletions src/ruleset/formats.ts
Original file line number Diff line number Diff line change
@@ -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];
25 changes: 6 additions & 19 deletions src/ruleset/functions/documentStructure.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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);
}
}
}

Expand Down
7 changes: 3 additions & 4 deletions src/ruleset/functions/unusedComponent.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
}

Expand Down
8 changes: 5 additions & 3 deletions src/ruleset/ruleset.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { aas2All as aas2AllFormats } from './formats';

import { lastVersion } from '../constants';
import { truthy, schema } from '@stoplight/spectral-functions';

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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down
9 changes: 4 additions & 5 deletions src/ruleset/v2/ruleset.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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': {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.*',
Expand Down
32 changes: 30 additions & 2 deletions test/parse.spec.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -22,6 +22,20 @@ 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).toBeInstanceOf(AsyncAPIDocumentV3);
expect(diagnostics.length === 0).toEqual(true);
});

it('should parse invalid document', async function() {
const documentRaw = {
asyncapi: '2.0.0',
Expand All @@ -36,6 +50,20 @@ describe('parse()', function() {
expect(diagnostics.length > 0).toEqual(true);
});

it('should parse invalid v3 document', async function() {
const documentRaw = {
asyncapi: '3.0.0',
not_a_valid_info_object: {
title: 'Invalid AsyncApi document',
version: '1.0',
},
};
const { document, diagnostics } = await parser.parse(documentRaw);

expect(document).toBeInstanceOf(AsyncAPIDocumentV3);
expect(diagnostics.length === 0).toEqual(true); // Validation in v3 is still not enabled. This test will intentionally fail once that changes.
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the important change.

});

it('should return extras', async function() {
const documentRaw = {
asyncapi: '2.0.0',
Expand Down
Loading