diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md index d295b15c8..d55e43772 100644 --- a/docs/reference/openapi-rules.md +++ b/docs/reference/openapi-rules.md @@ -846,3 +846,79 @@ schemas: - name - petType ``` + +### oas3-server-variables + +This rule ensures that server variables defined in OpenAPI Specification 3 (OAS3) and 3.1 are valid, not unused, and result in a valid URL. Properly defining and using server variables is crucial for the accurate representation of API endpoints and preventing potential misconfigurations or security issues. + +**Recommended**: Yes + +**Bad Examples** + +1. **Missing definition for a URL variable**: + +```yaml +servers: + - url: "https://api.{region}.example.com/v1" + variables: + version: + default: "v1" +``` + +In this example, the variable **`{region}`** in the URL is not defined within the **`variables`** object. + +2. **Unused URL variable:** + +```yaml +servers: + - url: "https://api.example.com/v1" + variables: + region: + default: "us-west" +``` + +Here, the variable **`region`** is defined but not used in the server URL. + +3. **Invalid default value for an allowed value variable**: + +```yaml +servers: + - url: "https://api.{region}.example.com/v1" + variables: + region: + default: "us-south" + enum: + - "us-west" + - "us-east" +``` + +The default value 'us-south' isn't one of the allowed values in the **`enum`**. + +4. **Invalid resultant URL**: + +```yaml +servers: + - url: "https://api.example.com:{port}/v1" + variables: + port: + default: "8o80" +``` + +Substituting the default value of **`{port}`** results in an invalid URL. + +**Good Example** + +```yaml +servers: + - url: "https://api.{region}.example.com/{version}" + variables: + region: + default: "us-west" + enum: + - "us-west" + - "us-east" + version: + default: "v1" +``` + +In this example, both **`{region}`** and **`{version}`** variables are properly defined and used in the server URL. Also, the default value for **`region`** is within the allowed values. diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-variables.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-variables.test.ts index b529611aa..8f46235b6 100644 --- a/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-variables.test.ts +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-variables.test.ts @@ -92,7 +92,7 @@ testRule('asyncapi-server-variables', [ }, { - name: 'server has redundant url variables', + name: 'server has unused url variables', document: { asyncapi: '2.0.0', servers: { @@ -109,12 +109,12 @@ testRule('asyncapi-server-variables', [ }, errors: [ { - message: 'Server\'s "variables" object has redundant defined "anotherParam1" url variable.', + message: 'Server\'s "variables" object has unused defined "anotherParam1" url variable.', path: ['servers', 'production', 'variables', 'anotherParam1'], severity: DiagnosticSeverity.Error, }, { - message: 'Server\'s "variables" object has redundant defined "anotherParam2" url variable.', + message: 'Server\'s "variables" object has unused defined "anotherParam2" url variable.', path: ['servers', 'production', 'variables', 'anotherParam2'], severity: DiagnosticSeverity.Error, }, @@ -122,7 +122,7 @@ testRule('asyncapi-server-variables', [ }, { - name: 'server has redundant url variables (in the components.servers)', + name: 'server has unused url variables (in the components.servers)', document: { asyncapi: '2.3.0', components: { @@ -141,12 +141,12 @@ testRule('asyncapi-server-variables', [ }, errors: [ { - message: 'Server\'s "variables" object has redundant defined "anotherParam1" url variable.', + message: 'Server\'s "variables" object has unused defined "anotherParam1" url variable.', path: ['components', 'servers', 'production', 'variables', 'anotherParam1'], severity: DiagnosticSeverity.Error, }, { - message: 'Server\'s "variables" object has redundant defined "anotherParam2" url variable.', + message: 'Server\'s "variables" object has unused 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 index 9c214e6fb..71a189fdc 100644 --- a/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelParameters.ts +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelParameters.ts @@ -1,10 +1,9 @@ import { createRulesetFunction } from '@stoplight/spectral-core'; -import { parseUrlVariables } from './utils/parseUrlVariables'; -import { getMissingProps } from './utils/getMissingProps'; -import { getRedundantProps } from './utils/getRedundantProps'; - import type { IFunctionResult } from '@stoplight/spectral-core'; +import { parseUrlVariables } from '../../shared/functions/serverVariables/utils/parseUrlVariables'; +import { getMissingProps } from '../../shared/utils/getMissingProps'; +import { getRedundantProps } from '../../shared/utils/getRedundantProps'; export default createRulesetFunction<{ parameters: Record }, null>( { @@ -26,7 +25,7 @@ export default createRulesetFunction<{ parameters: Record }, nu const parameters = parseUrlVariables(path); if (parameters.length === 0) return; - const missingParameters = getMissingProps(parameters, targetVal.parameters); + const missingParameters = getMissingProps(parameters, Object.keys(targetVal.parameters)); if (missingParameters.length) { results.push({ message: `Not all channel's parameters are described with "parameters" object. Missed: ${missingParameters.join( @@ -36,7 +35,7 @@ export default createRulesetFunction<{ parameters: Record }, nu }); } - const redundantParameters = getRedundantProps(parameters, targetVal.parameters); + const redundantParameters = getRedundantProps(parameters, Object.keys(targetVal.parameters)); if (redundantParameters.length) { redundantParameters.forEach(param => { results.push({ diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2ServerVariables.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2ServerVariables.ts deleted file mode 100644 index 3fda72d8e..000000000 --- a/packages/rulesets/src/asyncapi/functions/asyncApi2ServerVariables.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { createRulesetFunction } from '@stoplight/spectral-core'; - -import { parseUrlVariables } from './utils/parseUrlVariables'; -import { getMissingProps } from './utils/getMissingProps'; -import { getRedundantProps } from './utils/getRedundantProps'; - -import type { IFunctionResult } from '@stoplight/spectral-core'; - -export default createRulesetFunction<{ url: string; variables: Record }, null>( - { - input: { - type: 'object', - properties: { - url: { - type: 'string', - }, - variables: { - type: 'object', - }, - }, - required: ['url', 'variables'], - }, - options: null, - }, - function asyncApi2ServerVariables(targetVal, _, ctx) { - const results: IFunctionResult[] = []; - - const variables = parseUrlVariables(targetVal.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}" url variable.`, - path: [...ctx.path, 'variables', variable], - }); - }); - } - - return results; - }, -); diff --git a/packages/rulesets/src/asyncapi/functions/utils/getMissingProps.ts b/packages/rulesets/src/asyncapi/functions/utils/getMissingProps.ts deleted file mode 100644 index 72d5784ed..000000000 --- a/packages/rulesets/src/asyncapi/functions/utils/getMissingProps.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function getMissingProps(arr: string[] = [], obj: Record = {}): string[] { - if (!Object.keys(obj).length) return arr; - return arr.filter(val => { - return !Object.prototype.hasOwnProperty.call(obj, val); - }); -} diff --git a/packages/rulesets/src/asyncapi/functions/utils/getRedundantProps.ts b/packages/rulesets/src/asyncapi/functions/utils/getRedundantProps.ts deleted file mode 100644 index d7a9fdeae..000000000 --- a/packages/rulesets/src/asyncapi/functions/utils/getRedundantProps.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function getRedundantProps(arr: string[] = [], obj: Record = {}): string[] { - if (!arr.length) return Object.keys(obj); - return Object.keys(obj).filter(val => { - return !arr.includes(val); - }); -} diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index eb20fa1fb..680cda751 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -16,7 +16,7 @@ import asyncApi2MessageIdUniqueness from './functions/asyncApi2MessageIdUniquene import asyncApi2OperationIdUniqueness from './functions/asyncApi2OperationIdUniqueness'; import asyncApi2SchemaValidation from './functions/asyncApi2SchemaValidation'; import asyncApi2PayloadValidation from './functions/asyncApi2PayloadValidation'; -import asyncApi2ServerVariables from './functions/asyncApi2ServerVariables'; +import serverVariables from '../shared/functions/serverVariables'; import { uniquenessTags } from '../shared/functions'; import asyncApi2Security from './functions/asyncApi2Security'; import { latestVersion } from './functions/utils/specs'; @@ -370,7 +370,7 @@ export default { recommended: true, given: ['$.servers.*', '$.components.servers.*'], then: { - function: asyncApi2ServerVariables, + function: serverVariables, }, }, 'asyncapi-server-no-empty-variable': { diff --git a/packages/rulesets/src/oas/__tests__/oas3-server-variables.test.ts b/packages/rulesets/src/oas/__tests__/oas3-server-variables.test.ts new file mode 100644 index 000000000..487eb1f34 --- /dev/null +++ b/packages/rulesets/src/oas/__tests__/oas3-server-variables.test.ts @@ -0,0 +1,308 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import testRule from './__helpers__/tester'; + +testRule('oas3-server-variables', [ + { + name: 'valid case', + document: { + openapi: '3.1.0', + servers: [ + { + url: '{protocol}://{env}.stoplight.io:{port}', + variables: { + env: { + default: 'v2', + }, + protocol: { + enum: ['http', 'https'], + default: 'https', + }, + port: { + enum: ['80', '443'], + default: '443', + }, + }, + }, + ], + paths: { + '/': { + servers: [], + responses: { + '2xx': { + links: { + user: { + $ref: '#/components/links/User', + }, + }, + }, + }, + }, + '/user': { + get: { + operationId: 'getUser', + }, + }, + }, + components: { + links: { + User: { + operationId: 'getUser', + parameters: [], + server: { + url: 'https://{env}.stoplight.io', + variables: { + env: { + default: 'v2', + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, + + { + name: 'server has not defined definition for one of the url variables', + document: { + openapi: '3.1.0', + servers: [ + { + url: '{protocol}://stoplight.io:{port}', + variables: {}, + }, + ], + paths: { + '/': { + servers: [ + { + url: '{protocol}://stoplight.io', + variables: {}, + }, + ], + get: { + servers: [ + { + url: 'https://{env}.stoplight.io', + variables: {}, + }, + ], + }, + }, + }, + }, + errors: [ + { + message: 'Not all server\'s variables are described with "variables" object. Missed: protocol, port.', + path: ['servers', '0', 'variables'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Not all server\'s variables are described with "variables" object. Missed: protocol.', + path: ['paths', '/', 'servers', '0', 'variables'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Not all server\'s variables are described with "variables" object. Missed: env.', + path: ['paths', '/', 'get', 'servers', '0', 'variables'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'server has unused url variables', + document: { + openapi: '3.1.0', + servers: [ + { + url: 'https://stoplight.io:{port}', + variables: { + port: { + default: '443', + }, + env: { + enum: ['staging', 'integration'], + }, + }, + }, + ], + paths: { + '/': { + servers: [ + { + url: 'https://{env}.stoplight.io', + variables: { + port: { + default: '443', + }, + env: { + enum: ['staging', 'integration'], + }, + }, + }, + ], + get: { + servers: [ + { + url: 'https://stoplight.io', + variables: { + port: {}, + env: {}, + }, + }, + ], + }, + }, + }, + }, + errors: [ + { + message: 'Server\'s "variables" object has unused defined "env" url variable.', + path: ['servers', '0', 'variables', 'env'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Server\'s "variables" object has unused defined "port" url variable.', + path: ['paths', '/', 'servers', '0', 'variables', 'port'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Server\'s "variables" object has unused defined "port" url variable.', + path: ['paths', '/', 'get', 'servers', '0', 'variables', 'port'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Server\'s "variables" object has unused defined "env" url variable.', + path: ['paths', '/', 'get', 'servers', '0', 'variables', 'env'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'server has an unlisted default', + document: { + openapi: '3.1.0', + servers: [ + { + url: 'https://stoplight.io:{port}', + variables: { + port: { + enum: ['80'], + default: '443', + }, + }, + }, + ], + paths: { + '/': { + operationId: 'test', + servers: [ + { + url: 'https://{env}.stoplight.io', + variables: { + env: { + enum: [], + default: 'staging', + }, + }, + }, + ], + }, + }, + components: { + links: { + Address: { + operationId: 'test', + server: { + url: 'https://{env}.stoplight.io', + variables: { + env: { + enum: [], + default: 'staging', + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + 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', + path: ['paths', '/', 'servers', '0', 'variables', 'env', 'default'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Server Variable "env" has a default not listed in the enum', + path: ['components', 'links', 'Address', 'server', 'variables', 'env', 'default'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'server has a server variable resulting in broken URL', + document: { + openapi: '3.1.0', + servers: [ + { + url: 'https://stoplight.io:{port}', + variables: { + port: { + enum: ['invalid port', 'another-one', '443'], + }, + }, + }, + { + url: '{username}', + variables: { + username: { + enum: ['stoplight', 'io'], + }, + }, + }, + ], + paths: { + '/': { + servers: [ + { + url: '{base}.test', + variables: { + base: { + enum: ['http', 'https', 'ftp', 'ftps', 'ssh', 'smtp'], + }, + }, + }, + ], + }, + }, + }, + errors: [ + { + message: + 'A few substitutions of server variables resulted in invalid URLs: https://stoplight.io:invalid%20port, https://stoplight.io:another-one', + path: ['servers', '0', 'variables'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'A few substitutions of server variables resulted in invalid URLs: stoplight, io', + path: ['servers', '1', 'variables'], + severity: DiagnosticSeverity.Error, + }, + { + message: + 'At least 5 substitutions of server variables resulted in invalid URLs: http.test, https.test, ftp.test, ftps.test, ssh.test and more', + path: ['paths', '/', 'servers', '0', 'variables'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index 120b39953..98094f2e1 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -26,6 +26,7 @@ import { oasDiscriminator, } from './functions'; import { uniquenessTags } from '../shared/functions'; +import serverVariables from '../shared/functions/serverVariables'; export { ruleset as default }; @@ -35,6 +36,26 @@ const ruleset = { aliases: { PathItem: ['$.paths[*]'], OperationObject: ['#PathItem[get,put,post,delete,options,head,patch,trace]'], + ResponseObject: { + targets: [ + { + formats: [oas2], + given: ['#OperationObject.responses[*]', '$.responses[*]'], + }, + { + formats: [oas3], + given: ['#OperationObject.responses[*]', '$.components.responses[*]'], + }, + ], + }, + LinkObject: { + targets: [ + { + formats: [oas3], + given: ['$.components.links[*]', '#ResponseObject.links[*]'], + }, + ], + }, }, rules: { 'operation-success-response': { @@ -671,5 +692,18 @@ const ruleset = { function: oasUnusedComponent, }, }, + 'oas3-server-variables': { + description: 'Server variables must be defined and valid and there must be no unused variables.', + message: '{{error}}', + severity: 0, + recommended: true, + given: ['$.servers[*]', '#PathItem.servers[*]', '#OperationObject.servers[*]', '#LinkObject.server'], + then: { + function: serverVariables, + functionOptions: { + checkSubstitutions: true, + }, + }, + }, }, }; diff --git a/packages/rulesets/src/shared/functions/serverVariables/index.ts b/packages/rulesets/src/shared/functions/serverVariables/index.ts new file mode 100644 index 000000000..de2abe5e2 --- /dev/null +++ b/packages/rulesets/src/shared/functions/serverVariables/index.ts @@ -0,0 +1,168 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; +import type { IFunctionResult } from '@stoplight/spectral-core'; + +import { parseUrlVariables } from './utils/parseUrlVariables'; +import { getMissingProps } from '../../utils/getMissingProps'; +import { getRedundantProps } from '../../utils/getRedundantProps'; +import { applyUrlVariables } from './utils/applyUrlVariables'; +import { JsonPath } from '@stoplight/types'; + +type Input = { + url: string; + variables?: Record< + string, + { + enum: string[] | never; + default: string | never; + description: string | never; + examples: string | never; + [key: string]: unknown; // ^x- + } + >; +}; + +type Options = { + checkSubstitutions?: boolean; +} | null; + +export default createRulesetFunction( + { + input: { + errorMessage: 'Invalid Server Object', + type: 'object', + properties: { + url: { + type: 'string', + }, + variables: { + type: 'object', + additionalProperties: { + type: 'object', + properties: { + enum: { + type: 'array', + items: { + type: 'string', + }, + }, + default: { + type: 'string', + }, + description: { + type: 'string', + }, + examples: { + type: 'string', + }, + }, + patternProperties: { + '^x-': true, + }, + additionalProperties: false, + }, + }, + }, + required: ['url'], + }, + errorOnInvalidInput: true, + options: { + type: ['object', 'null'], + properties: { + checkSubstitutions: { + 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], + }); + } + + 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'], + }); + } + + const variablePairs: [key: string, values: string[]][] = []; + + for (const key of definedVariablesKeys) { + if (redundantVariables.includes(key)) continue; + + const values = variables[key]; + + if ('enum' in values) { + variablePairs.push([key, values.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'], + }); + } + } else { + variablePairs.push([key, [values.default ?? '']]); + } + } + + if (opts?.checkSubstitutions === true && variablePairs.length > 0) { + checkSubstitutions(results, ctx.path, url, variablePairs); + } + + return results; + }, +); + +function checkSubstitutions( + results: IFunctionResult[], + path: JsonPath, + url: string, + variables: [key: string, values: string[]][], +): void { + const invalidUrls: string[] = []; + + for (const substitutedUrl of applyUrlVariables(url, variables)) { + try { + new URL(substitutedUrl); + } catch { + invalidUrls.push(substitutedUrl); + if (invalidUrls.length === 5) { + break; + } + } + } + + if (invalidUrls.length === 5) { + results.push({ + message: `At least 5 substitutions of server variables resulted in invalid URLs: ${invalidUrls.join( + ', ', + )} and more`, + path: [...path, 'variables'], + }); + } else if (invalidUrls.length > 0) { + results.push({ + message: `A few substitutions of server variables resulted in invalid URLs: ${invalidUrls.join(', ')}`, + path: [...path, 'variables'], + }); + } +} diff --git a/packages/rulesets/src/shared/functions/serverVariables/utils/__tests__/applyUrlVariables.spec.ts b/packages/rulesets/src/shared/functions/serverVariables/utils/__tests__/applyUrlVariables.spec.ts new file mode 100644 index 000000000..2fd7db308 --- /dev/null +++ b/packages/rulesets/src/shared/functions/serverVariables/utils/__tests__/applyUrlVariables.spec.ts @@ -0,0 +1,22 @@ +import { applyUrlVariables } from '../applyUrlVariables'; + +describe('applyUrlVariables', () => { + test('should return all possible combinations', () => { + const result = [ + ...applyUrlVariables('{protocol}://{env}.stoplight.io:{port}', [ + ['protocol', ['https']], + ['env', ['integration', 'staging', 'prod']], + ['port', ['8080', '443']], + ]), + ]; + + expect(result).toStrictEqual([ + 'https://integration.stoplight.io:8080', + 'https://integration.stoplight.io:443', + 'https://staging.stoplight.io:8080', + 'https://staging.stoplight.io:443', + 'https://prod.stoplight.io:8080', + 'https://prod.stoplight.io:443', + ]); + }); +}); diff --git a/packages/rulesets/src/asyncapi/functions/utils/__tests__/parseUrlVariables.test.ts b/packages/rulesets/src/shared/functions/serverVariables/utils/__tests__/parseUrlVariables.test.ts similarity index 100% rename from packages/rulesets/src/asyncapi/functions/utils/__tests__/parseUrlVariables.test.ts rename to packages/rulesets/src/shared/functions/serverVariables/utils/__tests__/parseUrlVariables.test.ts diff --git a/packages/rulesets/src/shared/functions/serverVariables/utils/applyUrlVariables.ts b/packages/rulesets/src/shared/functions/serverVariables/utils/applyUrlVariables.ts new file mode 100644 index 000000000..7643c2ccf --- /dev/null +++ b/packages/rulesets/src/shared/functions/serverVariables/utils/applyUrlVariables.ts @@ -0,0 +1,36 @@ +type Variable = readonly [name: string, values: readonly string[]]; +type ApplicableVariable = readonly [name: RegExp, encodedValues: readonly string[]]; + +export function* applyUrlVariables(url: string, variables: readonly Variable[]): Iterable { + yield* _applyUrlVariables(url, 0, variables.map(toApplicableVariable)); +} + +// this loosely follows https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.2 +function* _applyUrlVariables(url: string, i: number, variables: readonly ApplicableVariable[]): Iterable { + const [name, values] = variables[i]; + let x = 0; + while (x < values.length) { + const substitutedValue = url.replace(name, values[x]); + + if (i === variables.length - 1) { + yield substitutedValue; + } else { + yield* _applyUrlVariables(substitutedValue, i + 1, variables); + } + + x++; + } +} + +function toApplicableVariable([name, values]: Variable): ApplicableVariable { + return [toReplaceRegExp(name), values.map(encodeURI)]; +} + +function toReplaceRegExp(name: string): RegExp { + return RegExp(escapeRegexp(`{${name}}`), 'g'); +} + +// https://github.com/tc39/proposal-regex-escaping/blob/main/polyfill.js +function escapeRegexp(value: string): string { + return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); +} diff --git a/packages/rulesets/src/asyncapi/functions/utils/parseUrlVariables.ts b/packages/rulesets/src/shared/functions/serverVariables/utils/parseUrlVariables.ts similarity index 100% rename from packages/rulesets/src/asyncapi/functions/utils/parseUrlVariables.ts rename to packages/rulesets/src/shared/functions/serverVariables/utils/parseUrlVariables.ts diff --git a/packages/rulesets/src/asyncapi/functions/utils/__tests__/getMissingProps.test.ts b/packages/rulesets/src/shared/utils/__tests__/getMissingProps.test.ts similarity index 63% rename from packages/rulesets/src/asyncapi/functions/utils/__tests__/getMissingProps.test.ts rename to packages/rulesets/src/shared/utils/__tests__/getMissingProps.test.ts index 928d06ae0..8edff3f18 100644 --- a/packages/rulesets/src/asyncapi/functions/utils/__tests__/getMissingProps.test.ts +++ b/packages/rulesets/src/shared/utils/__tests__/getMissingProps.test.ts @@ -2,17 +2,17 @@ import { getMissingProps } from '../getMissingProps'; describe('getMissingProps', () => { test('should return all props when object is empty', () => { - const result = getMissingProps(['one', 'two', 'three'], {}); + const result = getMissingProps(['one', 'two', 'three'], []); expect(result).toEqual(['one', 'two', 'three']); }); test('should return only missed props', () => { - const result = getMissingProps(['one', 'two', 'three'], { one: {}, three: {} }); + const result = getMissingProps(['one', 'two', 'three'], ['one', 'three']); expect(result).toEqual(['two']); }); test('should return empty array when all props are defined', () => { - const result = getMissingProps(['one', 'two', 'three'], { one: {}, two: {}, three: {} }); + const result = getMissingProps(['one', 'two', 'three'], ['one', 'two', 'three']); expect(result).toEqual([]); }); }); diff --git a/packages/rulesets/src/asyncapi/functions/utils/__tests__/getRedundantProps.test.ts b/packages/rulesets/src/shared/utils/__tests__/getRedundantProps.test.ts similarity index 62% rename from packages/rulesets/src/asyncapi/functions/utils/__tests__/getRedundantProps.test.ts rename to packages/rulesets/src/shared/utils/__tests__/getRedundantProps.test.ts index bad9660fa..cf28e7d75 100644 --- a/packages/rulesets/src/asyncapi/functions/utils/__tests__/getRedundantProps.test.ts +++ b/packages/rulesets/src/shared/utils/__tests__/getRedundantProps.test.ts @@ -2,17 +2,17 @@ import { getRedundantProps } from '../getRedundantProps'; describe('getRedundantProps', () => { test('should return all redundant props when array is empty', () => { - const result = getRedundantProps([], { one: {}, two: {}, three: {} }); + const result = getRedundantProps([], ['one', 'two', 'three']); expect(result).toEqual(['one', 'two', 'three']); }); test('should return only redundant props', () => { - const result = getRedundantProps(['one', 'three'], { one: {}, two: {}, three: {} }); + const result = getRedundantProps(['one', 'three'], ['one', 'two', 'three']); expect(result).toEqual(['two']); }); test('should return empty array when all props are defined', () => { - const result = getRedundantProps(['one', 'two', 'three'], { one: {}, two: {}, three: {} }); + const result = getRedundantProps(['one', 'two', 'three'], ['one', 'two', 'three']); expect(result).toEqual([]); }); }); diff --git a/packages/rulesets/src/shared/utils/getMissingProps.ts b/packages/rulesets/src/shared/utils/getMissingProps.ts new file mode 100644 index 000000000..d5dd68c55 --- /dev/null +++ b/packages/rulesets/src/shared/utils/getMissingProps.ts @@ -0,0 +1,5 @@ +export function getMissingProps(arr: string[], props: string[]): string[] { + return arr.filter(val => { + return !props.includes(val); + }); +} diff --git a/packages/rulesets/src/shared/utils/getRedundantProps.ts b/packages/rulesets/src/shared/utils/getRedundantProps.ts new file mode 100644 index 000000000..20f4d8c2c --- /dev/null +++ b/packages/rulesets/src/shared/utils/getRedundantProps.ts @@ -0,0 +1,5 @@ +export function getRedundantProps(arr: string[], keys: string[]): string[] { + return keys.filter(val => { + return !arr.includes(val); + }); +}