diff --git a/src/ruleset/v3/functions/serverVariables3.ts b/src/ruleset/v3/functions/serverVariables3.ts new file mode 100644 index 000000000..903a216ab --- /dev/null +++ b/src/ruleset/v3/functions/serverVariables3.ts @@ -0,0 +1,56 @@ + +import { createRulesetFunction } from '@stoplight/spectral-core'; + +import { getMissingProps, getRedundantProps, parseUrlVariables } from '../../utils'; + +import type { IFunctionResult } from '@stoplight/spectral-core'; + +export const serverVariables3 = createRulesetFunction<{ host: string, pathname: string; variables: Record }, null>( + { + input: { + type: 'object', + properties: { + host: { + type: 'string', + }, + pathname: { + type: 'string', + }, + variables: { + type: 'object', + }, + }, + required: ['host', 'variables'], + }, + options: null, + }, + (targetVal, _, ctx) => { + const results: IFunctionResult[] = []; + const url = targetVal.host + targetVal.pathname; + + const variables = parseUrlVariables(url); + if (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}" host and pathname variable.`, + path: [...ctx.path, 'variables', variable], + }); + }); + } + + return results; + }, +); \ No newline at end of file diff --git a/src/ruleset/v3/ruleset.ts b/src/ruleset/v3/ruleset.ts index 724339e84..7351d0961 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 { serverVariables3 } from './functions/serverVariables3'; import { pattern } from '@stoplight/spectral-functions'; export const v3CoreRuleset = { @@ -56,6 +57,20 @@ export const v3CoreRuleset = { match: '#\\/servers\\/', // If doesn't match, rule fails. }, }, - } + }, + + /** + * Server Object rules + */ + 'asyncapi3-server-variables': { + description: 'Server variables must be defined and there must be no redundant variables.', + message: '{{error}}', + severity: 'error', + recommended: true, + given: ['$.servers.*', '$.components.servers.*'], + then: { + function: serverVariables3, + }, + }, }, }; diff --git a/test/ruleset/rules/v3/asyncapi3-server-variables.spec.ts b/test/ruleset/rules/v3/asyncapi3-server-variables.spec.ts new file mode 100644 index 000000000..56de59d18 --- /dev/null +++ b/test/ruleset/rules/v3/asyncapi3-server-variables.spec.ts @@ -0,0 +1,179 @@ +import { testRule, DiagnosticSeverity } from '../../tester'; + +testRule('asyncapi3-server-variables', [ + { + name: 'valid case', + document: { + asyncapi: '3.0.0', + servers: { + production: { + host: '{sub}.stoplight.io', + protocol: 'https', + variables: { + sub: {}, + }, + }, + }, + }, + errors: [], + }, + + { + name: 'server has not defined definition for one of the host variables', + document: { + asyncapi: '3.0.0', + servers: { + production: { + host: '{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 host variables', + document: { + asyncapi: '3.0.0', + servers: { + production: { + host: '{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 pathname variables', + document: { + asyncapi: '3.0.0', + servers: { + production: { + host: '{sub}.stoplight.io', + pathname: '/{anotherParam}', + 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 one of the host variables (in the components.servers)', + document: { + asyncapi: '3.0.0', + components: { + servers: { + production: { + host: '{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 host variables', + document: { + asyncapi: '3.0.0', + servers: { + production: { + host: '{sub}.stoplight.io', + protocol: 'https', + variables: { + sub: {}, + anotherParam1: {}, + anotherParam2: {}, + }, + }, + }, + }, + errors: [ + { + message: 'Server\'s "variables" object has redundant defined "anotherParam1" host and pathname variable.', + path: ['servers', 'production', 'variables', 'anotherParam1'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Server\'s "variables" object has redundant defined "anotherParam2" host and pathname variable.', + path: ['servers', 'production', 'variables', 'anotherParam2'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'server has redundant host variables (in the components.servers)', + document: { + asyncapi: '3.0.0', + components: { + servers: { + production: { + host: '{sub}.stoplight.io', + protocol: 'https', + variables: { + sub: {}, + anotherParam1: {}, + anotherParam2: {}, + }, + }, + }, + }, + }, + errors: [ + { + message: 'Server\'s "variables" object has redundant defined "anotherParam1" host and pathname variable.', + path: ['components', 'servers', 'production', 'variables', 'anotherParam1'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Server\'s "variables" object has redundant defined "anotherParam2" host and pathname variable.', + path: ['components', 'servers', 'production', 'variables', 'anotherParam2'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); \ No newline at end of file