diff --git a/src/ruleset/v3/functions/channelServer3.ts b/src/ruleset/v3/functions/channelServer3.ts new file mode 100644 index 000000000..581d34feb --- /dev/null +++ b/src/ruleset/v3/functions/channelServer3.ts @@ -0,0 +1,57 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; +import type { IFunctionResult } from '@stoplight/spectral-core'; + +export const channelServers3 = 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: 'object', + required: ['$ref'], + properties: { + $ref: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + options: null, + }, + (targetVal) => { + console.log('channelServers function called with:', JSON.stringify(targetVal, null, 2)); + const results: IFunctionResult[] = []; + if (!targetVal.channels) return results; + + Object.entries(targetVal.channels ?? {}).forEach(([channelAddress, channel]) => { + if (!channel.servers) return; + + channel.servers.forEach((serverRef, index) => { + if (!serverRef.$ref.startsWith('#/servers/')) { + results.push({ + message: 'Channel server must reference a server defined in the root "servers" object.', + path: ['channels', channelAddress, 'servers', index, '$ref'], + }); + } + }); + }); + console.log('channelServers function returning results:', results); + return results; + }, +); \ No newline at end of file diff --git a/src/ruleset/v3/ruleset.ts b/src/ruleset/v3/ruleset.ts index 724339e84..5565a8950 100644 --- a/src/ruleset/v3/ruleset.ts +++ b/src/ruleset/v3/ruleset.ts @@ -2,6 +2,7 @@ import { AsyncAPIFormats } from '../formats'; import { operationMessagesUnambiguity } from './functions/operationMessagesUnambiguity'; +import { channelServers3 } from './functions/channelServers3'; import { pattern } from '@stoplight/spectral-functions'; export const v3CoreRuleset = { @@ -56,6 +57,16 @@ export const v3CoreRuleset = { match: '#\\/servers\\/', // If doesn't match, rule fails. }, }, + }, + 'asyncapi3-channel-servers': { + description: 'Channel servers must be defined in the root "servers" object.', + message: '{{error}}', + severity: 'error', + recommended: true, + given: '$', + then: { + function: channelServers3, + }, } }, }; diff --git a/test/ruleset/rules/v3/asyncapi3-channel-server.spec.ts b/test/ruleset/rules/v3/asyncapi3-channel-server.spec.ts new file mode 100644 index 000000000..e2f62bb61 --- /dev/null +++ b/test/ruleset/rules/v3/asyncapi3-channel-server.spec.ts @@ -0,0 +1,210 @@ +import { testRule, DiagnosticSeverity } from '../../tester'; + +testRule('asyncapi3-channel-servers', [ + { + name: 'valid case', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: [{ $ref: '#/servers/development' }], + }, + }, + }, + errors: [], + }, + + { + name: 'valid case - multiple servers', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + staging: {}, + }, + channels: { + channel: { + servers: [ + { $ref: '#/servers/development' }, + { $ref: '#/servers/production' }, + ], + }, + }, + }, + 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 - incorrect $ref', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: [{ $ref: '#/components/servers/another-server' }], + }, + }, + }, + errors: [ + { + message: 'Channel server must reference a server defined in the root "servers" object.', + path: ['channels', 'channel', 'servers', '0', '$ref'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case - one server is correct, another one is incorrect', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: [ + { $ref: '#/servers/production' }, + { $ref: '#/components/servers/another-server' }, + ], + }, + }, + }, + errors: [ + { + message: 'Channel server must reference a server defined in the root "servers" object.', + path: ['channels', 'channel', 'servers', '1', '$ref'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case - without defined servers', + document: { + asyncapi: '3.0.0', + channels: { + channel: { + servers: [{ $ref: '#/servers/production' }], + }, + }, + }, + errors: [ + { + message: 'Channel server must reference a server defined in the root "servers" object.', + path: ['channels', 'channel', 'servers', '0', '$ref'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case - incorrect $ref format', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: [{ $ref: 'production' }], + }, + }, + }, + errors: [ + { + message: 'Channel server must reference a server defined in the root "servers" object.', + path: ['channels', 'channel', 'servers', '0', '$ref'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case - servers is not an array', + document: { + asyncapi: '3.0.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: { $ref: '#/servers/production' }, + }, + }, + }, + errors: [ + { + message: 'Channel servers must be an array of references to servers.', + path: ['channels', 'channel', 'servers'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); \ No newline at end of file