diff --git a/package-lock.json b/package-lock.json index 2af7a44a36..e47eeb40e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "dependencies": { "@stoplight/spectral-cli": "^6.14.2", "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-functions": "^1.9.3", "@stoplight/spectral-ref-resolver": "^1.0.5", "@stoplight/spectral-ruleset-bundler": "^1.6.1", "eslint-plugin-jest": "^28.9.0", diff --git a/package.json b/package.json index 8744a4a3ff..ebe70edb00 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "@stoplight/spectral-cli": "^6.14.2", "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-functions": "^1.9.3", "@stoplight/spectral-ref-resolver": "^1.0.5", "@stoplight/spectral-ruleset-bundler": "^1.6.1", "eslint-plugin-jest": "^28.9.0", diff --git a/tools/spectral/ipa/__tests__/eachCustomMethodMustBeGetOrPost.test.js b/tools/spectral/ipa/__tests__/eachCustomMethodMustBeGetOrPost.test.js index 369f61efcd..ec919c3e94 100644 --- a/tools/spectral/ipa/__tests__/eachCustomMethodMustBeGetOrPost.test.js +++ b/tools/spectral/ipa/__tests__/eachCustomMethodMustBeGetOrPost.test.js @@ -30,6 +30,28 @@ testRule('xgen-IPA-109-custom-method-must-be-GET-or-POST', [ }, errors: [], }, + { + name: 'invalid methods with exception', + document: { + paths: { + '/d/{exampleId}:method': { + get: {}, + post: {}, + 'x-xgen-IPA-exception': { + 'xgen-IPA-109-custom-method-must-be-GET-or-POST': {}, + }, + }, + '/d:method': { + get: {}, + post: {}, + 'x-xgen-IPA-exception': { + 'xgen-IPA-109-custom-method-must-be-GET-or-POST': {}, + }, + }, + }, + }, + errors: [], + }, { name: 'invalid methods', document: { diff --git a/tools/spectral/ipa/__tests__/eachCustomMethodMustUseCamelCase.test.js b/tools/spectral/ipa/__tests__/eachCustomMethodMustUseCamelCase.test.js new file mode 100644 index 0000000000..205a35d58e --- /dev/null +++ b/tools/spectral/ipa/__tests__/eachCustomMethodMustUseCamelCase.test.js @@ -0,0 +1,84 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +testRule('xgen-IPA-109-custom-method-must-use-camel-case', [ + { + name: 'valid methods', + document: { + paths: { + '/a/{exampleId}:methodName': {}, + '/a:methodName': {}, + }, + }, + errors: [], + }, + { + name: 'invalid methods with exception', + document: { + paths: { + '/b/{exampleId}:MethodName': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-109-custom-method-must-use-camel-case': {}, + }, + }, + '/b:MethodName': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-109-custom-method-must-use-camel-case': {}, + }, + }, + }, + }, + errors: [], + }, + { + name: 'invalid methods', + document: { + paths: { + '/a/{exampleId}:MethodName': {}, + '/a:MethodName': {}, + '/a/{exampleId}:method_name': {}, + '/a:method_name': {}, + '/a/{exampleId}:': {}, + '/a:': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-109-custom-method-must-use-camel-case', + message: 'MethodName must use camelCase format. http://go/ipa/109', + path: ['paths', '/a/{exampleId}:MethodName'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-109-custom-method-must-use-camel-case', + message: 'MethodName must use camelCase format. http://go/ipa/109', + path: ['paths', '/a:MethodName'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-109-custom-method-must-use-camel-case', + message: 'method_name must use camelCase format. http://go/ipa/109', + path: ['paths', '/a/{exampleId}:method_name'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-109-custom-method-must-use-camel-case', + message: 'method_name must use camelCase format. http://go/ipa/109', + path: ['paths', '/a:method_name'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-109-custom-method-must-use-camel-case', + message: 'Custom method name cannot be empty or blank. http://go/ipa/109', + path: ['paths', '/a/{exampleId}:'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-109-custom-method-must-use-camel-case', + message: 'Custom method name cannot be empty or blank. http://go/ipa/109', + path: ['paths', '/a:'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-109.yaml b/tools/spectral/ipa/rulesets/IPA-109.yaml index 48c1645e07..5d3561f386 100644 --- a/tools/spectral/ipa/rulesets/IPA-109.yaml +++ b/tools/spectral/ipa/rulesets/IPA-109.yaml @@ -3,6 +3,7 @@ functions: - eachCustomMethodMustBeGetOrPost + - eachCustomMethodMustUseCamelCase rules: xgen-IPA-109-custom-method-must-be-GET-or-POST: @@ -12,3 +13,11 @@ rules: given: '$.paths[*]' then: function: 'eachCustomMethodMustBeGetOrPost' + + xgen-IPA-109-custom-method-must-use-camel-case: + description: 'The custom method must use camelCase format. http://go/ipa/109' + message: '{{error}} http://go/ipa/109' + severity: warn + given: '$.paths[*]' + then: + function: 'eachCustomMethodMustUseCamelCase' diff --git a/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustBeGetOrPost.js b/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustBeGetOrPost.js index bb7be025da..fa14929c6f 100644 --- a/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustBeGetOrPost.js +++ b/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustBeGetOrPost.js @@ -1,5 +1,7 @@ import { isCustomMethod } from './utils/resourceEvaluation.js'; +import { hasException } from './utils/exceptions.js'; +const RULE_NAME = 'xgen-IPA-109-custom-method-must-be-GET-or-POST'; const ERROR_MESSAGE = 'The HTTP method for custom methods must be GET or POST.'; const ERROR_RESULT = [{ message: ERROR_MESSAGE }]; const VALID_METHODS = ['get', 'post']; @@ -11,6 +13,10 @@ export default (input, opts, { path }) => { if (!isCustomMethod(pathKey)) return; + if (hasException(input, RULE_NAME)) { + return; + } + //Extract the keys which are equivalent of the http methods let keys = Object.keys(input); const httpMethods = keys.filter((key) => HTTP_METHODS.includes(key)); diff --git a/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustUseCamelCase.js b/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustUseCamelCase.js new file mode 100644 index 0000000000..d027d78f67 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustUseCamelCase.js @@ -0,0 +1,25 @@ +import { getCustomMethodName, isCustomMethod } from './utils/resourceEvaluation.js'; +import { hasException } from './utils/exceptions.js'; +import { casing } from '@stoplight/spectral-functions'; + +const RULE_NAME = 'xgen-IPA-109-custom-method-must-use-camel-case'; + +export default (input, opts, { path }) => { + // Extract the path key (e.g., '/a/{exampleId}:method') from the JSONPath. + let pathKey = path[1]; + + if (!isCustomMethod(pathKey)) return; + + if (hasException(input, RULE_NAME)) { + return; + } + + let methodName = getCustomMethodName(pathKey); + if (methodName.length === 0 || methodName.trim().length === 0) { + return [{ message: 'Custom method name cannot be empty or blank.' }]; + } + + if (casing(methodName, { type: 'camel', disallowDigits: true })) { + return [{ message: `${methodName} must use camelCase format.` }]; + } +}; diff --git a/tools/spectral/ipa/rulesets/functions/utils/resourceEvaluation.js b/tools/spectral/ipa/rulesets/functions/utils/resourceEvaluation.js index 0c7d61e0e1..fe65ad4f97 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/resourceEvaluation.js +++ b/tools/spectral/ipa/rulesets/functions/utils/resourceEvaluation.js @@ -6,6 +6,10 @@ export function isCustomMethod(path) { return path.includes(':'); } +export function getCustomMethodName(path) { + return path.split(':')[1]; +} + /** * Checks if a resource is a singleton resource ({@link https://docs.devprod.prod.corp.mongodb.com/ipa/113 IPA-113}) based on the paths for the * resource. The resource may have custom methods. Use {@link getResourcePaths} to get all paths of a resource.