diff --git a/packages/rulesets/src/oas/__tests__/oas3-server-variables.test.ts b/packages/rulesets/src/oas/__tests__/oas3-server-variables.test.ts index 487eb1f34..6effb31b4 100644 --- a/packages/rulesets/src/oas/__tests__/oas3-server-variables.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas3-server-variables.test.ts @@ -70,7 +70,6 @@ testRule('oas3-server-variables', [ servers: [ { url: '{protocol}://stoplight.io:{port}', - variables: {}, }, ], paths: { @@ -85,7 +84,6 @@ testRule('oas3-server-variables', [ servers: [ { url: 'https://{env}.stoplight.io', - variables: {}, }, ], }, @@ -95,7 +93,7 @@ testRule('oas3-server-variables', [ errors: [ { message: 'Not all server\'s variables are described with "variables" object. Missed: protocol, port.', - path: ['servers', '0', 'variables'], + path: ['servers', '0'], severity: DiagnosticSeverity.Error, }, { @@ -105,7 +103,7 @@ testRule('oas3-server-variables', [ }, { message: 'Not all server\'s variables are described with "variables" object. Missed: env.', - path: ['paths', '/', 'get', 'servers', '0', 'variables'], + path: ['paths', '/', 'get', 'servers', '0'], severity: DiagnosticSeverity.Error, }, ], @@ -124,6 +122,7 @@ testRule('oas3-server-variables', [ }, env: { enum: ['staging', 'integration'], + default: 'staging', }, }, }, @@ -139,6 +138,7 @@ testRule('oas3-server-variables', [ }, env: { enum: ['staging', 'integration'], + default: 'staging', }, }, }, @@ -182,7 +182,48 @@ testRule('oas3-server-variables', [ }, { - name: 'server has an unlisted default', + name: 'server variable has a missing default', + document: { + openapi: '3.1.0', + servers: [ + { + url: 'https://{env}.stoplight.io', + variables: { + env: { + enum: ['staging', 'integration'], + }, + }, + }, + ], + paths: { + '/': { + servers: [ + { + url: 'https://stoplight.io:{port}', + variables: { + port: {}, + }, + }, + ], + }, + }, + }, + errors: [ + { + code: 'oas3-server-variables', + message: 'Server Variable "env" has a missing default.', + path: ['servers', '0', 'variables', 'env'], + }, + { + code: 'oas3-server-variables', + message: 'Server Variable "port" has a missing default.', + path: ['paths', '/', 'servers', '0', 'variables', 'port'], + }, + ], + }, + + { + name: 'server variable has an unlisted default', document: { openapi: '3.1.0', servers: [ @@ -231,17 +272,17 @@ testRule('oas3-server-variables', [ }, errors: [ { - message: 'Server Variable "port" has a default not listed in the enum', + message: 'Server Variable "port" has a default not listed in the enum.', path: ['servers', '0', 'variables', 'port', 'default'], severity: DiagnosticSeverity.Error, }, { - message: 'Server Variable "env" has a default not listed in the enum', + message: 'Server Variable "env" has a default not listed in the enum.', path: ['paths', '/', 'servers', '0', 'variables', 'env', 'default'], severity: DiagnosticSeverity.Error, }, { - message: 'Server Variable "env" has a default not listed in the enum', + message: 'Server Variable "env" has a default not listed in the enum.', path: ['components', 'links', 'Address', 'server', 'variables', 'env', 'default'], severity: DiagnosticSeverity.Error, }, @@ -258,6 +299,7 @@ testRule('oas3-server-variables', [ variables: { port: { enum: ['invalid port', 'another-one', '443'], + default: '443', }, }, }, @@ -266,6 +308,7 @@ testRule('oas3-server-variables', [ variables: { username: { enum: ['stoplight', 'io'], + default: 'stoplight', }, }, }, @@ -278,6 +321,7 @@ testRule('oas3-server-variables', [ variables: { base: { enum: ['http', 'https', 'ftp', 'ftps', 'ssh', 'smtp'], + default: 'https', }, }, }, diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index c0295725a..4f60d5e6f 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -702,6 +702,7 @@ const ruleset = { function: serverVariables, functionOptions: { checkSubstitutions: true, + requireDefault: true, }, }, }, diff --git a/packages/rulesets/src/shared/functions/serverVariables/index.ts b/packages/rulesets/src/shared/functions/serverVariables/index.ts index de2abe5e2..6b63fba4c 100644 --- a/packages/rulesets/src/shared/functions/serverVariables/index.ts +++ b/packages/rulesets/src/shared/functions/serverVariables/index.ts @@ -23,6 +23,7 @@ type Input = { type Options = { checkSubstitutions?: boolean; + requireDefault?: boolean; } | null; export default createRulesetFunction( @@ -72,60 +73,54 @@ export default createRulesetFunction( type: 'boolean', default: 'false', }, + requireDefault: { + type: 'boolean', + default: 'false', + }, }, additionalProperties: false, }, }, function serverVariables({ url, variables }, opts, ctx) { - if (variables === void 0) return; - const results: IFunctionResult[] = []; const foundVariables = parseUrlVariables(url); - const definedVariablesKeys = Object.keys(variables); - - const redundantVariables = getRedundantProps(foundVariables, definedVariablesKeys); - for (const variable of redundantVariables) { - results.push({ - message: `Server's "variables" object has unused defined "${variable}" url variable.`, - path: [...ctx.path, 'variables', variable], - }); - } + const definedVariablesKeys = variables === void 0 ? [] : Object.keys(variables); + + accumulateRedundantVariables(results, ctx.path, foundVariables, definedVariablesKeys); if (foundVariables.length === 0) return results; - const missingVariables = getMissingProps(foundVariables, definedVariablesKeys); - if (missingVariables.length > 0) { - results.push({ - message: `Not all server's variables are described with "variables" object. Missed: ${missingVariables.join( - ', ', - )}.`, - path: [...ctx.path, 'variables'], - }); - } + accumulateMissingVariables(results, ctx.path, foundVariables, definedVariablesKeys); + + if (variables === void 0) return results; const variablePairs: [key: string, values: string[]][] = []; for (const key of definedVariablesKeys) { - if (redundantVariables.includes(key)) continue; + if (!foundVariables.includes(key)) continue; - const values = variables[key]; + const variable = variables[key]; - if ('enum' in values) { - variablePairs.push([key, values.enum]); + if ('enum' in variable) { + variablePairs.push([key, variable.enum]); - if ('default' in values && !values.enum.includes(values.default)) { - results.push({ - message: `Server Variable "${key}" has a default not listed in the enum`, - path: [...ctx.path, 'variables', key, 'default'], - }); - } + checkVariableEnumValues(results, ctx.path, key, variable.enum, variable.default); + } else if ('default' in variable) { + variablePairs.push([key, [variable.default]]); } else { - variablePairs.push([key, [values.default ?? '']]); + variablePairs.push([key, []]); + } + + if (!('default' in variable) && opts?.requireDefault === true) { + results.push({ + message: `Server Variable "${key}" has a missing default.`, + path: [...ctx.path, 'variables', key], + }); } } - if (opts?.checkSubstitutions === true && variablePairs.length > 0) { + if (opts?.checkSubstitutions === true) { checkSubstitutions(results, ctx.path, url, variablePairs); } @@ -133,12 +128,65 @@ export default createRulesetFunction( }, ); +function accumulateRedundantVariables( + results: IFunctionResult[], + path: JsonPath, + foundVariables: string[], + definedVariablesKeys: string[], +): void { + if (definedVariablesKeys.length === 0) return; + + const redundantVariables = getRedundantProps(foundVariables, definedVariablesKeys); + for (const variable of redundantVariables) { + results.push({ + message: `Server's "variables" object has unused defined "${variable}" url variable.`, + path: [...path, 'variables', variable], + }); + } +} + +function accumulateMissingVariables( + results: IFunctionResult[], + path: JsonPath, + foundVariables: string[], + definedVariablesKeys: string[], +): void { + const missingVariables = + definedVariablesKeys.length === 0 ? foundVariables : getMissingProps(foundVariables, definedVariablesKeys); + + if (missingVariables.length > 0) { + results.push({ + message: `Not all server's variables are described with "variables" object. Missed: ${missingVariables.join( + ', ', + )}.`, + path: [...path, 'variables'], + }); + } +} + +function checkVariableEnumValues( + results: IFunctionResult[], + path: JsonPath, + name: string, + enumValues: string[], + defaultValue: string | undefined, +): void { + if (defaultValue !== void 0 && !enumValues.includes(defaultValue)) { + results.push({ + message: `Server Variable "${name}" has a default not listed in the enum.`, + path: [...path, 'variables', name, 'default'], + }); + } +} + function checkSubstitutions( results: IFunctionResult[], path: JsonPath, url: string, variables: [key: string, values: string[]][], ): void { + if (variables.length === 0) return; + const invalidUrls: string[] = []; for (const substitutedUrl of applyUrlVariables(url, variables)) {