-
-
Notifications
You must be signed in to change notification settings - Fork 95
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: match Spectral formats based on schemas found in @asyncapi/specs pkg #822
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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]; |
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'; | ||
|
@@ -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') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For v3, we need to remember to let |
||
return; | ||
} | ||
|
||
|
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'; | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah but then I will need to deal with sorting and filtering making it a bit more complicated and I don't want to go in that direction yet if this solves the issue. |
||
given: [ | ||
'$.channels.*.[publish,subscribe][?(@property === "message" && @.oneOf == void 0)]', | ||
'$.channels.*.[publish,subscribe].message.oneOf.*', | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not the biggest fan of using reverse instead of a sorting function, cause what if the spec library changes the order? It implies that the sorted list is a feature and the knowledge of it 😄