diff --git a/.prettierignore b/.prettierignore index 317beb2aa0..dc246e01d9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,4 @@ *.yaml *.yml *.html +!/tools/spectral/ipa/**/*.yaml \ No newline at end of file diff --git a/tools/spectral/ipa/__tests__/eachPathAlternatesBetweenResourceNameAndPathParam.test.js b/tools/spectral/ipa/__tests__/eachPathAlternatesBetweenResourceNameAndPathParam.test.js new file mode 100644 index 0000000000..e78401cbf8 --- /dev/null +++ b/tools/spectral/ipa/__tests__/eachPathAlternatesBetweenResourceNameAndPathParam.test.js @@ -0,0 +1,137 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +testRule('xgen-IPA-102-path-alternate-resource-name-path-param', [ + { + name: 'valid paths - api/atlas/v2', + document: { + paths: { + '/api/atlas/v2/resourceName': {}, + '/api/atlas/v2/resourceName/{pathParam}': {}, + '/api/atlas/v2/resourceName1/{pathParam}/resourceName2': {}, + '/api/atlas/v2/resourceName1/{pathParam1p}/resourceName2/{pathParam2}': {}, + '/api/atlas/v2/resourceName/{pathParam}:method': {}, + '/api/atlas/v2/custom:method': {}, + '/api/atlas/v2': {}, + }, + }, + errors: [], + }, + { + name: 'valid paths - api/atlas/v2/unauth', + document: { + paths: { + '/api/atlas/v2/unauth/resourceName': {}, + '/api/atlas/v2/unauth/resourceName/{pathParam}': {}, + '/api/atlas/v2/unauth/resourceName1/{pathParam}/resourceName2': {}, + '/api/atlas/v2/unauth/resourceName1/{pathParam1p}/resourceName2/{pathParam2}': {}, + '/api/atlas/v2/unauth/resourceName/{pathParam}:method': {}, + '/api/atlas/v2/unauth/custom:method': {}, + '/api/atlas/v2/unauth': {}, + }, + }, + errors: [], + }, + { + name: 'invalid paths - api/atlas/v2', + document: { + paths: { + '/api/atlas/v2/resourceName1/resourceName2': {}, + '/api/atlas/v2/resourceName/{pathParam1}/{pathParam2}': {}, + '/api/atlas/v2/resourceName1/{pathParam1}/resourceName2/resourceName3': {}, + '/api/atlas/v2/resourceName1/{pathParam1}/resourceName2/{pathParam2}/{pathParam3}': {}, + '/api/atlas/v2/{pathParam}': {}, + '/api/atlas/v2/{pathParam1}/{pathParam2}': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/resourceName1/resourceName2'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/resourceName/{pathParam1}/{pathParam2}'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/resourceName1/{pathParam1}/resourceName2/resourceName3'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/resourceName1/{pathParam1}/resourceName2/{pathParam2}/{pathParam3}'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/{pathParam}'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/{pathParam1}/{pathParam2}'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid paths - api/atlas/v2/unauth', + document: { + paths: { + '/api/atlas/v2/unauth/resourceName1/resourceName2': {}, + '/api/atlas/v2/unauth/resourceName/{pathParam1}/{pathParam2}': {}, + '/api/atlas/v2/unauth/resourceName1/{pathParam1}/resourceName2/resourceName3': {}, + '/api/atlas/v2/unauth/resourceName1/{pathParam1}/resourceName2/{pathParam2}/{pathParam3}': {}, + '/api/atlas/v2/unauth/{pathParam}': {}, + '/api/atlas/v2/unauth/{pathParam1}/{pathParam2}': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/unauth/resourceName1/resourceName2'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/unauth/resourceName/{pathParam1}/{pathParam2}'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/unauth/resourceName1/{pathParam1}/resourceName2/resourceName3'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/unauth/resourceName1/{pathParam1}/resourceName2/{pathParam2}/{pathParam3}'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/unauth/{pathParam}'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params. http://go/ipa/102', + path: ['paths', '/api/atlas/v2/unauth/{pathParam1}/{pathParam2}'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, +]); diff --git a/tools/spectral/ipa/__tests__/eachResourceHasGetMethod.test.js b/tools/spectral/ipa/__tests__/eachResourceHasGetMethod.test.js index dc03a39a57..64df503f92 100644 --- a/tools/spectral/ipa/__tests__/eachResourceHasGetMethod.test.js +++ b/tools/spectral/ipa/__tests__/eachResourceHasGetMethod.test.js @@ -95,31 +95,31 @@ testRule('xgen-IPA-104-resource-has-GET', [ errors: [ { code: 'xgen-IPA-104-resource-has-GET', - message: 'APIs must provide a get method for resources. http://go/ipa/117', + message: 'APIs must provide a get method for resources. http://go/ipa/104', path: ['paths', '/standard'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-104-resource-has-GET', - message: 'APIs must provide a get method for resources. http://go/ipa/117', + message: 'APIs must provide a get method for resources. http://go/ipa/104', path: ['paths', '/standard/{exampleId}/nested'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-104-resource-has-GET', - message: 'APIs must provide a get method for resources. http://go/ipa/117', + message: 'APIs must provide a get method for resources. http://go/ipa/104', path: ['paths', '/standard/{exampleId}/nestedSingleton'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-104-resource-has-GET', - message: 'APIs must provide a get method for resources. http://go/ipa/117', + message: 'APIs must provide a get method for resources. http://go/ipa/104', path: ['paths', '/custom'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-104-resource-has-GET', - message: 'APIs must provide a get method for resources. http://go/ipa/117', + message: 'APIs must provide a get method for resources. http://go/ipa/104', path: ['paths', '/singleton'], severity: DiagnosticSeverity.Warning, }, diff --git a/tools/spectral/ipa/ipa-spectral.yaml b/tools/spectral/ipa/ipa-spectral.yaml index 893169f21d..9e5e2b3ab7 100644 --- a/tools/spectral/ipa/ipa-spectral.yaml +++ b/tools/spectral/ipa/ipa-spectral.yaml @@ -1,2 +1,3 @@ extends: + - ./rulesets/IPA-102.yaml - ./rulesets/IPA-104.yaml diff --git a/tools/spectral/ipa/rulesets/IPA-102.yaml b/tools/spectral/ipa/rulesets/IPA-102.yaml new file mode 100644 index 0000000000..e2e2176137 --- /dev/null +++ b/tools/spectral/ipa/rulesets/IPA-102.yaml @@ -0,0 +1,15 @@ +# IPA-102: Resource Identifiers +# http://go/ipa/102 + +functions: + - eachPathAlternatesBetweenResourceNameAndPathParam + +rules: + xgen-IPA-102-path-alternate-resource-name-path-param: + description: 'Paths should alternate between resource names and path params. http://go/ipa/102' + message: '{{error}} http://go/ipa/102' + severity: warn + given: '$.paths' + then: + field: '@key' + function: 'eachPathAlternatesBetweenResourceNameAndPathParam' diff --git a/tools/spectral/ipa/rulesets/IPA-104.yaml b/tools/spectral/ipa/rulesets/IPA-104.yaml index 47bd8649c2..f0ef7eed24 100644 --- a/tools/spectral/ipa/rulesets/IPA-104.yaml +++ b/tools/spectral/ipa/rulesets/IPA-104.yaml @@ -6,10 +6,10 @@ functions: rules: xgen-IPA-104-resource-has-GET: - description: "APIs must provide a get method for resources. http://go/ipa/104" - message: "{{error}} http://go/ipa/117" + description: 'APIs must provide a get method for resources. http://go/ipa/104' + message: '{{error}} http://go/ipa/104' severity: warn - given: "$.paths" + given: '$.paths' then: - field: "@key" - function: "eachResourceHasGetMethod" + field: '@key' + function: 'eachResourceHasGetMethod' diff --git a/tools/spectral/ipa/rulesets/functions/eachPathAlternatesBetweenResourceNameAndPathParam.js b/tools/spectral/ipa/rulesets/functions/eachPathAlternatesBetweenResourceNameAndPathParam.js new file mode 100644 index 0000000000..6fd1f96398 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/eachPathAlternatesBetweenResourceNameAndPathParam.js @@ -0,0 +1,35 @@ +import { isPathParam } from './utils/pathUtils'; + +const ERROR_MESSAGE = 'API paths must alternate between resource name and path params.'; +const ERROR_RESULT = [{ message: ERROR_MESSAGE }]; +const AUTH_PREFIX = '/api/atlas/v2'; +const UNAUTH_PREFIX = '/api/atlas/v2/unauth'; + +const getPrefix = (path) => { + if (path.includes(UNAUTH_PREFIX)) return UNAUTH_PREFIX; + if (path.includes(AUTH_PREFIX)) return AUTH_PREFIX; + return null; +}; + +const validatePathStructure = (elements) => { + return elements.every((element, index) => { + const isEvenIndex = index % 2 === 0; + return isEvenIndex ? !isPathParam(element) : isPathParam(element); + }); +}; + +export default (input) => { + const prefix = getPrefix(input); + if (!prefix) return; + + let suffixWithLeadingSlash = input.slice(prefix.length); + if (suffixWithLeadingSlash.length === 0) { + return; + } + + let suffix = suffixWithLeadingSlash.slice(1); + let elements = suffix.split('/'); + if (!validatePathStructure(elements)) { + return ERROR_RESULT; + } +}; diff --git a/tools/spectral/ipa/rulesets/functions/utils/pathUtils.js b/tools/spectral/ipa/rulesets/functions/utils/pathUtils.js new file mode 100644 index 0000000000..34d1910175 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/utils/pathUtils.js @@ -0,0 +1,14 @@ +/** + * Checks if a string belongs to a path parameter or a path parameter with a custom method. + * + * A path parameter has the format: `{paramName}` + * A path parameter with a custom method has the format: `{paramName}:customMethod` + * + * @param {string} str - A string extracted from a path split by slashes. + * @returns {boolean} True if the string matches the expected formats, false otherwise. + */ +export function isPathParam(str) { + const pathParamRegEx = new RegExp(`^{[a-z][a-zA-Z0-9]*}$`); + const pathParamWithCustomMethodRegEx = new RegExp(`^{[a-z][a-zA-Z0-9]*}:[a-z][a-zA-Z0-9]*$`); + return pathParamRegEx.test(str) || pathParamWithCustomMethodRegEx.test(str); +}