diff --git a/package.json b/package.json index 1b7d4e30e3..8a28bf792b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "testPathIgnorePatterns": [ "__helpers__", "metrics/data", - "IPA\\d+ValidOperationID\\.test\\.js$" + "IPA\\d+ValidOperationID\\.test\\.js$" ] }, "dependencies": { diff --git a/tools/spectral/ipa/__tests__/utils/extensions.test.js b/tools/spectral/ipa/__tests__/utils/extensions.test.js new file mode 100644 index 0000000000..7a20020237 --- /dev/null +++ b/tools/spectral/ipa/__tests__/utils/extensions.test.js @@ -0,0 +1,69 @@ +import { describe, it, expect, toBe } from '@jest/globals'; +import { + hasMethodWithVerbOverride, + hasCustomMethodOverride, + hasMethodVerbOverride, +} from '../../rulesets/functions/utils/extensions'; + +const methodWithExtension = { + 'x-xgen-method-verb-override': { + verb: 'get', + customMethod: false, + }, +}; + +const customMethod = { + 'x-xgen-method-verb-override': { + verb: 'add', + customMethod: true, + }, +}; + +const endpointWithMethodExtension = { + delete: { + 'x-xgen-method-verb-override': { verb: 'remove', customMethod: true }, + }, +}; + +const endpointWithNoMethodExtension = { + exception: true, +}; + +describe('tools/spectral/ipa/rulesets/functions/utils/extensions.js', () => { + describe('hasMethodWithVerbOverride', () => { + it('returns true if endpoint has method with the extension', () => { + expect(hasMethodWithVerbOverride(endpointWithMethodExtension)).toBe(true); + }); + it('returns false if object does not a method with the extension', () => { + expect(hasMethodWithVerbOverride(endpointWithNoMethodExtension)).toBe(false); + }); + }); +}); + +describe('tools/spectral/ipa/rulesets/functions/utils/extensions.js', () => { + describe('hasCustomMethodOverride', () => { + it('returns true if the method has the extension with the cusotmMethod boolean set to true', () => { + expect(hasCustomMethodOverride(customMethod)).toBe(true); + }); + it('returns false if the method does not have the extension', () => { + expect(hasCustomMethodOverride({})).toBe(false); + }); + it('returns false if the method has the extension but is not a custom method', () => { + expect(hasCustomMethodOverride(methodWithExtension)).toBe(false); + }); + }); +}); + +describe('tools/spectral/ipa/rulesets/functions/utils/extensions.js', () => { + describe('hasMethodVerbOverride', () => { + it('returns true if the method has the extension with the expected verb', () => { + expect(hasMethodVerbOverride(methodWithExtension, 'get')).toBe(true); + }); + it('returns false if the method does not have the extension', () => { + expect(hasMethodVerbOverride({}, 'get')).toBe(false); + }); + it('returns false if the method has the extension but with an unexpected verb', () => { + expect(hasMethodVerbOverride(methodWithExtension, 'put')).toBe(false); + }); + }); +}); diff --git a/tools/spectral/ipa/__tests__/utils/operationIdGeneration.test.js b/tools/spectral/ipa/__tests__/utils/operationIdGeneration.test.js index b39ac9cd98..185987d00f 100644 --- a/tools/spectral/ipa/__tests__/utils/operationIdGeneration.test.js +++ b/tools/spectral/ipa/__tests__/utils/operationIdGeneration.test.js @@ -28,7 +28,7 @@ describe('tools/spectral/ipa/utils/operationIdGeneration.js', () => { expect(generateOperationID('addNode', '/groups/{groupId}/clusters/{clusterName}')).toEqual('addGroupClusterNode'); expect(generateOperationID('get', '/api/atlas/v2/groups/byName/{groupName}')).toEqual('getGroupByName'); expect(generateOperationID('', '/api/atlas/v2/groups/{groupId}/backup/exportBuckets/{exportBucketId}')).toEqual( - 'exportGroupBackupBuckets' + 'exportGroupBackupBucket' ); }); diff --git a/tools/spectral/ipa/rulesets/functions/IPA104ValidOperationID.js b/tools/spectral/ipa/rulesets/functions/IPA104ValidOperationID.js index f2583bfbb4..60dee4bdd3 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA104ValidOperationID.js +++ b/tools/spectral/ipa/rulesets/functions/IPA104ValidOperationID.js @@ -1,13 +1,9 @@ import { generateOperationID } from './utils/operationIdGeneration.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; import { hasException } from './utils/exceptions.js'; -import { - isSingleResourceIdentifier, - isResourceCollectionIdentifier, - isSingletonResource, - getResourcePathItems, - isCustomMethodIdentifier, -} from './utils/resourceEvaluation.js'; +import { getResourcePathItems, isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; +import { hasCustomMethodOverride, hasMethodVerbOverride } from './utils/extensions.js'; +import { isInvalidGetMethod } from './utils/methodLogic.js'; const RULE_NAME = 'xgen-IPA-104-valid-operation-id'; const ERROR_MESSAGE = 'Invalid OperationID.'; @@ -18,9 +14,10 @@ export default (input, { methodName }, { path, documentInventory }) => { const resourcePaths = getResourcePathItems(resourcePath, oas.paths); if ( + hasCustomMethodOverride(input) || isCustomMethodIdentifier(resourcePath) || - (!isSingleResourceIdentifier(resourcePath) && - !(isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePaths))) + hasMethodVerbOverride(input, 'list') || + (isInvalidGetMethod(resourcePath, resourcePaths) && !hasMethodVerbOverride(input, methodName)) ) { return; } diff --git a/tools/spectral/ipa/rulesets/functions/IPA105ValidOperationID.js b/tools/spectral/ipa/rulesets/functions/IPA105ValidOperationID.js index f35a694631..fef0997cca 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA105ValidOperationID.js +++ b/tools/spectral/ipa/rulesets/functions/IPA105ValidOperationID.js @@ -1,12 +1,9 @@ import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; -import { - getResourcePathItems, - isCustomMethodIdentifier, - isResourceCollectionIdentifier, - isSingletonResource, -} from './utils/resourceEvaluation.js'; +import { getResourcePathItems, isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; import { generateOperationID } from './utils/operationIdGeneration.js'; +import { isInvalidListMethod } from './utils/methodLogic.js'; +import { hasCustomMethodOverride, hasMethodVerbOverride } from './utils/extensions.js'; const RULE_NAME = 'xgen-IPA-105-valid-operation-id'; const ERROR_MESSAGE = 'Invalid OperationID.'; @@ -14,11 +11,13 @@ const ERROR_MESSAGE = 'Invalid OperationID.'; export default (input, { methodName }, { path, documentInventory }) => { const resourcePath = path[1]; const oas = documentInventory.resolved; + const resourcePaths = getResourcePathItems(resourcePath, oas.paths); if ( + hasCustomMethodOverride(input) || isCustomMethodIdentifier(resourcePath) || - !isResourceCollectionIdentifier(resourcePath) || - isSingletonResource(getResourcePathItems(resourcePath, oas.paths)) + hasMethodVerbOverride(input, 'get') || + (isInvalidListMethod(resourcePath, resourcePaths) && !hasMethodVerbOverride(input, methodName)) ) { return; } diff --git a/tools/spectral/ipa/rulesets/functions/IPA106ValidOperationID.js b/tools/spectral/ipa/rulesets/functions/IPA106ValidOperationID.js index 0b4c5039ad..428fd33b20 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA106ValidOperationID.js +++ b/tools/spectral/ipa/rulesets/functions/IPA106ValidOperationID.js @@ -1,23 +1,16 @@ import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectException, collectAndReturnViolation } from './utils/collectionUtils.js'; -import { - isCustomMethodIdentifier, - isResourceCollectionIdentifier, - isSingletonResource, - getResourcePathItems, -} from './utils/resourceEvaluation.js'; +import { isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; import { generateOperationID } from './utils/operationIdGeneration.js'; +import { hasCustomMethodOverride } from './utils/extensions.js'; const RULE_NAME = 'xgen-IPA-106-valid-operation-id'; const ERROR_MESSAGE = 'Invalid OperationID.'; -export default (input, { methodName }, { path, documentInventory }) => { +export default (input, { methodName }, { path }) => { const resourcePath = path[1]; - const oas = documentInventory.resolved; - const resourcePaths = getResourcePathItems(resourcePath, oas.paths); - const isResourceCollection = isResourceCollectionIdentifier(resourcePath) && !isSingletonResource(resourcePaths); - if (isCustomMethodIdentifier(resourcePath) || !isResourceCollection) { + if (hasCustomMethodOverride(input) || isCustomMethodIdentifier(resourcePath)) { return; } diff --git a/tools/spectral/ipa/rulesets/functions/IPA107ValidOperationID.js b/tools/spectral/ipa/rulesets/functions/IPA107ValidOperationID.js index 5e95184694..1500354412 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA107ValidOperationID.js +++ b/tools/spectral/ipa/rulesets/functions/IPA107ValidOperationID.js @@ -1,27 +1,16 @@ import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectException, collectAndReturnViolation } from './utils/collectionUtils.js'; -import { - isSingleResourceIdentifier, - isResourceCollectionIdentifier, - isSingletonResource, - getResourcePathItems, - isCustomMethodIdentifier, -} from './utils/resourceEvaluation.js'; +import { isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; import { generateOperationID } from './utils/operationIdGeneration.js'; +import { hasCustomMethodOverride } from './utils/extensions.js'; const RULE_NAME = 'xgen-IPA-107-valid-operation-id'; const ERROR_MESSAGE = 'Invalid OperationID.'; -export default (input, { methodName }, { path, documentInventory }) => { +export default (input, { methodName }, { path }) => { const resourcePath = path[1]; - const oas = documentInventory.resolved; - const resourcePaths = getResourcePathItems(resourcePath, oas.paths); - if ( - isCustomMethodIdentifier(resourcePath) || - (!isSingleResourceIdentifier(resourcePath) && - !(isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePaths))) - ) { + if (isCustomMethodIdentifier(resourcePath) || hasCustomMethodOverride(input)) { return; } diff --git a/tools/spectral/ipa/rulesets/functions/IPA108ValidOperationID.js b/tools/spectral/ipa/rulesets/functions/IPA108ValidOperationID.js index 3f4dfef420..7e049a815b 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA108ValidOperationID.js +++ b/tools/spectral/ipa/rulesets/functions/IPA108ValidOperationID.js @@ -1,7 +1,8 @@ import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectException, collectAndReturnViolation } from './utils/collectionUtils.js'; -import { isCustomMethodIdentifier, isSingleResourceIdentifier } from './utils/resourceEvaluation.js'; +import { isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; import { generateOperationID } from './utils/operationIdGeneration.js'; +import { hasCustomMethodOverride } from './utils/extensions.js'; const RULE_NAME = 'xgen-IPA-108-valid-operation-id'; const ERROR_MESSAGE = 'Invalid OperationID.'; @@ -9,7 +10,7 @@ const ERROR_MESSAGE = 'Invalid OperationID.'; export default (input, { methodName }, { path }) => { const resourcePath = path[1]; - if (isCustomMethodIdentifier(resourcePath) || !isSingleResourceIdentifier(resourcePath)) { + if (isCustomMethodIdentifier(resourcePath) || hasCustomMethodOverride(input)) { return; } diff --git a/tools/spectral/ipa/rulesets/functions/IPA109ValidOperationID.js b/tools/spectral/ipa/rulesets/functions/IPA109ValidOperationID.js index ec3a15f47d..ba432acb4e 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA109ValidOperationID.js +++ b/tools/spectral/ipa/rulesets/functions/IPA109ValidOperationID.js @@ -2,44 +2,68 @@ import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectException, collectAndReturnViolation } from './utils/collectionUtils.js'; import { isCustomMethodIdentifier, getCustomMethodName, stripCustomMethodName } from './utils/resourceEvaluation.js'; import { generateOperationID } from './utils/operationIdGeneration.js'; +import { hasMethodWithVerbOverride, hasCustomMethodOverride, VERB_OVERRIDE_EXTENSION } from './utils/extensions.js'; const RULE_NAME = 'xgen-IPA-109-valid-operation-id'; const ERROR_MESSAGE = 'Invalid OperationID.'; export default (input, _, { path }) => { let resourcePath = path[1]; - const methodName = getCustomMethodName(resourcePath); - if (!isCustomMethodIdentifier(resourcePath)) { + if (!isCustomMethodIdentifier(resourcePath) && !hasMethodWithVerbOverride(input)) { return; } - // TODO detect custom method extension - CLOUDP-306294 - - let obj; - if (input.post) { - obj = input.post; - } else if (input.get) { - obj = input.get; - } else { + if (hasException(input, RULE_NAME)) { + collectException(input, RULE_NAME, path); return; } - if (hasException(obj, RULE_NAME)) { - collectException(obj, RULE_NAME, path); - return; - } + if (isCustomMethodIdentifier(resourcePath)) { + let obj; + if (input.post) { + obj = input.post; + } else if (input.get) { + obj = input.get; + } else { + return; + } - const operationId = obj.operationId; - const expectedOperationID = generateOperationID(methodName, stripCustomMethodName(resourcePath)); - if (expectedOperationID !== operationId) { - const errors = [ - { - path, - message: `${ERROR_MESSAGE} Found ${operationId}, expected ${expectedOperationID}.`, - }, - ]; - return collectAndReturnViolation(path, RULE_NAME, errors); + const operationId = obj.operationId; + const expectedOperationID = generateOperationID( + getCustomMethodName(resourcePath), + stripCustomMethodName(resourcePath) + ); + if (operationId !== expectedOperationID) { + const errors = [ + { + path, + message: `${ERROR_MESSAGE} Found ${operationId}, expected ${expectedOperationID}.`, + }, + ]; + return collectAndReturnViolation(path, RULE_NAME, errors); + } + } else if (hasMethodWithVerbOverride(input)) { + const methods = Object.values(input); + let errors = []; + methods.forEach((method) => { + if (hasCustomMethodOverride(method)) { + const operationId = method.operationId; + const expectedOperationID = generateOperationID(method[VERB_OVERRIDE_EXTENSION].verb, resourcePath); + if (operationId !== expectedOperationID) { + errors.push({ + path, + message: `${ERROR_MESSAGE} Found ${operationId}, expected ${expectedOperationID}.`, + }); + } + } + }); + + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + } else { + return; } collectAdoption(path, RULE_NAME); diff --git a/tools/spectral/ipa/rulesets/functions/utils/extensions.js b/tools/spectral/ipa/rulesets/functions/utils/extensions.js new file mode 100644 index 0000000000..335d2be13c --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/utils/extensions.js @@ -0,0 +1,45 @@ +export const VERB_OVERRIDE_EXTENSION = 'x-xgen-method-verb-override'; + +/** + * Checks if the endpoint has a method with an extension "x-xgen-method-verb-override" + * + * @param endpoint the endpoint to evaluate + * @returns {boolean} true if the endpoint has a nested method with the extension, otherwise false + */ +export function hasMethodWithVerbOverride(endpoint) { + return Object.values(endpoint).some(hasVerbOverride); +} + +/** + * Checks if the object has an extension "x-xgen-method-verb-override" with the customMethod boolean set to true + * + * @param object the object to evaluate + * @returns {boolean} true if the object has an extension with customMethod=True, otherwise false + */ +export function hasCustomMethodOverride(object) { + return hasVerbOverride(object) && object[VERB_OVERRIDE_EXTENSION].customMethod; +} + +/** + * Checks if the object has an extension "x-xgen-method-verb-override" with the verb set to a specific verb + * + * @param object the object to evaluate + * @param verb the verb to inspect the extension for + * @returns {boolean} true if the object has the extension with the given verb, otherwise false + */ +export function hasMethodVerbOverride(object, verb) { + return hasVerbOverride(object) && object[VERB_OVERRIDE_EXTENSION].verb === verb; +} + +/** + * Checks if the object has an extension "x-xgen-method-verb-override" + * + * @param object the object to evaluate + * @returns {boolean} true if the object has the extension, otherwise false + */ +function hasVerbOverride(object) { + if (!object[VERB_OVERRIDE_EXTENSION]) { + return false; + } + return true; +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/methodLogic.js b/tools/spectral/ipa/rulesets/functions/utils/methodLogic.js new file mode 100644 index 0000000000..3677186895 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/utils/methodLogic.js @@ -0,0 +1,33 @@ +import { isResourceCollectionIdentifier, isSingletonResource } from './resourceEvaluation.js'; + +/** + * Checks whether the get method at a given path is valid + * + * @param resourcePath the resource path to inspect + * @param resourcePaths the resource paths generated by getResourcePathItems + * @returns true if the resourcePath has an invalid get method, false otherwise + */ +export function isInvalidGetMethod(resourcePath, resourcePaths) { + return ( + !lastIdentifierIsPathParam(resourcePath) && + !(isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePaths)) + ); +} + +/** + * Checks whether the list method at a given path is valid + * + * @param resourcePath the resource path to inspect + * @param resourcePaths the resource paths generated by getResourcePathItems + * @returns true if the resourcePath has an invalid list method, false otherwise + */ +export function isInvalidListMethod(resourcePath, resourcePaths) { + return lastIdentifierIsPathParam(resourcePath) || isSingletonResource(resourcePaths); +} + +function lastIdentifierIsPathParam(resourceIdentifier) { + if (resourceIdentifier.includes('.')) { + resourceIdentifier = resourceIdentifier.substring(0, resourceIdentifier.lastIndexOf('.')); + } + return resourceIdentifier.at(-1) === '}'; +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js b/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js index 12ee3cfe98..69921f42db 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js +++ b/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js @@ -10,7 +10,15 @@ const CAMEL_CASE = /[A-Z]?[a-z]+/g; * @param path the path for the endpoint */ export function generateOperationID(method, path) { + if (!path) { + return method; + } + let resourceIdentifier = removePrefix(path); + if (resourceIdentifier.includes('.')) { + resourceIdentifier = resourceIdentifier.substring(0, resourceIdentifier.lastIndexOf('.')); + } + let nouns = resourceIdentifier.split('/').filter((section) => section.length > 0 && !isPathParam(section)); // legacy custom method - use end of path as custom method name @@ -33,7 +41,11 @@ export function generateOperationID(method, path) { } // singularize final noun, dependent on resource identifier - if (isSingleResourceIdentifier(resourceIdentifier) || verb === 'create') { + if ( + isPathParam(resourceIdentifier.split('/').pop()) || + isSingleResourceIdentifier(resourceIdentifier) || + verb === 'create' + ) { nouns[nouns.length - 1] = inflection.singularize(nouns[nouns.length - 1]); }