Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,20 @@ 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: [],
},
]);
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,20 @@ 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: [],
},
]);
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,19 @@ 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: [],
},
]);
Original file line number Diff line number Diff line change
@@ -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.';
Expand All @@ -21,7 +22,11 @@ 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you update the rule descriptions for these to clarify the exception behaviour?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule documentation updated 👍

const pathWithException = findExceptionInPathHierarchy(oas, pathKey, RULE_NAME);
const objectToCheck = pathWithException ? oas.paths[pathWithException] : oas.paths[input];

return evaluateAndCollectAdoptionStatus(violations, RULE_NAME, objectToCheck, path);
};

function checkViolations(pathKey, path, ignoredValues = []) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -17,7 +18,11 @@ export default (input, _, { path, documentInventory }) => {

const violations = checkViolations(input, path);

return evaluateAndCollectAdoptionStatus(violations, RULE_NAME, oas.paths[input], path);
// Check for exceptions in path hierarchy
const pathWithException = findExceptionInPathHierarchy(oas, input, RULE_NAME);
const objectToCheck = pathWithException ? oas.paths[pathWithException] : oas.paths[input];

return evaluateAndCollectAdoptionStatus(violations, RULE_NAME, objectToCheck, path);
};

function checkViolations(pathKey, path) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.';
Expand Down Expand Up @@ -37,7 +38,11 @@ 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 pathWithException = findExceptionInPathHierarchy(oas, input, RULE_NAME);
const objectToCheck = pathWithException ? oas.paths[pathWithException] : oas.paths[input];

return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, objectToCheck, path);
};

function checkViolationsAndReturnErrors(suffixWithLeadingSlash, path) {
Expand Down
29 changes: 29 additions & 0 deletions tools/spectral/ipa/rulesets/functions/utils/exceptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,32 @@ export function hasException(object, ruleName) {
}
return false;
}

/**
* Finds an exception in the path hierarchy of an OpenAPI Specification (OAS) document
* for a specific rule name, starting from the current path and traversing up to parent paths.
*
* @param {object} oas - OpenAPI Specification document containing the paths.
* @param {string} currentPath - Current path to check for exceptions.
* @param {string} ruleName - Name of the rule for which the exception is being checked.
* @return {string|null} Returns the path where the exception is found,
* or null if no exception is found in the current path or its hierarchy.
*/
export function findExceptionInPathHierarchy(oas, currentPath, ruleName) {
// Check current path first
if (hasException(oas.paths[currentPath], ruleName)) {
return currentPath;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we return a spectral error if the current path has an exception as well as the parent? So we can identify the unnecessary ones?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Added. Could you double-check?


// 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)) {
return parentPath;
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that if atlas/api/v2.0/groups/{groupId}/cluster/{clusterName} has an exception also that exception is applied to atlas/api/v2.0/groups/{groupId}/?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it is the opposite, if atlas/api/v2.0/groups/{groupId}/ has an exception for the rule, atlas/api/v2.0/groups/{groupId}/cluster/{clusterName} will inherit it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarifications. Why do we want to add this functionality? atlas/api/v2.0/groups/{groupId} and atlas/api/v2.0/groups/{groupId}/cluster/{clusterName} are two separated resources

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, when API producers define new API endpoints using Swagger annotations in Java, they specify only the child path segment to append to a parent path. They are not responsible for, nor can they modify, the parent path itself and exceptions on it.

This way, any new method extending the parent path does not require separate exceptions. API producers won’t need to add exceptions each time, though we will still collect metrics as if the child paths had defined exceptions

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's say that atlas/api/v2.0/groups/{groupId}/ has an exception to xgen-IPA-104-resource-has-GET which means that the GET method is missing. Why should we applied this exception to all the child resources?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only implemented for IPA-102 guidelines, which is covering the path formatting. It does not apply to other principles

return null;
}
Loading