Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0d7775d
Test extension detection
Jul 10, 2025
c45f169
CLOUDP-306294: Get/List extension implementation
Jul 10, 2025
79da6ec
CLOUDP-306294: Final get/list logic
Jul 11, 2025
3f502ee
CLOUDP-306294: Final create, update, delete logic
Jul 11, 2025
875b880
CLOUDP-306294: Draft custom method extension handling
Jul 11, 2025
e00ebcf
CLOUDP-306294: Uncommented extension utilities
Jul 11, 2025
794456c
CLOUDP-306294: updated utilities to handle get/list edge cases
Jul 11, 2025
d3deaae
Edit to logic to handle outliers
Jul 14, 2025
9ae2c93
Edit logic to handle outliers
Jul 14, 2025
3879992
Merge branch 'main' into CLOUDP-306294
Jul 14, 2025
ea69c29
CLOUDP-306294: Refactored and commented opID related utilities
Jul 14, 2025
00a2052
fixed flawed test
Jul 14, 2025
1c864be
Added tests for extension utilities
Jul 14, 2025
6c2f9fc
CLOUDP-306294: silenced opIdgeneration tests + prettier
Jul 14, 2025
9680213
fix lint errors
Jul 14, 2025
a15ca0e
Update tools/spectral/ipa/rulesets/functions/utils/extensions.js
sphterry Jul 14, 2025
c95204c
Apply refactor suggestions from code review
sphterry Jul 14, 2025
e5f2adc
cleaned up hasMethodWithVerbOverride logic
Jul 14, 2025
c1dff45
Apply suggestions from code review
sphterry Jul 14, 2025
eca80ae
renamed utils and enabled opID unit tests
Jul 14, 2025
a4d1db9
Merge branch 'main' into CLOUDP-306294
Jul 15, 2025
dc29321
CLOUDP-306294: Refactored IPA109 Validation function + prettier
Jul 15, 2025
30fdbac
Simplify utility logic
Jul 15, 2025
c5cb6aa
remove import
Jul 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"testPathIgnorePatterns": [
"__helpers__",
"metrics/data",
"IPA\\d+ValidOperationID\\.test\\.js$"
"IPA\\d+ValidOperationID\\.test\\.js$"
]
},
"dependencies": {
Expand Down
69 changes: 69 additions & 0 deletions tools/spectral/ipa/__tests__/utils/extensions.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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.';
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
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.';

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;
}
Expand Down
15 changes: 4 additions & 11 deletions tools/spectral/ipa/rulesets/functions/IPA106ValidOperationID.js
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
19 changes: 4 additions & 15 deletions tools/spectral/ipa/rulesets/functions/IPA107ValidOperationID.js
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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.';

export default (input, { methodName }, { path }) => {
const resourcePath = path[1];

if (isCustomMethodIdentifier(resourcePath) || !isSingleResourceIdentifier(resourcePath)) {
if (isCustomMethodIdentifier(resourcePath) || hasCustomMethodOverride(input)) {
return;
}

Expand Down
72 changes: 48 additions & 24 deletions tools/spectral/ipa/rulesets/functions/IPA109ValidOperationID.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
45 changes: 45 additions & 0 deletions tools/spectral/ipa/rulesets/functions/utils/extensions.js
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading