diff --git a/tools/spectral/ipa/__tests__/IPA104ValidOperationID.test.js b/tools/spectral/ipa/__tests__/IPA104ValidOperationID.test.js index 1ecc24552c..47b3e70cb3 100644 --- a/tools/spectral/ipa/__tests__/IPA104ValidOperationID.test.js +++ b/tools/spectral/ipa/__tests__/IPA104ValidOperationID.test.js @@ -67,6 +67,15 @@ testRule('xgen-IPA-104-valid-operation-id', [ name: 'valid methods with valid overrides', document: { paths: { + '/api/atlas/v2': { + get: { + 'x-xgen-method-verb-override': { + verb: 'getSystemStatus', + customMethod: false, + }, + operationId: 'getSystemStatus', + }, + }, '/api/atlas/v2/federationSettings/{federationSettingsId}/connectedOrgConfigs/{orgId}/roleMappings/{id}': { get: { operationId: 'getFederationSettingConnectedOrgConfigRoleMapping', @@ -122,7 +131,10 @@ testRule('xgen-IPA-104-valid-operation-id', [ '/api/atlas/v2/groups/{groupId}/streams/{tenantName}': { get: { operationId: 'getGroupStreamWorkspace', - 'x-xgen-method-verb-override': { verb: 'getWorkspace', customMethod: false }, + 'x-xgen-method-verb-override': { + verb: 'getWorkspace', + customMethod: false, + }, }, }, }, diff --git a/tools/spectral/ipa/__tests__/IPA105ValidOperationID.test.js b/tools/spectral/ipa/__tests__/IPA105ValidOperationID.test.js index 6bf654c213..7121dfe5c3 100644 --- a/tools/spectral/ipa/__tests__/IPA105ValidOperationID.test.js +++ b/tools/spectral/ipa/__tests__/IPA105ValidOperationID.test.js @@ -50,7 +50,7 @@ testRule('xgen-IPA-105-valid-operation-id', [ { code: 'xgen-IPA-105-valid-operation-id', message: - "Invalid OperationID. Found 'returnAllControlPlaneIpAddresses', expected 'listControlPlaneIPAddresses'. ", + "Invalid OperationID. Found 'returnAllControlPlaneIpAddresses', expected 'listControlPlaneIpAddresses'. ", path: ['paths', '/api/atlas/v2/unauth/controlPlaneIPAddresses', 'get', 'operationId'], severity: DiagnosticSeverity.Warning, }, @@ -73,6 +73,12 @@ testRule('xgen-IPA-105-valid-operation-id', [ 'x-xgen-operation-id-override': 'listExportBuckets', }, }, + '/api/atlas/v2/unauth/controlPlaneIPAddresses': { + get: { + operationId: 'listControlPlaneIpAddresses', + 'x-xgen-operation-id-override': 'listControlPlaneAddresses', + }, + }, }, }, errors: [], @@ -112,7 +118,10 @@ testRule('xgen-IPA-105-valid-operation-id', [ '/api/atlas/v2/groups/{groupId}/serverless': { get: { operationId: 'listGroupServerlessInstances', - 'x-xgen-method-verb-override': { verb: 'listInstances', customMethod: false }, + 'x-xgen-method-verb-override': { + verb: 'listInstances', + customMethod: false, + }, 'x-xgen-operation-id-override': 'listServerlessInstances', }, }, diff --git a/tools/spectral/ipa/__tests__/IPA109ValidOperationID.test.js b/tools/spectral/ipa/__tests__/IPA109ValidOperationID.test.js index d5aa026fd2..0bd9659262 100644 --- a/tools/spectral/ipa/__tests__/IPA109ValidOperationID.test.js +++ b/tools/spectral/ipa/__tests__/IPA109ValidOperationID.test.js @@ -74,10 +74,19 @@ testRule('xgen-IPA-109-valid-operation-id', [ '/api/atlas/v2/federationSettings/{federationSettingsId}/connectedOrgConfigs/{orgId}': { delete: { operationId: 'removeFederationSettingConnectedOrgConfig', - 'x-xgen-method-verb-override': { verb: 'remove', customMethod: true }, + 'x-xgen-method-verb-override': { + verb: 'remove', + customMethod: true, + }, 'x-xgen-operation-id-override': 'removeConnectedOrgConfig', }, }, + '/api/atlas/v2/groups/{groupId}/clusters/{clusterName}:revokeMongoDBEmployeeAccess': { + delete: { + operationId: 'revokeGroupClusterMongoDbEmployeeAccess', + 'x-xgen-operation-id-override': 'revokeEmployeeAccess', + }, + }, }, }, errors: [], @@ -89,7 +98,10 @@ testRule('xgen-IPA-109-valid-operation-id', [ '/api/atlas/v2/federationSettings/{federationSettingsId}/connectedOrgConfigs/{orgId}': { delete: { operationId: 'removeFederationSettingConnectedOrgConfig', - 'x-xgen-method-verb-override': { verb: 'remove', customMethod: true }, + 'x-xgen-method-verb-override': { + verb: 'remove', + customMethod: true, + }, 'x-xgen-operation-id-override': 'removeOrgConfigTest', }, }, diff --git a/tools/spectral/ipa/__tests__/utils/operationIdGeneration.test.js b/tools/spectral/ipa/__tests__/utils/operationIdGeneration.test.js index 6fb11e856c..b6c0522412 100644 --- a/tools/spectral/ipa/__tests__/utils/operationIdGeneration.test.js +++ b/tools/spectral/ipa/__tests__/utils/operationIdGeneration.test.js @@ -50,6 +50,19 @@ describe('tools/spectral/ipa/utils/operationIdGeneration.js', () => { expect(generateOperationID('get', '')).toEqual('get'); expect(generateOperationID('getInfo', '')).toEqual('getInfo'); }); + + it('should transform uppercase abbreviations and numbers to camel case correctly', () => { + expect(generateOperationID('get', '/api/atlas/v2/groups/{groupId}/openAPI')).toEqual('getGroupOpenApi'); + expect(generateOperationID('list', '/api/atlas/v2/unauth/controlPlaneIPAddresses')).toEqual( + 'listControlPlaneIpAddresses' + ); + expect(generateOperationID('delete', '/api/atlas/v2/groups/{groupId}/userSecurity/ldap/userToDNMapping')).toEqual( + 'deleteGroupUserSecurityLdapUserToDnMapping' + ); + expect(generateOperationID('get', '/api/atlas/v2/groups/{groupId}/userSecurity/customerX509')).toEqual( + 'getGroupUserSecurityCustomerX509' + ); + }); }); describe('numberOfWords', () => { @@ -60,6 +73,8 @@ describe('tools/spectral/ipa/utils/operationIdGeneration.js', () => { expect(numberOfWords('createGroupClusterIndex')).toEqual(4); expect(numberOfWords('getOpenAPIInfo')).toEqual(4); expect(numberOfWords('getCustomDNS')).toEqual(3); + expect(numberOfWords('getX509Certificate')).toEqual(3); + expect(numberOfWords('X509Certificate')).toEqual(2); expect(numberOfWords('')).toEqual(0); }); }); @@ -72,6 +87,7 @@ describe('tools/spectral/ipa/utils/operationIdGeneration.js', () => { expect(shortenOperationId('getFederationSettingConnectedOrgConfigRoleMapping')).toEqual('getConfigRoleMapping'); expect(shortenOperationId('getGroupAwsCustomDNS')).toEqual('getAwsCustomDNS'); expect(shortenOperationId('getExampleOpenAPIInfo')).toEqual('getOpenAPIInfo'); + expect(shortenOperationId('getGroupUserX509Certificate')).toEqual('getUserX509Certificate'); }); it('should make no change if the operation ID is <= 4 words long or undefined', () => { diff --git a/tools/spectral/ipa/rulesets/IPA-109.yaml b/tools/spectral/ipa/rulesets/IPA-109.yaml index 08b72e8509..fc71434144 100644 --- a/tools/spectral/ipa/rulesets/IPA-109.yaml +++ b/tools/spectral/ipa/rulesets/IPA-109.yaml @@ -7,6 +7,10 @@ functions: - IPA109CustomMethodIdentifierFormat - IPA109ValidOperationID +aliases: + OperationObject: + - '$.paths[*][get,put,post,delete,options,head,patch,trace]' + rules: xgen-IPA-109-custom-method-must-be-GET-or-POST: description: | @@ -76,7 +80,7 @@ rules: - `ignorePluralizationList`: Words that are allowed to maintain their assumed plurality (e.g., "Fts") message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-109-valid-operation-id' severity: warn - given: '$.paths[*][*]' + given: '#OperationObject' then: function: 'IPA109ValidOperationID' functionOptions: diff --git a/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js b/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js index 5471455677..8c6a235418 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js +++ b/tools/spectral/ipa/rulesets/functions/utils/operationIdGeneration.js @@ -2,7 +2,7 @@ const inflection = require('inflection'); import { isPathParam, removePrefix, isSingleResourceIdentifier } from './resourceEvaluation.js'; const CAMEL_CASE = /[A-Z]?[a-z]+/g; -const CAMEL_CASE_WITH_ABBREVIATIONS = /[A-Z]+(?![a-z])|[A-Z]*[a-z]+/g; +export const CAMEL_CASE_WITH_ABBREVIATIONS = /[A-Z]+(?![a-z0-9])|[A-Z]*[a-z0-9]+/g; /** * Returns IPA Compliant Operation ID. @@ -40,7 +40,7 @@ export function generateOperationID(method, path, ignorePluralizationList = []) let opID = verb; for (let i = 0; i < nouns.length - 1; i++) { - opID += singularize(nouns[i], ignorePluralizationList); + opID += upperCamelCase(singularize(nouns[i], ignorePluralizationList)); } // singularize final noun, dependent on resource identifier - leave custom nouns alone @@ -52,13 +52,13 @@ export function generateOperationID(method, path, ignorePluralizationList = []) nouns[nouns.length - 1] = singularize(nouns[nouns.length - 1], ignorePluralizationList); } - opID += nouns.pop(); + opID += upperCamelCase(nouns.pop()); return opID; } /** - * Counts the number of words in a camelCase string. Allows for abbreviations (e.g. 'getOpenAPI'). + * Counts the number of words in a camelCase string. Allows for abbreviations (e.g. 'getOpenAPI') and numbers (e.g. 'X509'). * @param operationId * @returns {number} */ @@ -99,3 +99,13 @@ function singularize(noun, ignorePluralizationList = []) { } return noun; } + +function upperCamelCase(input) { + if (input) { + return input + .match(CAMEL_CASE_WITH_ABBREVIATIONS) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(''); + } + return input; +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/validations/validateOperationIdAndReturnErrors.js b/tools/spectral/ipa/rulesets/functions/utils/validations/validateOperationIdAndReturnErrors.js index daa7b1cb9d..e12037c5be 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/validations/validateOperationIdAndReturnErrors.js +++ b/tools/spectral/ipa/rulesets/functions/utils/validations/validateOperationIdAndReturnErrors.js @@ -1,8 +1,11 @@ -import { generateOperationID, numberOfWords, shortenOperationId } from '../operationIdGeneration.js'; +import { + CAMEL_CASE_WITH_ABBREVIATIONS, + generateOperationID, + numberOfWords, + shortenOperationId, +} from '../operationIdGeneration.js'; import { getOperationIdOverride, hasOperationIdOverride, OPERATION_ID_OVERRIDE_EXTENSION } from '../extensions.js'; -const CAMEL_CASE = /[A-Z]?[a-z]+/g; - const INVALID_OP_ID_ERROR_MESSAGE = 'Invalid OperationID.'; const TOO_LONG_OP_ID_ERROR_MESSAGE = "The Operation ID is longer than 4 words. Please add an '" + @@ -16,6 +19,7 @@ const TOO_LONG_OP_ID_ERROR_MESSAGE = * @param resourcePath the resource path for the endpoint (e.g. '/users', '/users/{userId}', etc.). For custom methods, this is the path without the custom method name. * @param operationObject the operation object to validate, which should contain the operationId and optionally the x-xgen-operation-id-override extension. * @param path the path to the operation object being evaluated, used for error reporting with Spectral. + * @param ignorePluralizationList an array of nouns to ignore when singularizing resource names. * @returns {[{path: string[], message: string}]} an array of error objects, each containing a path and a message, or an empty array if no errors are found. */ export function validateOperationIdAndReturnErrors( @@ -60,7 +64,7 @@ export function validateOperationIdAndReturnErrors( } function validateOperationIdOverride(operationIdOverridePath, override, expectedOperationId) { - const expectedVerb = expectedOperationId.match(CAMEL_CASE)[0]; + const expectedVerb = expectedOperationId.match(CAMEL_CASE_WITH_ABBREVIATIONS)[0]; const errors = []; if (!override.startsWith(expectedVerb)) { errors.push({ @@ -76,7 +80,7 @@ function validateOperationIdOverride(operationIdOverridePath, override, expected }); } - const overrideWords = override.match(CAMEL_CASE).slice(1); + const overrideWords = override.match(CAMEL_CASE_WITH_ABBREVIATIONS).slice(1); if (overrideWords.some((word) => !expectedOperationId.includes(word))) { errors.push({ path: operationIdOverridePath, @@ -84,7 +88,8 @@ function validateOperationIdOverride(operationIdOverridePath, override, expected }); } - const expectedLastNoun = expectedOperationId.match(CAMEL_CASE)[numberOfWords(expectedOperationId) - 1]; + const expectedLastNoun = + expectedOperationId.match(CAMEL_CASE_WITH_ABBREVIATIONS)[numberOfWords(expectedOperationId) - 1]; if (!override.endsWith(expectedLastNoun)) { errors.push({ path: operationIdOverridePath,