From e5525786ca0caf1d60a0fb1b91a474fea1d1edac Mon Sep 17 00:00:00 2001 From: dishafaujdar Date: Sat, 4 Jan 2025 21:45:47 +0530 Subject: [PATCH] feat: create rule for v3 core ruleset --- .changeset/rich-elephants-applaud.md | 5 + .../ruleset/v3/functions/channelServers.ts | 54 +++++++ packages/parser/src/ruleset/v3/ruleset.ts | 11 ++ .../v3/asyncapi3-channel-servers.spec.ts | 140 ++++++++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 .changeset/rich-elephants-applaud.md create mode 100644 packages/parser/src/ruleset/v3/functions/channelServers.ts create mode 100644 packages/parser/test/ruleset/rules/v3/asyncapi3-channel-servers.spec.ts diff --git a/.changeset/rich-elephants-applaud.md b/.changeset/rich-elephants-applaud.md new file mode 100644 index 000000000..4ec4ba2ac --- /dev/null +++ b/.changeset/rich-elephants-applaud.md @@ -0,0 +1,5 @@ +--- +"@asyncapi/parser": minor +--- + +feat: create the rule `asyncapi3-channel-servers` for the v3 rule core ruleset diff --git a/packages/parser/src/ruleset/v3/functions/channelServers.ts b/packages/parser/src/ruleset/v3/functions/channelServers.ts new file mode 100644 index 000000000..9ff9d31c1 --- /dev/null +++ b/packages/parser/src/ruleset/v3/functions/channelServers.ts @@ -0,0 +1,54 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; + +import type { IFunctionResult } from '@stoplight/spectral-core'; + +export const channelServers = createRulesetFunction< + { servers?: Record; channels?: Record }> }, + null +>( + { + input: { + type: 'object', + properties: { + servers: { + type: 'object', + }, + channels: { + type: 'object', + additionalProperties: { + type: 'object', + properties: { + servers: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + options: null, + }, + (targetVal) => { + const results: IFunctionResult[] = []; + if (!targetVal.channels) return results; + const serverNames = Object.keys(targetVal.servers ?? {}); + + Object.entries(targetVal.channels ?? {}).forEach(([channelAddress, channel]) => { + if (!channel.servers) return; + + channel.servers.forEach((serverName, index) => { + if (!serverNames.includes(serverName)) { + results.push({ + message: 'Channel contains server that are not defined on the "servers" object.', + path: ['channels', channelAddress, 'servers', index], + }); + } + }); + }); + + return results; + }, +); diff --git a/packages/parser/src/ruleset/v3/ruleset.ts b/packages/parser/src/ruleset/v3/ruleset.ts index 62e7ecb51..cf70cbfa1 100644 --- a/packages/parser/src/ruleset/v3/ruleset.ts +++ b/packages/parser/src/ruleset/v3/ruleset.ts @@ -3,6 +3,7 @@ import { AsyncAPIFormats } from '../formats'; import { operationMessagesUnambiguity } from './functions/operationMessagesUnambiguity'; import { pattern } from '@stoplight/spectral-functions'; +import { channelServers } from './functions/channelServers'; export const v3CoreRuleset = { description: 'Core AsyncAPI 3.x.x ruleset.', @@ -57,6 +58,16 @@ export const v3CoreRuleset = { }, }, }, + 'asyncapi3-channel-servers': { + description: 'Channel servers must be defined in the "servers" object.', + message: '{{error}}', + severity: 'error', + recommended: true, + given: '$', + then: { + function: channelServers, + }, + }, 'asyncapi3-channel-no-query-nor-fragment': { description: 'Channel address should not include query ("?") or fragment ("#") delimiter.', severity: 'error', diff --git a/packages/parser/test/ruleset/rules/v3/asyncapi3-channel-servers.spec.ts b/packages/parser/test/ruleset/rules/v3/asyncapi3-channel-servers.spec.ts new file mode 100644 index 000000000..e567132b1 --- /dev/null +++ b/packages/parser/test/ruleset/rules/v3/asyncapi3-channel-servers.spec.ts @@ -0,0 +1,140 @@ +import { testRule, DiagnosticSeverity } from '../../tester'; + +testRule('asyncapi3-channel-servers', [ + { + name: 'valid case', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: ['development'], + }, + }, + }, + errors: [], + }, + + { + name: 'valid case - without defined servers', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: {}, + }, + }, + errors: [], + }, + + { + name: 'valid case - without defined servers in the root', + document: { + asyncapi: '3.0.0', + channels: { + channel: {}, + }, + }, + errors: [], + }, + + { + name: 'valid case - without defined channels in the root', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + }, + }, + errors: [], + }, + + { + name: 'valid case - with empty array', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: [], + }, + }, + }, + errors: [], + }, + + { + name: 'invalid case', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: ['another-server'], + }, + }, + }, + errors: [ + { + message: 'Channel contains server that are not defined on the "servers" object.', + path: ['channels', 'channel', 'servers', '0'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case - one server is defined, another one not', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: ['production', 'another-server'], + }, + }, + }, + errors: [ + { + message: 'Channel contains server that are not defined on the "servers" object.', + path: ['channels', 'channel', 'servers', '1'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case - without defined servers', + document: { + asyncapi: '3.0.0', + channels: { + channel: { + servers: ['production'], + }, + }, + }, + errors: [ + { + message: 'Channel contains server that are not defined on the "servers" object.', + path: ['channels', 'channel', 'servers', '0'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]);