diff --git a/tools/spectral/ipa/__tests__/IPA109CustomMethodIdentifierFormat.test.js b/tools/spectral/ipa/__tests__/IPA109CustomMethodIdentifierFormat.test.js new file mode 100644 index 0000000000..1e90d3111b --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA109CustomMethodIdentifierFormat.test.js @@ -0,0 +1,103 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +testRule('xgen-IPA-109-custom-method-identifier-format', [ + { + name: 'valid custom method formats', + document: { + paths: { + '/resource:customMethod': {}, + '/resource/{resourceId}:customMethod': {}, + '/resource/{resourceId}/subresource:customMethod': {}, + '/resource/{resourceId}/subresource/{resourceId}:customMethod': {}, + '/resource/subresource/{resourceId}:customMethod': {}, + '/resource/{resourceId}/subresource/{resourceId}/nested:customMethod': {}, + '/resource/{resourceId}/subresource/{resourceId}/nested/{resourceId}:customMethod': {}, + '/resource/subresource/nested/{resourceId}:customMethod': {}, + }, + }, + errors: [], + }, + { + name: 'invalid custom method formats not validated by this rule', + document: { + paths: { + '/resources:custom&method': {}, + '/resources:custom/method': {}, + }, + }, + errors: [], + }, + { + name: 'invalid custom method with exception', + document: { + paths: { + '/resources/:customMethod': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-109-custom-method-identifier-format': 'exception reason', + }, + }, + '/resources:{resourceId}:customMethod': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-109-custom-method-identifier-format': 'exception reason', + }, + }, + }, + }, + errors: [], + }, + { + name: 'invalid custom method formats', + document: { + paths: { + '/resources/:customMethod': {}, + '/resources:{resourceId}:customMethod': {}, + '/resources/:{resourceId}:customMethod': {}, + '/:customMethod': {}, + '/resources/{resourceId}/:customMethod': {}, + '/resources/{resourceId}:custom::Method': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-109-custom-method-identifier-format', + message: + "The path /resources/:customMethod contains a '/' before the custom method name. Custom method paths should not have a '/' before the ':'.", + path: ['paths', '/resources/:customMethod'], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-109-custom-method-identifier-format', + message: 'Multiple colons found in "/resources:{resourceId}:customMethod"', + path: ['paths', '/resources:{resourceId}:customMethod'], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-109-custom-method-identifier-format', + message: 'Multiple colons found in "/resources/:{resourceId}:customMethod".', + path: ['paths', '/resources/:{resourceId}:customMethod'], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-109-custom-method-identifier-format', + message: + "The path /:customMethod contains a '/' before the custom method name. Custom method paths should not have a '/' before the ':'.", + path: ['paths', '/:customMethod'], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-109-custom-method-identifier-format', + message: + "The path /resources/{resourceId}/:customMethod contains a '/' before the custom method name. Custom method paths should not have a '/' before the ':'.", + path: ['paths', '/resources/{resourceId}/:customMethod'], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-109-custom-method-identifier-format', + message: 'Multiple colons found in "/resources/{resourceId}:custom::Method".', + path: ['paths', '/resources/{resourceId}:custom::Method'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-109.yaml b/tools/spectral/ipa/rulesets/IPA-109.yaml index ad7668ebcc..7433a0da69 100644 --- a/tools/spectral/ipa/rulesets/IPA-109.yaml +++ b/tools/spectral/ipa/rulesets/IPA-109.yaml @@ -4,6 +4,7 @@ functions: - IPA109EachCustomMethodMustBeGetOrPost - IPA109EachCustomMethodMustUseCamelCase + - IPA109CustomMethodIdentifierFormat rules: xgen-IPA-109-custom-method-must-be-GET-or-POST: @@ -38,3 +39,20 @@ rules: given: '$.paths[*]' then: function: 'IPA109EachCustomMethodMustUseCamelCase' + xgen-IPA-109-custom-method-identifier-format: + description: | + Custom methods must be defined using a colon followed by the method name. + + ##### Implementation details + Rule checks for the following conditions: + - Identifies paths containing a colon (potential custom methods) + - Validates that the path follows proper custom method format + - Does not validate after the colon (xgen-IPA-109-custom-method-must-use-camel-case rule validates the method name) + - Fails if a slash appears before a colon + - Fails if multiple colons appear in the path + - Fails if other than an alphabetical character or a closing curly brace appears before a colon + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-109-custom-method-identifier-format' + severity: error + given: '$.paths[*]' + then: + function: 'IPA109CustomMethodIdentifierFormat' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index d5546e0ce5..dbd413b208 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -396,6 +396,20 @@ Rule checks for the following conditions: - Validates that the method name uses proper camelCase formatting - Fails if the method name contains invalid casing (such as snake_case, PascalCase, etc.) +#### xgen-IPA-109-custom-method-identifier-format + + ![error](https://img.shields.io/badge/error-red) +Custom methods must be defined using a colon followed by the method name. + +##### Implementation details +Rule checks for the following conditions: + - Identifies paths containing a colon (potential custom methods) + - Validates that the path follows proper custom method format + - Does not validate after the colon (xgen-IPA-109-custom-method-must-use-camel-case rule validates the method name) + - Fails if a slash appears before a colon + - Fails if multiple colons appear in the path + - Fails if other than an alphabetical character or a closing curly brace appears before a colon + ### IPA-113 diff --git a/tools/spectral/ipa/rulesets/functions/IPA109CustomMethodIdentifierFormat.js b/tools/spectral/ipa/rulesets/functions/IPA109CustomMethodIdentifierFormat.js new file mode 100644 index 0000000000..f21318161d --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA109CustomMethodIdentifierFormat.js @@ -0,0 +1,76 @@ +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; +import { isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; +import { hasException } from './utils/exceptions.js'; + +const RULE_NAME = 'xgen-IPA-109-custom-method-identifier-format'; + +/** + * Validates custom method identifiers follow the correct format pattern. + * Custom methods should be defined using a colon character followed by the method name. + * Valid formats: /resources/{resourceId}:customMethod or /resources:customMethod + * + * @param {object} input - The path string being evaluated + * @param {object} _ - Unused + * @param {object} context - The context object containing path and document information + */ +export default (input, _, { path }) => { + let pathKey = path[1]; + + if (!isCustomMethodIdentifier(pathKey)) { + return; + } + + if (hasException(input, RULE_NAME)) { + collectException(input, RULE_NAME, path); + return; + } + + const errors = checkViolationsAndReturnErrors(pathKey, path); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + collectAdoption(path, RULE_NAME); +}; + +function checkViolationsAndReturnErrors(pathKey, path) { + try { + // Check for multiple colons + const colonCount = (pathKey.match(/:/g) || []).length; + if (colonCount > 1) { + return [{ path, message: `Multiple colons found in "${pathKey}".` }]; + } + + // Check for slash before colon + const invalidSlashBeforeColonPattern = /\/:/; + + if (invalidSlashBeforeColonPattern.test(pathKey)) { + return [ + { + path, + message: `The path ${pathKey} contains a '/' before the custom method name. Custom method paths should not have a '/' before the ':'.`, + }, + ]; + } + + // Check for invalid character before colon + // The character before colon should be either an alphabetical character or a closing curly brace '}' + const beforeColonMatch = pathKey.match(/([^a-zA-Z}]):/); + if (beforeColonMatch && beforeColonMatch[1] !== '') { + return [ + { + path, + message: `Invalid character '${beforeColonMatch[1]}' before colon in "${pathKey}".`, + }, + ]; + } + + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +}