diff --git a/tools/spectral/ipa/__tests__/IPA102CollectionIdentifierCamelCase.test.js b/tools/spectral/ipa/__tests__/IPA102CollectionIdentifierCamelCase.test.js index 4c8866c516..3f62a10a6e 100644 --- a/tools/spectral/ipa/__tests__/IPA102CollectionIdentifierCamelCase.test.js +++ b/tools/spectral/ipa/__tests__/IPA102CollectionIdentifierCamelCase.test.js @@ -254,4 +254,85 @@ testRule('xgen-IPA-102-collection-identifier-camelCase', [ }, ], }, + { + name: 'child paths inherit parent exceptions', + document: { + paths: { + '/resource_groups': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-collection-identifier-camelCase': 'Legacy API path that cannot be changed', + }, + }, + '/resource_groups/{id}': {}, + '/resource_groups/{id}/User-Profiles': {}, + '/resource_groups/{id}/User-Profiles/{profileId}': {}, + }, + }, + errors: [], + }, + { + name: 'child paths have exceptions along with parent exceptions', + document: { + paths: { + '/resource_groups': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-collection-identifier-camelCase': 'Legacy API path that cannot be changed', + }, + }, + '/resource_groups/{id}': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-collection-identifier-camelCase': 'Legacy API path that cannot be changed', + }, + }, + '/resource_groups/{id}/User-Profiles': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-collection-identifier-camelCase': 'Legacy API path that cannot be changed', + }, + }, + '/resource_groups/{id}/User-Profiles/{profileId}': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-collection-identifier-camelCase': 'Legacy API path that cannot be changed', + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + 'This component adopts the rule and does not need an exception. Please remove the exception. https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-102-collection-identifier-camelCase', + path: [ + 'paths', + '/resource_groups/{id}', + 'x-xgen-IPA-exception', + 'xgen-IPA-102-collection-identifier-camelCase', + ], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + 'This component adopts the rule and does not need an exception. Please remove the exception. https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-102-collection-identifier-camelCase', + path: [ + 'paths', + '/resource_groups/{id}/User-Profiles', + 'x-xgen-IPA-exception', + 'xgen-IPA-102-collection-identifier-camelCase', + ], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + 'This component adopts the rule and does not need an exception. Please remove the exception. https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-102-collection-identifier-camelCase', + path: [ + 'paths', + '/resource_groups/{id}/User-Profiles/{profileId}', + 'x-xgen-IPA-exception', + 'xgen-IPA-102-collection-identifier-camelCase', + ], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/tools/spectral/ipa/__tests__/IPA102CollectionIdentifierPattern.test.js b/tools/spectral/ipa/__tests__/IPA102CollectionIdentifierPattern.test.js index 8b9e7e8f0d..74b950692d 100644 --- a/tools/spectral/ipa/__tests__/IPA102CollectionIdentifierPattern.test.js +++ b/tools/spectral/ipa/__tests__/IPA102CollectionIdentifierPattern.test.js @@ -89,4 +89,80 @@ testRule('xgen-IPA-102-collection-identifier-pattern', [ }, errors: [], }, + { + name: 'child paths inherit parent exceptions', + document: { + paths: { + '/resource-groups': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-collection-identifier-pattern': 'Legacy API path that cannot be changed', + }, + }, + '/resource-groups/{id}': {}, + '/resource-groups/{id}/sub_resources': {}, + '/resource-groups/{id}/sub_resources/{subId}': {}, + }, + }, + errors: [], + }, + { + name: 'child paths have exceptions along with parent exceptions', + document: { + paths: { + '/resource-groups': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-collection-identifier-pattern': 'Legacy API path that cannot be changed', + }, + }, + '/resource-groups/{id}': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-collection-identifier-pattern': 'Legacy API path that cannot be changed', + }, + }, + '/resource-groups/{id}/sub_resources': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-collection-identifier-pattern': 'Legacy API path that cannot be changed', + }, + }, + '/resource-groups/{id}/sub_resources/{subId}': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-collection-identifier-pattern': 'Legacy API path that cannot be changed', + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-pattern', + message: + 'This component adopts the rule and does not need an exception. Please remove the exception. https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-102-collection-identifier-pattern', + path: ['paths', '/resource-groups/{id}', 'x-xgen-IPA-exception', 'xgen-IPA-102-collection-identifier-pattern'], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-102-collection-identifier-pattern', + message: + 'This component adopts the rule and does not need an exception. Please remove the exception. https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-102-collection-identifier-pattern', + path: [ + 'paths', + '/resource-groups/{id}/sub_resources', + 'x-xgen-IPA-exception', + 'xgen-IPA-102-collection-identifier-pattern', + ], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-102-collection-identifier-pattern', + message: + 'This component adopts the rule and does not need an exception. Please remove the exception. https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-102-collection-identifier-pattern', + path: [ + 'paths', + '/resource-groups/{id}/sub_resources/{subId}', + 'x-xgen-IPA-exception', + 'xgen-IPA-102-collection-identifier-pattern', + ], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js b/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js index 41a6e0cfbf..9bb8a2cc91 100644 --- a/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js +++ b/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js @@ -152,4 +152,67 @@ testRule('xgen-IPA-102-path-alternate-resource-name-path-param', [ }, errors: [], }, + { + name: 'child paths inherit parent exceptions', + document: { + paths: { + '/api/atlas/v2/resourceName1/resourceName2': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-path-alternate-resource-name-path-param': 'parent exception reason', + }, + }, + '/api/atlas/v2/resourceName1/resourceName2/child': {}, + '/api/atlas/v2/resourceName1/resourceName2/child/{id}': {}, + }, + }, + errors: [], + }, + { + name: 'child paths have exceptions along with parent exceptions', + document: { + paths: { + '/api/atlas/v2/resourceName1/resourceName2': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-path-alternate-resource-name-path-param': 'parent exception reason', + }, + }, + '/api/atlas/v2/resourceName1/resourceName2/child': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-path-alternate-resource-name-path-param': 'child exception reason', + }, + }, + '/api/atlas/v2/resourceName1/resourceName2/child/{id}': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-path-alternate-resource-name-path-param': 'child exception reason', + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: + 'This component adopts the rule and does not need an exception. Please remove the exception. https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-102-path-alternate-resource-name-path-param', + path: [ + 'paths', + '/api/atlas/v2/resourceName1/resourceName2/child', + 'x-xgen-IPA-exception', + 'xgen-IPA-102-path-alternate-resource-name-path-param', + ], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: + 'This component adopts the rule and does not need an exception. Please remove the exception. https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-102-path-alternate-resource-name-path-param', + path: [ + 'paths', + '/api/atlas/v2/resourceName1/resourceName2/child/{id}', + 'x-xgen-IPA-exception', + 'xgen-IPA-102-path-alternate-resource-name-path-param', + ], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/tools/spectral/ipa/__tests__/utils/collectionUtils.test.js b/tools/spectral/ipa/__tests__/utils/collectionUtils.test.js index 7e7b9e516a..6e98fa5042 100644 --- a/tools/spectral/ipa/__tests__/utils/collectionUtils.test.js +++ b/tools/spectral/ipa/__tests__/utils/collectionUtils.test.js @@ -125,8 +125,6 @@ describe('tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js', () => expect(result[0].message).toEqual( 'This component adopts the rule and does not need an exception. Please remove the exception.' ); - expect(collector.add).toHaveBeenCalledTimes(1); - expect(collector.add).toHaveBeenCalledWith(TEST_ENTRY_TYPE.VIOLATION, testPath, testRuleName); }); }); diff --git a/tools/spectral/ipa/ipa-spectral.yaml b/tools/spectral/ipa/ipa-spectral.yaml index 6dd8bde3a4..a46dedf224 100644 --- a/tools/spectral/ipa/ipa-spectral.yaml +++ b/tools/spectral/ipa/ipa-spectral.yaml @@ -58,7 +58,7 @@ overrides: - '**#/paths/~1rest~1unauth~1version' # external reference, to be covered by CLOUDP-309694 rules: xgen-IPA-114-error-responses-refer-to-api-error: 'off' - - files: + - files: # To be removed in CLOUDP-338425 - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1%7BclusterView%7D~1%7BdatabaseName%7D~1%7BcollectionName%7D~1collStats~1measurements' # reference to support future investigation - CLOUDP-310775 - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1processes~1%7BprocessId%7D~1%7BdatabaseName%7D~1%7BcollectionName%7D~1collStats~1measurements' # reference to support future investigation - CLOUDP-310775 - '**#/components/schemas/HostMetricValue' # reference to support future investigation - CLOUDP-310775 @@ -177,3 +177,40 @@ overrides: - '**#/components/schemas/NewRelic' rules: xgen-IPA-117-description-must-not-use-html: 'off' + - files: # To be removed in CLOUDP-338425 + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1backup~1exportBuckets~1%7BexportBucketId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1backup~1exports~1%7BexportId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1backup~1restoreJobs~1%7BrestoreJobId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1backup~1snapshots~1%7BsnapshotId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1backup~1snapshots~1shardedCluster~1%7BsnapshotId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1backup~1snapshots~1shardedClusters' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1backup~1tenant~1restores~1%7BrestoreId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1backup~1tenant~1snapshots~1%7BsnapshotId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1fts~1indexes~1%7BdatabaseName%7D~1%7BcollectionName%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1fts~1indexes~1%7BindexId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1search~1indexes~1%7BdatabaseName%7D~1%7BcollectionName%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1search~1indexes~1%7BdatabaseName%7D~1%7BcollectionName%7D~1%7BindexName%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1clusters~1%7BclusterName%7D~1search~1indexes~1%7BindexId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1customDBRoles~1roles~1%7BroleName%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1flexClusters~1%7Bname%7D~1backup~1restoreJobs~1%7BrestoreJobId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1flexClusters~1%7Bname%7D~1backup~1snapshots~1%7BsnapshotId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1hosts~1%7BprocessId%7D~1fts~1metrics~1measurements' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1privateEndpoint~1serverless~1instance~1%7BinstanceName%7D~1endpoint~1%7BendpointId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1privateNetworkSettings~1endpointIds~1%7BendpointId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1serverless~1%7BclusterName%7D~1backup~1restoreJobs~1%7BrestoreJobId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1serverless~1%7BclusterName%7D~1backup~1snapshots~1%7BsnapshotId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1streams~1privateLinkConnections~1%7BconnectionId%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1streams~1vpcPeeringConnections~1%7Bid%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1streams~1vpcPeeringConnections~1%7Bid%7D%3Aaccept' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1streams~1vpcPeeringConnections~1%7Bid%7D%3Areject' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1userSecurity~1ldap~1verify~1%7BrequestId%7D' + - '**#/paths/~1api~1atlas~1v2~1orgs~1%7BorgId%7D~1billing~1costExplorer~1usage~1%7Btoken%7D' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1hosts~1%7BprocessId%7D~1fts~1metrics~1indexes~1%7BdatabaseName%7D~1%7BcollectionName%7D~1measurements' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1hosts~1%7BprocessId%7D~1fts~1metrics~1indexes~1%7BdatabaseName%7D~1%7BcollectionName%7D~1%7BindexName%7D~1measurements' + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1liveMigrations~1validate~1%7BvalidationId%7D' + rules: + xgen-IPA-102-path-alternate-resource-name-path-param: 'off' + - files: # To be removed in CLOUDP-338425 + - '**#/paths/~1api~1atlas~1v2~1groups~1%7BgroupId%7D~1customDBRoles~1roles~1%7BroleName%7D' + rules: + xgen-IPA-102-collection-identifier-camelCase: 'off' diff --git a/tools/spectral/ipa/rulesets/IPA-102.yaml b/tools/spectral/ipa/rulesets/IPA-102.yaml index 82cb3ab0e1..d573295097 100644 --- a/tools/spectral/ipa/rulesets/IPA-102.yaml +++ b/tools/spectral/ipa/rulesets/IPA-102.yaml @@ -16,6 +16,7 @@ rules: argument to the rule - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation - Double slashes (//) are not allowed in paths + - If any parent path has an exception for this rule, the exception will be inherited. message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-102-collection-identifier-camelCase' severity: error @@ -38,6 +39,8 @@ rules: - Even-indexed path segments should be resource names (not path parameters) - Odd-indexed path segments should be path parameters - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation + - If any parent path has an exception for this rule, the exception will be inherited. + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-102-path-alternate-resource-name-path-param' severity: error given: '$.paths' @@ -57,6 +60,7 @@ rules: - Custom methods (segments containing colons) are excluded from validation - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation - Each non-parameter path segment must start with a lowercase letter followed by any combination of ASCII letters and numbers + - If any parent path has an exception for this rule, the exception will be inherited. message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-102-collection-identifier-pattern' severity: error diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index fa16b86cbd..475eb49d9e 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -44,6 +44,7 @@ Collection identifiers must be in camelCase. argument to the rule - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation - Double slashes (//) are not allowed in paths + - If any parent path has an exception for this rule, the exception will be inherited. #### xgen-IPA-102-path-alternate-resource-name-path-param @@ -57,6 +58,7 @@ Rule checks for the following conditions: - Even-indexed path segments should be resource names (not path parameters) - Odd-indexed path segments should be path parameters - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation + - If any parent path has an exception for this rule, the exception will be inherited. #### xgen-IPA-102-collection-identifier-pattern @@ -71,6 +73,7 @@ Rule checks for the following conditions: - Custom methods (segments containing colons) are excluded from validation - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation - Each non-parameter path segment must start with a lowercase letter followed by any combination of ASCII letters and numbers + - If any parent path has an exception for this rule, the exception will be inherited. diff --git a/tools/spectral/ipa/rulesets/functions/IPA102CollectionIdentifierCamelCase.js b/tools/spectral/ipa/rulesets/functions/IPA102CollectionIdentifierCamelCase.js index ce1f039277..1a9e1930d8 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA102CollectionIdentifierCamelCase.js +++ b/tools/spectral/ipa/rulesets/functions/IPA102CollectionIdentifierCamelCase.js @@ -1,6 +1,7 @@ import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; import { isPathParam } from './utils/componentUtils.js'; import { casing } from '@stoplight/spectral-functions'; +import { findExceptionInPathHierarchy } from './utils/exceptions.js'; const RULE_NAME = 'xgen-IPA-102-collection-identifier-camelCase'; const ERROR_MESSAGE = 'Collection identifiers must be in camelCase.'; @@ -8,6 +9,8 @@ const ERROR_MESSAGE = 'Collection identifiers must be in camelCase.'; /** * Checks if collection identifiers in paths follow camelCase convention * + * The function checks the entire path hierarchy. If any parent path has an exception, the exception will be inherited. + * * @param {object} input - The path key from the OpenAPI spec * @param {object} options - Rule configuration options * @param {object} context - The context object containing the path and documentInventory @@ -21,7 +24,14 @@ export default (input, options, { path, documentInventory }) => { const violations = checkViolations(pathKey, path, ignoredValues); - return evaluateAndCollectAdoptionStatus(violations, RULE_NAME, oas.paths[input], path); + // Check for exceptions in path hierarchy + const result = findExceptionInPathHierarchy(oas, pathKey, RULE_NAME, path); + if (result?.error) { + return result.error; + } + const objectToCheckForException = result ? oas.paths[result.parentPath] : oas.paths[input]; + + return evaluateAndCollectAdoptionStatus(violations, RULE_NAME, objectToCheckForException, path); }; function checkViolations(pathKey, path, ignoredValues = []) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA102CollectionIdentifierPattern.js b/tools/spectral/ipa/rulesets/functions/IPA102CollectionIdentifierPattern.js index 5aa57ee944..40bba90d2e 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA102CollectionIdentifierPattern.js +++ b/tools/spectral/ipa/rulesets/functions/IPA102CollectionIdentifierPattern.js @@ -1,4 +1,5 @@ import { evaluateAndCollectAdoptionStatus } from './utils/collectionUtils.js'; +import { findExceptionInPathHierarchy } from './utils/exceptions.js'; const RULE_NAME = 'xgen-IPA-102-collection-identifier-pattern'; const ERROR_MESSAGE = @@ -8,16 +9,25 @@ const VALID_IDENTIFIER_PATTERN = /^[a-z][a-zA-Z0-9]*$/; /** * Checks if collection identifiers in paths begin with a lowercase letter and contain only ASCII letters and numbers * + * The function checks the entire path hierarchy. If any parent path has an exception, the exception will be inherited. + * * @param {object} input - The paths object from the OpenAPI spec * @param {object} _ - Unused - * @param {object} context - The context object containing the path + * @param {object} context - The context object containing the path and documentInventory */ export default (input, _, { path, documentInventory }) => { const oas = documentInventory.resolved; const violations = checkViolations(input, path); - return evaluateAndCollectAdoptionStatus(violations, RULE_NAME, oas.paths[input], path); + // Check for exceptions in path hierarchy + const result = findExceptionInPathHierarchy(oas, input, RULE_NAME, path); + if (result?.error) { + return result.error; + } + const objectToCheckForException = result ? oas.paths[result.parentPath] : oas.paths[input]; + + return evaluateAndCollectAdoptionStatus(violations, RULE_NAME, objectToCheckForException, path); }; function checkViolations(pathKey, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js b/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js index 5d49b27023..85891cce38 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js +++ b/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js @@ -1,6 +1,7 @@ import { isPathParam } from './utils/componentUtils.js'; import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; import { AUTH_PREFIX, UNAUTH_PREFIX } from './utils/resourceEvaluation.js'; +import { findExceptionInPathHierarchy } from './utils/exceptions.js'; const RULE_NAME = 'xgen-IPA-102-path-alternate-resource-name-path-param'; const ERROR_MESSAGE = 'API paths must alternate between resource name and path params.'; @@ -22,6 +23,15 @@ const validatePathStructure = (elements) => { }); }; +/** + * Checks if the resource identifier components alternate between collection identifiers and resourceIDs + * + * The function checks the entire path hierarchy. If any parent path has an exception, the exception will be inherited. + * + * @param {object} input - The path key from the OpenAPI spec + * @param {object} _ - Unused + * @param {object} context - The context object containing the path and documentInventory + */ export default (input, _, { path, documentInventory }) => { const oas = documentInventory.resolved; @@ -37,7 +47,15 @@ export default (input, _, { path, documentInventory }) => { const errors = checkViolationsAndReturnErrors(suffixWithLeadingSlash, path); - return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, oas.paths[input], path); + // Check for exceptions in path hierarchy + const result = findExceptionInPathHierarchy(oas, input, RULE_NAME, path); + if (result?.error) { + return result.error; + } + + const objectToCheckForException = result ? oas.paths[result.parentPath] : oas.paths[input]; + + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, objectToCheckForException, path); }; function checkViolationsAndReturnErrors(suffixWithLeadingSlash, path) { diff --git a/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js b/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js index c55ad9689c..eba0e27625 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js @@ -1,10 +1,10 @@ import collector, { EntryType } from '../../../metrics/collector.js'; -import { EXCEPTION_EXTENSION, hasException } from './exceptions.js'; +import { EXCEPTION_EXTENSION, getUnnecessaryExceptionError, hasException } from './exceptions.js'; /** * Evaluates and collects adoptions, exceptions and violations based on the rule, evaluated object and the validation errors. * If the object is violating the rule, but has an exception, the validation error is ignored - * If the object is adopting the rule, but has an exception, a validation error will be returned + * If the object is adopting the rule, but has an exception, an unnecessary exception error is returned, but the object is counted as adopting the rule * * @param {Array<{path: Array, message: string}>} validationErrors the error results from the rule * @param {string} ruleName the name of the rule @@ -20,15 +20,11 @@ export function evaluateAndCollectAdoptionStatus(validationErrors, ruleName, obj } return collectAndReturnViolation(objectPath, ruleName, validationErrors); } + collectAdoption(objectPath, ruleName); + if (hasException(object, ruleName)) { - return collectAndReturnViolation(objectPath, ruleName, [ - { - path: [...objectPath, EXCEPTION_EXTENSION, ruleName], - message: 'This component adopts the rule and does not need an exception. Please remove the exception.', - }, - ]); + return returnViolation(getUnnecessaryExceptionError(objectPath, ruleName)); } - collectAdoption(objectPath, ruleName); } /** @@ -60,6 +56,10 @@ export function evaluateAndCollectAdoptionStatusWithoutExceptions(validationErro function collectAndReturnViolation(jsonPath, ruleName, errorData) { collector.add(EntryType.VIOLATION, jsonPath, ruleName); + return returnViolation(errorData); +} + +export function returnViolation(errorData) { if (typeof errorData === 'string') { return [{ message: errorData }]; } else if (Array.isArray(errorData)) { diff --git a/tools/spectral/ipa/rulesets/functions/utils/exceptions.js b/tools/spectral/ipa/rulesets/functions/utils/exceptions.js index 256cab89dc..1edf519735 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/exceptions.js +++ b/tools/spectral/ipa/rulesets/functions/utils/exceptions.js @@ -13,3 +13,43 @@ export function hasException(object, ruleName) { } return false; } + +export function getUnnecessaryExceptionError(objectPath, ruleName) { + return [ + { + path: [...objectPath, EXCEPTION_EXTENSION, ruleName], + message: 'This component adopts the rule and does not need an exception. Please remove the exception.', + }, + ]; +} + +/** + * Finds an exception for a specified rule name in the current path or its parent paths within the given OpenAPI Specification (OAS) object. + * + * @param {Object} oas - The OpenAPI Specification object containing path definitions. + * @param {string} currentPath - The path to start searching for exceptions. + * @param {string} ruleName - The name of the rule to check for exceptions. + * @param {string} jsonPath - The JSON path to the current operation or entity being checked. + * @return {string|null|Object} The parent path with the rule exception if found, or null if no exceptions exist. + */ +export function findExceptionInPathHierarchy(oas, currentPath, ruleName, jsonPath) { + let currentPathHasException = false; + if (hasException(oas.paths[currentPath], ruleName)) { + currentPathHasException = true; + } + + // Check parent paths by removing segments from the end + const pathSegments = currentPath.split('/').filter((segment) => segment !== ''); + + for (let i = pathSegments.length - 1; i > 0; i--) { + const parentPath = '/' + pathSegments.slice(0, i).join('/'); + if (oas.paths[parentPath] && hasException(oas.paths[parentPath], ruleName)) { + if (currentPathHasException) { + return { error: getUnnecessaryExceptionError(jsonPath, ruleName) }; + } + return { parentPath: parentPath }; + } + } + + return null; +}