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

refactor: add rule to check if AsyncAPI document is defined or not #583

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
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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));
4 changes: 2 additions & 2 deletions src/schema-parser/asyncapi-schema-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/schema-parser/spectral-rule-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { Parser } from '../parser';
import type { ValidateSchemaInput } from './index';
import type { SchemaValidateResult } from '../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],
Expand Down
48 changes: 45 additions & 3 deletions src/spectral.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -14,11 +18,49 @@ 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',
'asyncapi-payload': 'off',
},
} 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<MaybeAsyncAPI, null>(
{
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: [],
}
];
}
}
),
},
}
}
71 changes: 71 additions & 0 deletions test/spectral.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}