From e5525786ca0caf1d60a0fb1b91a474fea1d1edac Mon Sep 17 00:00:00 2001 From: dishafaujdar Date: Sat, 4 Jan 2025 21:45:47 +0530 Subject: [PATCH 1/2] 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, + }, + ], + }, +]); From 4363b420a2168a34e8a84661db2d8bc2986279ff Mon Sep 17 00:00:00 2001 From: dishafaujdar Date: Sun, 19 Jan 2025 20:54:34 +0530 Subject: [PATCH 2/2] resolved duplication of code --- .../{v2 => }/functions/channelServers.ts | 0 packages/parser/src/ruleset/v2/ruleset.ts | 2 +- .../ruleset/v3/functions/channelServers.ts | 54 ------------------- packages/parser/src/ruleset/v3/ruleset.ts | 2 +- 4 files changed, 2 insertions(+), 56 deletions(-) rename packages/parser/src/ruleset/{v2 => }/functions/channelServers.ts (100%) delete mode 100644 packages/parser/src/ruleset/v3/functions/channelServers.ts diff --git a/packages/parser/src/ruleset/v2/functions/channelServers.ts b/packages/parser/src/ruleset/functions/channelServers.ts similarity index 100% rename from packages/parser/src/ruleset/v2/functions/channelServers.ts rename to packages/parser/src/ruleset/functions/channelServers.ts diff --git a/packages/parser/src/ruleset/v2/ruleset.ts b/packages/parser/src/ruleset/v2/ruleset.ts index f5e0d3274..b03a7549f 100644 --- a/packages/parser/src/ruleset/v2/ruleset.ts +++ b/packages/parser/src/ruleset/v2/ruleset.ts @@ -4,7 +4,7 @@ import { AsyncAPIFormats } from '../formats'; import { truthy, pattern } from '@stoplight/spectral-functions'; import { channelParameters } from './functions/channelParameters'; -import { channelServers } from './functions/channelServers'; +import { channelServers } from '../functions/channelServers'; import { checkId } from './functions/checkId'; import { messageExamples } from './functions/messageExamples'; import { asyncApi2MessageExamplesParserRule } from './functions/messageExamples-spectral-rule-v2'; diff --git a/packages/parser/src/ruleset/v3/functions/channelServers.ts b/packages/parser/src/ruleset/v3/functions/channelServers.ts deleted file mode 100644 index 9ff9d31c1..000000000 --- a/packages/parser/src/ruleset/v3/functions/channelServers.ts +++ /dev/null @@ -1,54 +0,0 @@ -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 cf70cbfa1..b85187d22 100644 --- a/packages/parser/src/ruleset/v3/ruleset.ts +++ b/packages/parser/src/ruleset/v3/ruleset.ts @@ -3,7 +3,7 @@ import { AsyncAPIFormats } from '../formats'; import { operationMessagesUnambiguity } from './functions/operationMessagesUnambiguity'; import { pattern } from '@stoplight/spectral-functions'; -import { channelServers } from './functions/channelServers'; +import { channelServers } from '../functions/channelServers'; export const v3CoreRuleset = { description: 'Core AsyncAPI 3.x.x ruleset.',