From be69a6995be0e0da237922ca5732793bb6cffc66 Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Tue, 22 Mar 2022 10:32:16 +0100 Subject: [PATCH] feat(rulesets): add rules for validation of server variables and channel parameters --- docs/reference/asyncapi-rules.md | 12 ++ ...syncapi-channel-parameters-defined.test.ts | 142 ++++++++++++++++ .../asyncapi-server-variables-defined.test.ts | 154 ++++++++++++++++++ .../functions/asyncApi2ChannelParameters.ts | 38 +++++ .../functions/asyncApi2ServerVariables.ts | 37 +++++ .../rulesets/src/asyncapi/functions/utils.ts | 20 +++ packages/rulesets/src/asyncapi/index.ts | 30 ++++ 7 files changed, 433 insertions(+) create mode 100644 packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-parameters-defined.test.ts create mode 100644 packages/rulesets/src/asyncapi/__tests__/asyncapi-server-variables-defined.test.ts create mode 100644 packages/rulesets/src/asyncapi/functions/asyncApi2ChannelParameters.ts create mode 100644 packages/rulesets/src/asyncapi/functions/asyncApi2ServerVariables.ts create mode 100644 packages/rulesets/src/asyncapi/functions/utils.ts diff --git a/docs/reference/asyncapi-rules.md b/docs/reference/asyncapi-rules.md index 4e488bbe2..260d4c50c 100644 --- a/docs/reference/asyncapi-rules.md +++ b/docs/reference/asyncapi-rules.md @@ -24,6 +24,12 @@ Keep trailing slashes off of channel names, as it can cause some confusion. Most **Recommended:** Yes +### asyncapi-channel-parameters-defined + +All channel parameters should be defined in the `parameters` object of the channel. They should also not contain redundant parameters that do not exist in the channel address. + +**Recommended:** Yes + ### asyncapi-headers-schema-type-object The schema definition of the application headers must be of type “object”. @@ -288,6 +294,12 @@ Server URL should not point at example.com. **Recommended:** No +### asyncapi-server-variables-defined + +All server URL variables should be defined in the `variables` object of the server. They should also not contain redundant variables that do not exist in the server address. + +**Recommended:** Yes + ### asyncapi-servers A non empty `servers` object is expected to be located at the root of the document. diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-parameters-defined.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-parameters-defined.test.ts new file mode 100644 index 000000000..3a82a74c1 --- /dev/null +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-parameters-defined.test.ts @@ -0,0 +1,142 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import testRule from './__helpers__/tester'; + +testRule('asyncapi-channel-parameters-defined', [ + { + name: 'valid case', + document: { + asyncapi: '2.0.0', + channels: { + 'users/{userId}/signedUp': { + parameters: { + userId: {} + } + }, + }, + }, + errors: [], + }, + + { + name: 'channel has not defined definition for one of the parameters', + document: { + asyncapi: '2.0.0', + channels: { + 'users/{userId}/{anotherParam}/signedUp': { + parameters: { + userId: {} + } + }, + }, + }, + errors: [ + { + message: 'Not all channel\'s parameters are described with \"parameters\" object. Missed: anotherParam.', + path: ['channels', 'users/{userId}/{anotherParam}/signedUp', 'parameters'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'channel has not defined definition for two+ of the parameters', + document: { + asyncapi: '2.0.0', + channels: { + 'users/{userId}/{anotherParam1}/{anotherParam2}/signedUp': { + parameters: { + userId: {} + } + }, + }, + }, + errors: [ + { + message: 'Not all channel\'s parameters are described with \"parameters\" object. Missed: anotherParam1, anotherParam2.', + path: ['channels', 'users/{userId}/{anotherParam1}/{anotherParam2}/signedUp', 'parameters'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'channel has not defined definition for one of the parameters (in the components.channels)', + document: { + asyncapi: '2.3.0', + components: { + channels: { + 'users/{userId}/{anotherParam}/signedUp': { + parameters: { + userId: {} + } + }, + }, + } + }, + errors: [ + { + message: 'Not all channel\'s parameters are described with \"parameters\" object. Missed: anotherParam.', + path: ['components', 'channels', 'users/{userId}/{anotherParam}/signedUp', 'parameters'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'channel has redundant paramaters', + document: { + asyncapi: '2.0.0', + channels: { + 'users/{userId}/signedUp': { + parameters: { + userId: {}, + anotherParam1: {}, + anotherParam2: {}, + } + }, + }, + }, + errors: [ + { + message: 'Channel\'s \"parameters\" object has redundant defined \"anotherParam1\" parameter.', + path: ['channels', 'users/{userId}/signedUp', 'parameters', 'anotherParam1'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Channel\'s \"parameters\" object has redundant defined \"anotherParam2\" parameter.', + path: ['channels', 'users/{userId}/signedUp', 'parameters', 'anotherParam2'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'channel has redundant paramaters (in the components.channels)', + document: { + asyncapi: '2.3.0', + components: { + channels: { + 'users/{userId}/signedUp': { + parameters: { + userId: {}, + anotherParam1: {}, + anotherParam2: {}, + } + }, + }, + }, + }, + errors: [ + { + message: 'Channel\'s \"parameters\" object has redundant defined \"anotherParam1\" parameter.', + path: ['components', 'channels', 'users/{userId}/signedUp', 'parameters', 'anotherParam1'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Channel\'s \"parameters\" object has redundant defined \"anotherParam2\" parameter.', + path: ['components', 'channels', 'users/{userId}/signedUp', 'parameters', 'anotherParam2'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-variables-defined.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-variables-defined.test.ts new file mode 100644 index 000000000..a250f406c --- /dev/null +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-variables-defined.test.ts @@ -0,0 +1,154 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import testRule from './__helpers__/tester'; + +testRule('asyncapi-server-variables-defined', [ + { + name: 'valid case', + document: { + asyncapi: '2.0.0', + servers: { + production: { + url: '{sub}.stoplight.io', + protocol: 'https', + variables: { + sub: {} + } + }, + }, + }, + errors: [], + }, + + { + name: 'server has not defined definition for one of the url variables', + document: { + asyncapi: '2.0.0', + servers: { + production: { + url: '{sub}.{anotherParam}.stoplight.io', + protocol: 'https', + variables: { + sub: {} + } + }, + }, + }, + errors: [ + { + message: 'Not all server\'s variables are described with \"variables\" object. Missed: anotherParam.', + path: ['servers', 'production', 'variables'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'server has not defined definition for two of the url variables', + document: { + asyncapi: '2.0.0', + servers: { + production: { + url: '{sub}.{anotherParam1}.{anotherParam2}.stoplight.io', + protocol: 'https', + variables: { + sub: {} + } + }, + }, + }, + errors: [ + { + message: 'Not all server\'s variables are described with \"variables\" object. Missed: anotherParam1, anotherParam2.', + path: ['servers', 'production', 'variables'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'server has not defined definition for one of the url variables (in the components.servers)', + document: { + asyncapi: '2.3.0', + components: { + servers: { + production: { + url: '{sub}.{anotherParam}.stoplight.io', + protocol: 'https', + variables: { + sub: {} + } + }, + }, + } + }, + errors: [ + { + message: 'Not all server\'s variables are described with \"variables\" object. Missed: anotherParam.', + path: ['components', 'servers', 'production', 'variables'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'server has redundant url variables', + document: { + asyncapi: '2.0.0', + servers: { + production: { + url: '{sub}.stoplight.io', + protocol: 'https', + variables: { + sub: {}, + anotherParam1: {}, + anotherParam2: {}, + } + }, + }, + }, + errors: [ + { + message: 'Server\'s \"variables\" object has redundant defined \"anotherParam1\" url variable.', + path: ['servers', 'production', 'variables', 'anotherParam1'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Server\'s \"variables\" object has redundant defined \"anotherParam2\" url variable.', + path: ['servers', 'production', 'variables', 'anotherParam2'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'server has redundant url variables (in the components.servers)', + document: { + asyncapi: '2.3.0', + components: { + servers: { + production: { + url: '{sub}.stoplight.io', + protocol: 'https', + variables: { + sub: {}, + anotherParam1: {}, + anotherParam2: {}, + } + }, + }, + } + }, + errors: [ + { + message: 'Server\'s \"variables\" object has redundant defined \"anotherParam1\" url variable.', + path: ['components', 'servers', 'production', 'variables', 'anotherParam1'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Server\'s \"variables\" object has redundant defined \"anotherParam2\" url variable.', + path: ['components', 'servers', 'production', 'variables', 'anotherParam2'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelParameters.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelParameters.ts new file mode 100644 index 000000000..d52ef1221 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelParameters.ts @@ -0,0 +1,38 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; +import { parseUrlVariables, getMissingProps, getRedundantProps } from './utils'; + +import type { IFunctionResult } from '@stoplight/spectral-core'; + +export default createRulesetFunction<{ parameters: Record }, null>( + { + input: null, + options: null, + }, + function asyncApi2ChannelParameters(targetVal, _, ctx) { + const path = ctx.path[ctx.path.length - 1] as string; + const results: IFunctionResult[] = []; + + let parameters = parseUrlVariables(path); + if (!parameters || parameters.length === 0) return; + + const missingParameters = getMissingProps(parameters, targetVal.parameters); + if (missingParameters.length) { + results.push({ + message: `Not all channel's parameters are described with "parameters" object. Missed: ${missingParameters.join(', ')}.`, + path: [...ctx.path, 'parameters'], + }); + } + + const redundantParameters = getRedundantProps(parameters, targetVal.parameters); + if (redundantParameters.length) { + redundantParameters.forEach(param => { + results.push({ + message: `Channel's "parameters" object has redundant defined "${param}" parameter.`, + path: [...ctx.path, 'parameters', param], + }); + }); + } + + return results; + }, +); diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2ServerVariables.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2ServerVariables.ts new file mode 100644 index 000000000..be36a1ce4 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2ServerVariables.ts @@ -0,0 +1,37 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; +import { parseUrlVariables, getMissingProps, getRedundantProps } from './utils'; + +import type { IFunctionResult } from '@stoplight/spectral-core'; + +export default createRulesetFunction<{ url: string, variables: Record }, null>( + { + input: null, + options: null, + }, + function asyncApi2ServerVariables(targetVal, _, ctx) { + const results: IFunctionResult[] = []; + + let variables = parseUrlVariables(targetVal.url); + if (!variables || variables.length === 0) return results; + + const missingVariables = getMissingProps(variables, targetVal.variables); + if (missingVariables.length) { + results.push({ + message: `Not all server's variables are described with "variables" object. Missed: ${missingVariables.join(', ')}.`, + path: [...ctx.path, 'variables'], + }) + }; + + const redundantVariables = getRedundantProps(variables, targetVal.variables); + if (redundantVariables.length) { + redundantVariables.forEach(variable => { + results.push({ + message: `Server's "variables" object has redundant defined "${variable}" url variable.`, + path: [...ctx.path, 'variables', variable], + }); + }); + } + + return results; + }, +); diff --git a/packages/rulesets/src/asyncapi/functions/utils.ts b/packages/rulesets/src/asyncapi/functions/utils.ts new file mode 100644 index 000000000..7300bbc57 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/utils.ts @@ -0,0 +1,20 @@ +export function parseUrlVariables(str: string): string[] | undefined { + if (typeof str !== 'string') return; + const variables = str.match(/{(.+?)}/g); + if (!variables || variables.length === 0) return; + return variables.map(v => v.slice(1,-1)); +}; + +export function getMissingProps(arr: string[] = [], obj: Record = {}) { + if (!Object.keys(obj).length) return arr; + return arr.filter(val => { + return !obj.hasOwnProperty(val); + }); +}; + +export function getRedundantProps(arr: string[] = [], obj: Record = {}) { + if (!arr.length) return Object.keys(obj); + return Object.keys(obj).filter(val => { + return !arr.includes(val); + }); +}; \ No newline at end of file diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index 434240a9a..0fec1387f 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -8,9 +8,11 @@ import { alphabetical, } from '@stoplight/spectral-functions'; +import asyncApi2ChannelParameters from './functions/asyncApi2ChannelParameters'; import asyncApi2DocumentSchema from './functions/asyncApi2DocumentSchema'; import asyncApi2SchemaValidation from './functions/asyncApi2SchemaValidation'; import asyncApi2PayloadValidation from './functions/asyncApi2PayloadValidation'; +import asyncApi2ServerVariables from './functions/asyncApi2ServerVariables'; export default { documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md', @@ -55,6 +57,20 @@ export default { }, }, }, + 'asyncapi-channel-parameters-defined': { + description: 'Channel parameters must be defined and there must be no redundant parameters.', + message: '{{error}}', + severity: 'error', + type: 'validation', + recommended: true, + given: [ + '$.channels.*', + '$.components.channels.*', + ], + then: { + function: asyncApi2ChannelParameters, + }, + }, 'asyncapi-headers-schema-type-object': { description: 'Headers schema type must be "object".', message: 'Headers schema type must be "object" ({{error}}).', @@ -282,6 +298,20 @@ export default { function: asyncApi2DocumentSchema, }, }, + 'asyncapi-server-variables-defined': { + description: 'Server variables must be defined and there must be no redundant variables.', + message: '{{error}}', + severity: 'error', + type: 'validation', + recommended: true, + given: [ + '$.servers.*', + '$.components.servers.*', + ], + then: { + function: asyncApi2ServerVariables, + }, + }, 'asyncapi-server-no-empty-variable': { description: 'Server URL must not have empty variable substitution pattern.', recommended: true,