diff --git a/src/constants.ts b/src/constants.ts index 14c751d8a..668b139a6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,6 @@ +// @ts-ignore +import specs from '@asyncapi/specs'; + export const xParserSpecParsed = 'x-parser-spec-parsed'; export const xParserSpecStringified = 'x-parser-spec-stringified'; @@ -11,3 +14,6 @@ export const xParserOriginalTraits = 'x-parser-original-traits'; export const xParserCircular = 'x-parser-circular'; export const EXTENSION_REGEX = /^x-[\w\d\.\-\_]+$/; + +// Only >=2.0.0 versions are supported +export const specVersions = Object.keys(specs).filter((version: string) => !['1.0.0', '1.1.0', '1.2.0', '2.0.0-rc1', '2.0.0-rc2'].includes(version)); \ No newline at end of file diff --git a/src/schema-parser/asyncapi-schema-parser.ts b/src/schema-parser/asyncapi-schema-parser.ts index 0e520b040..60deeae78 100644 --- a/src/schema-parser/asyncapi-schema-parser.ts +++ b/src/schema-parser/asyncapi-schema-parser.ts @@ -2,6 +2,8 @@ import Ajv from "ajv"; // @ts-ignore import specs from '@asyncapi/specs'; +import { specVersions } from '../constants'; + import type { ErrorObject, ValidateFunction } from "ajv"; import type { AsyncAPISchema, SchemaValidateResult } from '../types'; import type { SchemaParser, ParseSchemaInput, ValidateSchemaInput } from "../schema-parser"; @@ -11,8 +13,6 @@ const ajv = new Ajv({ strict: false, logger: false, }); -// Only versions compatible with JSON Schema Draf-07 are supported. -const specVersions = Object.keys(specs).filter((version: string) => !['1.0.0', '1.1.0', '1.2.0', '2.0.0-rc1', '2.0.0-rc2'].includes(version)); export function AsyncAPISchemaParser(): SchemaParser { return { diff --git a/src/schema-parser/spectral-rule-v2.ts b/src/schema-parser/spectral-rule-v2.ts index b4048edf0..5b514b989 100644 --- a/src/schema-parser/spectral-rule-v2.ts +++ b/src/schema-parser/spectral-rule-v2.ts @@ -10,7 +10,7 @@ import type { ValidateSchemaInput } from './index'; import type { SchemaValidateResult } from '../types'; import type { v2 } from '../spec-types'; -export function aas2schemaParserRule(parser: Parser): RuleDefinition { +export function asyncApi2SchemaParserRule(parser: Parser): RuleDefinition { return { description: 'Custom schema must be correctly formatted from the point of view of the used format.', formats: [aas2_0, aas2_1, aas2_2, aas2_3, aas2_4], diff --git a/src/spectral.ts b/src/spectral.ts index 52a81a508..a9d231a0d 100644 --- a/src/spectral.ts +++ b/src/spectral.ts @@ -1,9 +1,13 @@ -import { RulesetDefinition } from "@stoplight/spectral-core"; +import { createRulesetFunction } from '@stoplight/spectral-core'; import { asyncapi as aasRuleset } from "@stoplight/spectral-rulesets"; -import { aas2schemaParserRule } from './schema-parser/spectral-rule-v2'; +import { asyncApi2SchemaParserRule } from './schema-parser/spectral-rule-v2'; +import { specVersions } from './constants'; +import { isObject } from './utils'; +import type { RuleDefinition, RulesetDefinition } from "@stoplight/spectral-core"; import type { Parser } from "./parser"; +import type { MaybeAsyncAPI } from "./types"; export function configureSpectral(parser: Parser) { const ruleset = configureRuleset(parser); @@ -14,7 +18,8 @@ function configureRuleset(parser: Parser): RulesetDefinition { return { extends: [aasRuleset], rules: { - 'asyncapi-schemas-v2': aas2schemaParserRule(parser), + 'asyncapi-is-asyncapi': asyncApi2IsAsyncApi(), + 'asyncapi-schemas-v2': asyncApi2SchemaParserRule(parser), // We do not use these rules from the official ruleset due to the fact // that the given rules validate only AsyncAPI Schemas and prevent defining schemas in other formats 'asyncapi-payload-unsupported-schemaFormat': 'off', @@ -22,3 +27,40 @@ function configureRuleset(parser: Parser): RulesetDefinition { }, } as RulesetDefinition; } + +function asyncApi2IsAsyncApi(): RuleDefinition { + return { + description: 'The input must be a document with a supported version of AsyncAPI.', + formats: [(_: unknown) => true], // run rule for all inputs + message: '{{error}}', + severity: 'error', + type: 'validation', + recommended: true, + given: '$', + then: { + function: createRulesetFunction( + { + input: null, + options: null, + }, + function asyncApi2IsAsyncAPI(targetVal) { + if (!isObject(targetVal) || typeof targetVal.asyncapi !== 'string') { + return [ + { + message: 'This is not an AsyncAPI document. The "asyncapi" field as string is missing.', + path: [], + } + ]; + } else if (!specVersions.includes(targetVal.asyncapi)) { + return [ + { + message: `Version "${targetVal.asyncapi}" is not supported. Please use "${specVersions[specVersions.length - 1]}" (latest) version of the specification.`, + path: [], + } + ]; + } + } + ), + }, + } +} \ No newline at end of file diff --git a/test/spectral.test.ts b/test/spectral.test.ts new file mode 100644 index 000000000..a0c9116b7 --- /dev/null +++ b/test/spectral.test.ts @@ -0,0 +1,71 @@ +import { Parser } from '../src/parser'; +import { validate } from '../src/lint'; + +import { specVersions } from '../src/constants'; + +import type { ISpectralDiagnostic } from '@stoplight/spectral-core'; +import type { SchemaValidateResult } from '../src/types'; + +describe('Custom Spectral instance', function() { + const parser = new Parser(); + + describe('asyncapi-is-asyncapi Spectral rule', function() { + it('should throw error when input is not an AsyncAPI document (empty input case)', async function() { + const { diagnostics } = await validate(parser, ''); + + expect(diagnostics.length > 0).toEqual(true); + const filteredDiagnostics = filterDiagnostics(diagnostics, 'asyncapi-is-asyncapi'); + + const expectedResult: SchemaValidateResult[] = [ + { + message: 'This is not an AsyncAPI document. The "asyncapi" field as string is missing.', + path: [] + }, + ]; + + expect(filteredDiagnostics).toEqual(expectedResult.map(e => expect.objectContaining(e))); + }); + + it('should throw error when input is not an AsyncAPI document (another spec case)', async function() { + const document = { + openapi: '3.0.0', + } + const { diagnostics } = await validate(parser, document as any); + + expect(diagnostics.length > 0).toEqual(true); + const filteredDiagnostics = filterDiagnostics(diagnostics, 'asyncapi-is-asyncapi'); + + const expectedResult: SchemaValidateResult[] = [ + { + message: 'This is not an AsyncAPI document. The "asyncapi" field as string is missing.', + path: [] + }, + ]; + + expect(filteredDiagnostics).toEqual(expectedResult.map(e => expect.objectContaining(e))); + }); + + it('should throw error when input is an unsupported version of AsyncAPI', async function() { + const document = { + asyncapi: '2.1.37', + } + const { diagnostics } = await validate(parser, document as any); + + expect(diagnostics.length > 0).toEqual(true); + const filteredDiagnostics = filterDiagnostics(diagnostics, 'asyncapi-is-asyncapi'); + + const expectedResult: SchemaValidateResult[] = [ + { + message: `Version "2.1.37" is not supported. Please use "${specVersions[specVersions.length - 1]}" (latest) version of the specification.`, + path: [] + }, + ]; + + expect(filteredDiagnostics).toEqual(expectedResult.map(e => expect.objectContaining(e))); + }); + }); +}); + +function filterDiagnostics(diagnostics: ISpectralDiagnostic[], code: string) { + return diagnostics.filter(d => d.code === code); +} \ No newline at end of file