From 60308e4daadd1648a8034ad851aef39ca35eff40 Mon Sep 17 00:00:00 2001 From: wtrocki Date: Mon, 10 Mar 2025 17:13:00 +0100 Subject: [PATCH 1/7] CLOUDP-304929: camel case --- .../collectionIdentifierCamelCase.test.js | 135 ++++++++++++++++++ tools/spectral/ipa/rulesets/IPA-102.yaml | 12 ++ tools/spectral/ipa/rulesets/README.md | 1 + .../collectionIdentifierCamelCase.js | 85 +++++++++++ .../functions/utils/componentUtils.js | 21 +++ 5 files changed, 254 insertions(+) create mode 100644 tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js create mode 100644 tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js diff --git a/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js b/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js new file mode 100644 index 0000000000..28a39a206d --- /dev/null +++ b/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js @@ -0,0 +1,135 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +testRule('xgen-IPA-102-collection-identifier-camelCase', [ + { + name: 'valid camelCase identifiers', + document: { + paths: { + '/resources': {}, + '/users': {}, + '/resourceGroups': {}, + '/userProfiles': {}, + }, + }, + errors: [], + }, + { + name: 'valid camelCase with path parameters', + document: { + paths: { + '/resourceGroups/{groupId}': {}, + '/users/{userId}/userProfiles': {}, + }, + }, + errors: [], + }, + { + name: 'valid camelCase custom methods', + document: { + paths: { + '/resources:createResource': {}, + '/users/{userId}/data/:activateUser': {}, + }, + }, + errors: [], + }, + { + name: 'invalid PascalCase instead of camelCase', + document: { + paths: { + '/Resources': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path segment 'Resources' in path '/Resources' is not in camelCase. http://go/ipa/102", + path: ['paths', '/Resources'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid with snake_case instead of camelCase', + document: { + paths: { + '/resource_groups': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path segment 'resource_groups' in path '/resource_groups' is not in camelCase. http://go/ipa/102", + path: ['paths', '/resource_groups'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid with kebab-case instead of camelCase', + document: { + paths: { + '/resource-groups': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path segment 'resource-groups' in path '/resource-groups' is not in camelCase. http://go/ipa/102", + path: ['paths', '/resource-groups'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid custom method not in camelCase', + document: { + paths: { + '/resources:CREATE_RESOURCE': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Custom method 'CREATE_RESOURCE' in path '/resources:CREATE_RESOURCE' is not in camelCase. http://go/ipa/102", + path: ['paths', '/resources:CREATE_RESOURCE'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid with consecutive uppercase letters', + document: { + paths: { + '/resourcesAPI': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path segment 'resourcesAPI' in path '/resourcesAPI' is not in camelCase. http://go/ipa/102", + path: ['paths', '/resourcesAPI'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'valid with path-level exception', + document: { + paths: { + '/resource_groups': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-collection-identifier-camelCase': 'Legacy API path that cannot be changed', + }, + }, + }, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-102.yaml b/tools/spectral/ipa/rulesets/IPA-102.yaml index 766f67c1c4..4be2b1f1a4 100644 --- a/tools/spectral/ipa/rulesets/IPA-102.yaml +++ b/tools/spectral/ipa/rulesets/IPA-102.yaml @@ -2,6 +2,15 @@ # http://go/ipa/102 rules: + xgen-IPA-102-collection-identifier-camelCase: + description: Collection identifiers must be in camelCase. http://go/ipa/102 + message: '{{error}} http://go/ipa/102' + severity: warn + given: $.paths + then: + field: '@key' + function: collectionIdentifierCamelCase + xgen-IPA-102-path-alternate-resource-name-path-param: description: 'Paths should alternate between resource names and path params. http://go/ipa/102' message: '{{error}} http://go/ipa/102' @@ -23,3 +32,6 @@ rules: functions: - collectionIdentifierPattern - eachPathAlternatesBetweenResourceNameAndPathParam + - collectionIdentifierCamelCase + + diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index ab7314dc90..47cc3857f3 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -23,6 +23,7 @@ For rule definitions, see [IPA-102.yaml](https://github.com/mongodb/openapi/blob | Rule Name | Description | Severity | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | | xgen-IPA-102-path-alternate-resource-name-path-param | Paths should alternate between resource names and path params. http://go/ipa/102 | error | +| xgen-IPA-102-collection-identifier-camelCase | Collection identifiers must be in camelCase. http://go/ipa/102 | warn | | xgen-IPA-102-collection-identifier-pattern | Collection identifiers must begin with a lowercase letter and contain only ASCII letters and numbers. http://go/ipa/102 | warn | ### IPA-104 diff --git a/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js b/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js new file mode 100644 index 0000000000..0eb692ed2e --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js @@ -0,0 +1,85 @@ +import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { hasException } from './utils/exceptions.js'; +import { isCamelCase } from './utils/componentUtils.js'; + +const RULE_NAME = 'xgen-IPA-102-collection-identifier-camelCase'; +const ERROR_MESSAGE = 'Collection identifiers must be in camelCase.'; + +/** + * Checks if collection identifiers in paths follow camelCase convention + * + * @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; + const pathKey = input; + + // Check for exception at the path level + if (hasException(oas.paths[input], RULE_NAME)) { + collectException(oas.paths[input], RULE_NAME, path); + return; + } + + const violations = checkViolations(pathKey, path); + if (violations.length > 0) { + return collectAndReturnViolation(path, RULE_NAME, violations); + } + + return collectAdoption(path, RULE_NAME); +}; + +function checkViolations(pathKey, path) { + const violations = []; + const pathSegments = pathKey.split('/').filter((segment) => segment.length > 0); + + pathSegments.forEach((segment) => { + // Skip empty segments + if (!segment) return; + + // Handle path parameters - extract parameter name from {paramName} + if (segment.startsWith('{') && segment.endsWith('}')) { + // Extract parameter name without brackets + const paramName = segment.slice(1, -1); + // Check if it's a valid camelCase parameter name + if (!isCamelCase(paramName)) { + violations.push({ + message: `${ERROR_MESSAGE} Path parameter '${paramName}' in path '${pathKey}' is not in camelCase.`, + path: [...path, pathKey], + }); + } + return; + } + + // Check for custom methods + const parts = segment.split(':'); + const identifier = parts[0]; + + // Skip empty identifiers + if (identifier.length === 0) { + return; + } + + // Check if it's in camelCase + if (!isCamelCase(identifier)) { + violations.push({ + message: `${ERROR_MESSAGE} Path segment '${identifier}' in path '${pathKey}' is not in camelCase.`, + path: [...path, pathKey], + }); + } + + // If there's a custom method, check that too + if (parts.length > 1 && parts[1].length > 0) { + const methodName = parts[1]; + if (!isCamelCase(methodName)) { + violations.push({ + message: `${ERROR_MESSAGE} Custom method '${methodName}' in path '${pathKey}' is not in camelCase.`, + path: [...path, pathKey], + }); + } + } + }); + + return violations; +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js b/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js index 494537218a..59718ce70f 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js @@ -1,3 +1,24 @@ +/** + * Checks if a string is in camelCase format + * @param {string} str - The string to check + * @returns {boolean} - True if the string is in camelCase, false otherwise + */ +export function isCamelCase(str) { + // Must start with lowercase letter + if (!/^[a-z]/.test(str)) { + return false; + } + // Should not contain underscores or hyphens + if (/[-_]/.test(str)) { + return false; + } + // Should not have consecutive uppercase letters (not typical in camelCase) + if (/[A-Z]{2,}/.test(str)) { + return false; + } + return true; +} + /** * Checks if a string belongs to a path parameter or a path parameter with a custom method. * From a9152e2aaa1833a807acbf59173c0c81215ba697 Mon Sep 17 00:00:00 2001 From: wtrocki Date: Tue, 11 Mar 2025 12:51:42 +0100 Subject: [PATCH 2/7] fix: reuse method --- .../collectionIdentifierCamelCase.test.js | 14 +++---- tools/spectral/ipa/rulesets/IPA-102.yaml | 2 - .../collectionIdentifierCamelCase.js | 38 +++++++++---------- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js b/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js index 28a39a206d..a145aa3e37 100644 --- a/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js +++ b/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js @@ -25,11 +25,11 @@ testRule('xgen-IPA-102-collection-identifier-camelCase', [ errors: [], }, { - name: 'valid camelCase custom methods', + name: 'valid paths with custom methods (only checking identifier part)', document: { paths: { - '/resources:createResource': {}, - '/users/{userId}/data/:activateUser': {}, + '/resources:any_Custom_Method': {}, + '/users/{userId}/data/:AnyOtherMethod': {}, }, }, errors: [], @@ -86,18 +86,18 @@ testRule('xgen-IPA-102-collection-identifier-camelCase', [ ], }, { - name: 'invalid custom method not in camelCase', + name: 'invalid resource path with invalid casing but valid custom method', document: { paths: { - '/resources:CREATE_RESOURCE': {}, + '/Resources:createResource': {}, }, }, errors: [ { code: 'xgen-IPA-102-collection-identifier-camelCase', message: - "Collection identifiers must be in camelCase. Custom method 'CREATE_RESOURCE' in path '/resources:CREATE_RESOURCE' is not in camelCase. http://go/ipa/102", - path: ['paths', '/resources:CREATE_RESOURCE'], + "Collection identifiers must be in camelCase. Path segment 'Resources' in path '/Resources:createResource' is not in camelCase. http://go/ipa/102", + path: ['paths', '/Resources:createResource'], severity: DiagnosticSeverity.Warning, }, ], diff --git a/tools/spectral/ipa/rulesets/IPA-102.yaml b/tools/spectral/ipa/rulesets/IPA-102.yaml index 4be2b1f1a4..048c92f6ea 100644 --- a/tools/spectral/ipa/rulesets/IPA-102.yaml +++ b/tools/spectral/ipa/rulesets/IPA-102.yaml @@ -33,5 +33,3 @@ functions: - collectionIdentifierPattern - eachPathAlternatesBetweenResourceNameAndPathParam - collectionIdentifierCamelCase - - diff --git a/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js b/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js index 0eb692ed2e..dbbd6a404a 100644 --- a/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js +++ b/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js @@ -1,6 +1,7 @@ import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; import { hasException } from './utils/exceptions.js'; -import { isCamelCase } from './utils/componentUtils.js'; +import { isPathParam } from './utils/componentUtils.js'; +import { casing } from '@stoplight/spectral-functions'; const RULE_NAME = 'xgen-IPA-102-collection-identifier-camelCase'; const ERROR_MESSAGE = 'Collection identifiers must be in camelCase.'; @@ -38,12 +39,12 @@ function checkViolations(pathKey, path) { // Skip empty segments if (!segment) return; - // Handle path parameters - extract parameter name from {paramName} - if (segment.startsWith('{') && segment.endsWith('}')) { + // Skip path parameter validation if it matches the expected format + if (isPathParam(segment)) { // Extract parameter name without brackets - const paramName = segment.slice(1, -1); + const paramName = segment.slice(1, segment.indexOf('}')); // Check if it's a valid camelCase parameter name - if (!isCamelCase(paramName)) { + if (casing(paramName, { type: 'camel', disallowDigits: true })) { violations.push({ message: `${ERROR_MESSAGE} Path parameter '${paramName}' in path '${pathKey}' is not in camelCase.`, path: [...path, pathKey], @@ -52,33 +53,28 @@ function checkViolations(pathKey, path) { return; } - // Check for custom methods - const parts = segment.split(':'); - const identifier = parts[0]; + // For regular path segments, check if they contain custom method indicators + // If they do, only validate the identifier part (before the colon) + const colonIndex = segment.indexOf(':'); + let identifier = segment; + + if (colonIndex !== -1) { + // Only check the identifier part before the colon + identifier = segment.substring(0, colonIndex); + } // Skip empty identifiers if (identifier.length === 0) { return; } - // Check if it's in camelCase - if (!isCamelCase(identifier)) { + // Check if it's in camelCase using the casing function + if (casing(identifier, { type: 'camel', disallowDigits: true })) { violations.push({ message: `${ERROR_MESSAGE} Path segment '${identifier}' in path '${pathKey}' is not in camelCase.`, path: [...path, pathKey], }); } - - // If there's a custom method, check that too - if (parts.length > 1 && parts[1].length > 0) { - const methodName = parts[1]; - if (!isCamelCase(methodName)) { - violations.push({ - message: `${ERROR_MESSAGE} Custom method '${methodName}' in path '${pathKey}' is not in camelCase.`, - path: [...path, pathKey], - }); - } - } }); return violations; From d4ce211d5f336dd11f42f4b8ce234a0937ae4731 Mon Sep 17 00:00:00 2001 From: wtrocki Date: Tue, 11 Mar 2025 13:54:09 +0100 Subject: [PATCH 3/7] fix: remove function --- .../functions/utils/componentUtils.js | 21 ------------------- .../rulesets/functions/utils/schemaUtils.js | 12 +++++++++++ 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js b/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js index 59718ce70f..494537218a 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js @@ -1,24 +1,3 @@ -/** - * Checks if a string is in camelCase format - * @param {string} str - The string to check - * @returns {boolean} - True if the string is in camelCase, false otherwise - */ -export function isCamelCase(str) { - // Must start with lowercase letter - if (!/^[a-z]/.test(str)) { - return false; - } - // Should not contain underscores or hyphens - if (/[-_]/.test(str)) { - return false; - } - // Should not have consecutive uppercase letters (not typical in camelCase) - if (/[A-Z]{2,}/.test(str)) { - return false; - } - return true; -} - /** * Checks if a string belongs to a path parameter or a path parameter with a custom method. * diff --git a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js index ae10e80e63..14cb62e09a 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js @@ -1,8 +1,20 @@ +/** + * Checks if the object has results property + * @param {*} schema + * @returns true if schema object returns results property (pagination), false otherwise + */ export function schemaIsPaginated(schema) { const fields = Object.keys(schema); return fields.includes('properties') && Object.keys(schema['properties']).includes('results'); } + +/** + * Checks if schema is an array type of schema + * + * @param {*} schema + * @returns + */ export function schemaIsArray(schema) { const fields = Object.keys(schema); return fields.includes('type') && schema['type'] === 'array'; From 7751d68767d105f31ed2ab1363a5d701e89eaa8c Mon Sep 17 00:00:00 2001 From: wtrocki Date: Tue, 11 Mar 2025 14:47:22 +0100 Subject: [PATCH 4/7] fix: review comments --- .../collectionIdentifierCamelCase.test.js | 3 +- tools/spectral/ipa/rulesets/IPA-102.yaml | 3 + .../collectionIdentifierCamelCase.js | 101 +++++++++++------- .../rulesets/functions/utils/schemaUtils.js | 11 +- 4 files changed, 70 insertions(+), 48 deletions(-) diff --git a/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js b/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js index a145aa3e37..4a0efeb6e7 100644 --- a/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js +++ b/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js @@ -6,10 +6,11 @@ testRule('xgen-IPA-102-collection-identifier-camelCase', [ name: 'valid camelCase identifiers', document: { paths: { - '/resources': {}, + '/api/v2/atlas/test': {}, '/users': {}, '/resourceGroups': {}, '/userProfiles': {}, + '/api/v1/test': {}, }, }, errors: [], diff --git a/tools/spectral/ipa/rulesets/IPA-102.yaml b/tools/spectral/ipa/rulesets/IPA-102.yaml index 048c92f6ea..7086ea7cd4 100644 --- a/tools/spectral/ipa/rulesets/IPA-102.yaml +++ b/tools/spectral/ipa/rulesets/IPA-102.yaml @@ -10,6 +10,9 @@ rules: then: field: '@key' function: collectionIdentifierCamelCase + functionOptions: + # Contains list of ignored path params + ignoredValues: ['v2', 'v1'] xgen-IPA-102-path-alternate-resource-name-path-param: description: 'Paths should alternate between resource names and path params. http://go/ipa/102' diff --git a/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js b/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js index dbbd6a404a..96ce1f23a2 100644 --- a/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js +++ b/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js @@ -1,4 +1,9 @@ -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; import { hasException } from './utils/exceptions.js'; import { isPathParam } from './utils/componentUtils.js'; import { casing } from '@stoplight/spectral-functions'; @@ -10,10 +15,10 @@ const ERROR_MESSAGE = 'Collection identifiers must be in camelCase.'; * Checks if collection identifiers in paths follow camelCase convention * * @param {object} input - The path key from the OpenAPI spec - * @param {object} _ - Unused + * @param {object} options - Rule configuration options * @param {object} context - The context object containing the path and documentInventory */ -export default (input, _, { path, documentInventory }) => { +export default (input, options, { path, documentInventory }) => { const oas = documentInventory.resolved; const pathKey = input; @@ -23,7 +28,10 @@ export default (input, _, { path, documentInventory }) => { return; } - const violations = checkViolations(pathKey, path); + // Extract ignored values from options + const ignoredValues = options?.ignoredValues || []; + + const violations = checkViolations(pathKey, path, ignoredValues); if (violations.length > 0) { return collectAndReturnViolation(path, RULE_NAME, violations); } @@ -31,51 +39,62 @@ export default (input, _, { path, documentInventory }) => { return collectAdoption(path, RULE_NAME); }; -function checkViolations(pathKey, path) { +function checkViolations(pathKey, path, ignoredValues = []) { const violations = []; - const pathSegments = pathKey.split('/').filter((segment) => segment.length > 0); + try { + const pathSegments = pathKey.split('/'); - pathSegments.forEach((segment) => { - // Skip empty segments - if (!segment) return; + pathSegments.forEach((segment) => { + // Skip path parameter validation if it matches the expected format + if (isPathParam(segment)) { + // Extract parameter name without brackets + const paramName = segment.slice(1, segment.indexOf('}')); + // Check if it's a valid camelCase parameter name + if (casing(paramName, { type: 'camel', disallowDigits: true })) { + violations.push({ + message: `${ERROR_MESSAGE} Path parameter '${paramName}' in path '${pathKey}' is not in camelCase.`, + path: [...path, pathKey], + }); + } + return; + } - // Skip path parameter validation if it matches the expected format - if (isPathParam(segment)) { - // Extract parameter name without brackets - const paramName = segment.slice(1, segment.indexOf('}')); - // Check if it's a valid camelCase parameter name - if (casing(paramName, { type: 'camel', disallowDigits: true })) { - violations.push({ - message: `${ERROR_MESSAGE} Path parameter '${paramName}' in path '${pathKey}' is not in camelCase.`, - path: [...path, pathKey], - }); + // Skip validation for ignored values + if (ignoredValues.includes(segment)) { + return; } - return; - } - // For regular path segments, check if they contain custom method indicators - // If they do, only validate the identifier part (before the colon) - const colonIndex = segment.indexOf(':'); - let identifier = segment; + // For regular path segments, check if they contain custom method indicators + // If they do, only validate the identifier part (before the colon) + const colonIndex = segment.indexOf(':'); + let identifier = segment; + + if (colonIndex !== -1) { + // Only check the identifier part before the colon + identifier = segment.substring(0, colonIndex); + } - if (colonIndex !== -1) { - // Only check the identifier part before the colon - identifier = segment.substring(0, colonIndex); - } + // Skip empty identifiers + if (identifier.length === 0) { + return; + } - // Skip empty identifiers - if (identifier.length === 0) { - return; - } + // Skip validation for ignored values at the identifier level too + if (ignoredValues.includes(identifier)) { + return; + } - // Check if it's in camelCase using the casing function - if (casing(identifier, { type: 'camel', disallowDigits: true })) { - violations.push({ - message: `${ERROR_MESSAGE} Path segment '${identifier}' in path '${pathKey}' is not in camelCase.`, - path: [...path, pathKey], - }); - } - }); + // Check if it's in camelCase using the casing function + if (casing(identifier, { type: 'camel', disallowDigits: true })) { + violations.push({ + message: `${ERROR_MESSAGE} Path segment '${identifier}' in path '${pathKey}' is not in camelCase.`, + path: [...path, pathKey], + }); + } + }); + } catch (e) { + handleInternalError(RULE_NAME, [...path, pathKey], e); + } return violations; } diff --git a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js index 14cb62e09a..af2529e91f 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js @@ -1,6 +1,6 @@ /** - * Checks if the object has results property - * @param {*} schema + * Checks if the object has results property + * @param {*} schema * @returns true if schema object returns results property (pagination), false otherwise */ export function schemaIsPaginated(schema) { @@ -8,12 +8,11 @@ export function schemaIsPaginated(schema) { return fields.includes('properties') && Object.keys(schema['properties']).includes('results'); } - /** * Checks if schema is an array type of schema - * - * @param {*} schema - * @returns + * + * @param {*} schema + * @returns */ export function schemaIsArray(schema) { const fields = Object.keys(schema); From f36dce5a3cde4e140313fee40b859f3ee3928218 Mon Sep 17 00:00:00 2001 From: wtrocki Date: Tue, 11 Mar 2025 17:07:54 +0100 Subject: [PATCH 5/7] fix: example description --- tools/spectral/ipa/rulesets/IPA-102.yaml | 10 +++++++++- tools/spectral/ipa/rulesets/README.md | 10 +++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/tools/spectral/ipa/rulesets/IPA-102.yaml b/tools/spectral/ipa/rulesets/IPA-102.yaml index 7086ea7cd4..ca213c7d1d 100644 --- a/tools/spectral/ipa/rulesets/IPA-102.yaml +++ b/tools/spectral/ipa/rulesets/IPA-102.yaml @@ -3,7 +3,15 @@ rules: xgen-IPA-102-collection-identifier-camelCase: - description: Collection identifiers must be in camelCase. http://go/ipa/102 + description: >- + Collection identifiers must be in camelCase. Logic includes:
+ - All path segments that are not path parameters
+ - Only the resource identifier part before any colon in custom method paths (e.g., 'resource' in '/resource:customMethod')
+ - Path parameters should also follow camelCase naming
+ - Certain values can be exempted via the ignoredValues configuration (e.g., 'v1', 'v2') that can be supplied as `ignoredValues` + argument to the rule
+ - Paths with x-xgen-IPA-exception for this rule are excluded from validation
+ http://go/ipa/102 message: '{{error}} http://go/ipa/102' severity: warn given: $.paths diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 47cc3857f3..a56a05d999 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -20,11 +20,11 @@ For rule definitions, see [IPA-005.yaml](https://github.com/mongodb/openapi/blob For rule definitions, see [IPA-102.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-102.yaml). -| Rule Name | Description | Severity | -| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | -| xgen-IPA-102-path-alternate-resource-name-path-param | Paths should alternate between resource names and path params. http://go/ipa/102 | error | -| xgen-IPA-102-collection-identifier-camelCase | Collection identifiers must be in camelCase. http://go/ipa/102 | warn | -| xgen-IPA-102-collection-identifier-pattern | Collection identifiers must begin with a lowercase letter and contain only ASCII letters and numbers. http://go/ipa/102 | warn | +| Rule Name | Description | Severity | +| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| xgen-IPA-102-path-alternate-resource-name-path-param | Paths should alternate between resource names and path params. http://go/ipa/102 | error | +| xgen-IPA-102-collection-identifier-camelCase | Collection identifiers must be in camelCase. Logic includes:
- All path segments that are not path parameters
- Only the resource identifier part before any colon in custom method paths (e.g., 'resource' in '/resource:customMethod')
- Path parameters should also follow camelCase naming
- Certain values can be exempted via the ignoredValues configuration (e.g., 'v1', 'v2') that can be supplied as `ignoredValues` argument to the rule
- Paths with x-xgen-IPA-exception for this rule are excluded from validation
http://go/ipa/102 | warn | +| xgen-IPA-102-collection-identifier-pattern | Collection identifiers must begin with a lowercase letter and contain only ASCII letters and numbers. http://go/ipa/102 | warn | ### IPA-104 From c4f3b268e51866ebd577378e80088797cdb3705d Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Tue, 11 Mar 2025 17:28:38 +0100 Subject: [PATCH 6/7] Apply suggestions from Lovisa code review Co-authored-by: Lovisa Berggren <59226031+lovisaberggren@users.noreply.github.com> --- tools/spectral/ipa/rulesets/IPA-102.yaml | 4 ++-- tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/spectral/ipa/rulesets/IPA-102.yaml b/tools/spectral/ipa/rulesets/IPA-102.yaml index ca213c7d1d..7bbbd8ce16 100644 --- a/tools/spectral/ipa/rulesets/IPA-102.yaml +++ b/tools/spectral/ipa/rulesets/IPA-102.yaml @@ -6,11 +6,11 @@ rules: description: >- Collection identifiers must be in camelCase. Logic includes:
- All path segments that are not path parameters
- - Only the resource identifier part before any colon in custom method paths (e.g., 'resource' in '/resource:customMethod')
+ - Only the resource identifier part before any colon in custom method paths (e.g., `resource` in `/resource:customMethod`)
- Path parameters should also follow camelCase naming
- Certain values can be exempted via the ignoredValues configuration (e.g., 'v1', 'v2') that can be supplied as `ignoredValues` argument to the rule
- - Paths with x-xgen-IPA-exception for this rule are excluded from validation
+ - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation
http://go/ipa/102 message: '{{error}} http://go/ipa/102' severity: warn diff --git a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js index af2529e91f..18114a8481 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js @@ -1,6 +1,6 @@ /** * Checks if the object has results property - * @param {*} schema + * @param {Object} schema * @returns true if schema object returns results property (pagination), false otherwise */ export function schemaIsPaginated(schema) { @@ -11,7 +11,7 @@ export function schemaIsPaginated(schema) { /** * Checks if schema is an array type of schema * - * @param {*} schema + * @param {Object} schema * @returns */ export function schemaIsArray(schema) { From 7459c188a2854a3eebe8f70eb59fb36aab8a70b9 Mon Sep 17 00:00:00 2001 From: wtrocki Date: Tue, 11 Mar 2025 18:16:43 +0100 Subject: [PATCH 7/7] fix: extra unit tests and dedicated handling for slashes in input --- .../collectionIdentifierCamelCase.test.js | 123 +++++++++++++++++- tools/spectral/ipa/rulesets/IPA-102.yaml | 1 + tools/spectral/ipa/rulesets/README.md | 10 +- .../collectionIdentifierCamelCase.js | 30 ++++- 4 files changed, 152 insertions(+), 12 deletions(-) diff --git a/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js b/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js index 4a0efeb6e7..9f67d12763 100644 --- a/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js +++ b/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js @@ -30,7 +30,6 @@ testRule('xgen-IPA-102-collection-identifier-camelCase', [ document: { paths: { '/resources:any_Custom_Method': {}, - '/users/{userId}/data/:AnyOtherMethod': {}, }, }, errors: [], @@ -133,4 +132,126 @@ testRule('xgen-IPA-102-collection-identifier-camelCase', [ }, errors: [], }, + { + name: 'reports violations for paths with double slashes', + document: { + paths: { + '/api//users': {}, + '/resources///{resourceId}': {}, + '//doubleSlashAtStart': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path '/api//users' contains double slashes (//) which is not allowed. http://go/ipa/102", + path: ['paths', '/api//users'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path '/resources///{resourceId}' contains double slashes (//) which is not allowed. http://go/ipa/102", + path: ['paths', '/resources///{resourceId}'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path '//doubleSlashAtStart' contains double slashes (//) which is not allowed. http://go/ipa/102", + path: ['paths', '//doubleSlashAtStart'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'handles paths with trailing slashes', + document: { + paths: { + '/api/users/': {}, + '/resources/{resourceId}/': {}, + }, + }, + errors: [], + }, + { + name: 'detects multiple failures across a single path', + document: { + paths: { + '/API/Resource_groups/{userId}/User-profiles': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path segment 'API' in path '/API/Resource_groups/{userId}/User-profiles' is not in camelCase. http://go/ipa/102", + path: ['paths', '/API/Resource_groups/{userId}/User-profiles'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path segment 'Resource_groups' in path '/API/Resource_groups/{userId}/User-profiles' is not in camelCase. http://go/ipa/102", + path: ['paths', '/API/Resource_groups/{userId}/User-profiles'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path segment 'User-profiles' in path '/API/Resource_groups/{userId}/User-profiles' is not in camelCase. http://go/ipa/102", + path: ['paths', '/API/Resource_groups/{userId}/User-profiles'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'handles mixed valid and invalid segments with custom methods', + document: { + paths: { + '/api/Valid/Invalid_resource/{id}:validCustomMethod': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path segment 'Valid' in path '/api/Valid/Invalid_resource/{id}:validCustomMethod' is not in camelCase. http://go/ipa/102", + path: ['paths', '/api/Valid/Invalid_resource/{id}:validCustomMethod'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path segment 'Invalid_resource' in path '/api/Valid/Invalid_resource/{id}:validCustomMethod' is not in camelCase. http://go/ipa/102", + path: ['paths', '/api/Valid/Invalid_resource/{id}:validCustomMethod'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'handles double slashes with invalid segments - both issues reported', + document: { + paths: { + '/api//Invalid_segment//resources': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path '/api//Invalid_segment//resources' contains double slashes (//) which is not allowed. http://go/ipa/102", + path: ['paths', '/api//Invalid_segment//resources'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "Collection identifiers must be in camelCase. Path segment 'Invalid_segment' in path '/api//Invalid_segment//resources' is not in camelCase. http://go/ipa/102", + path: ['paths', '/api//Invalid_segment//resources'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, ]); diff --git a/tools/spectral/ipa/rulesets/IPA-102.yaml b/tools/spectral/ipa/rulesets/IPA-102.yaml index 7bbbd8ce16..756f3412e3 100644 --- a/tools/spectral/ipa/rulesets/IPA-102.yaml +++ b/tools/spectral/ipa/rulesets/IPA-102.yaml @@ -11,6 +11,7 @@ rules: - Certain values can be exempted via the ignoredValues configuration (e.g., 'v1', 'v2') that can be supplied as `ignoredValues` argument to the rule
- Paths with `x-xgen-IPA-exception` for this rule are excluded from validation
+ - Double slashes (//) are not allowed in paths
http://go/ipa/102 message: '{{error}} http://go/ipa/102' severity: warn diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index a56a05d999..fdb1f83cee 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -20,11 +20,11 @@ For rule definitions, see [IPA-005.yaml](https://github.com/mongodb/openapi/blob For rule definitions, see [IPA-102.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-102.yaml). -| Rule Name | Description | Severity | -| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| xgen-IPA-102-path-alternate-resource-name-path-param | Paths should alternate between resource names and path params. http://go/ipa/102 | error | -| xgen-IPA-102-collection-identifier-camelCase | Collection identifiers must be in camelCase. Logic includes:
- All path segments that are not path parameters
- Only the resource identifier part before any colon in custom method paths (e.g., 'resource' in '/resource:customMethod')
- Path parameters should also follow camelCase naming
- Certain values can be exempted via the ignoredValues configuration (e.g., 'v1', 'v2') that can be supplied as `ignoredValues` argument to the rule
- Paths with x-xgen-IPA-exception for this rule are excluded from validation
http://go/ipa/102 | warn | -| xgen-IPA-102-collection-identifier-pattern | Collection identifiers must begin with a lowercase letter and contain only ASCII letters and numbers. http://go/ipa/102 | warn | +| Rule Name | Description | Severity | +| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| xgen-IPA-102-path-alternate-resource-name-path-param | Paths should alternate between resource names and path params. http://go/ipa/102 | error | +| xgen-IPA-102-collection-identifier-camelCase | Collection identifiers must be in camelCase. Logic includes:
- All path segments that are not path parameters
- Only the resource identifier part before any colon in custom method paths (e.g., `resource` in `/resource:customMethod`)
- Path parameters should also follow camelCase naming
- Certain values can be exempted via the ignoredValues configuration (e.g., 'v1', 'v2') that can be supplied as `ignoredValues` argument to the rule
- Paths with `x-xgen-IPA-exception` for this rule are excluded from validation
- Double slashes (//) are not allowed in paths
http://go/ipa/102 | warn | +| xgen-IPA-102-collection-identifier-pattern | Collection identifiers must begin with a lowercase letter and contain only ASCII letters and numbers. http://go/ipa/102 | warn | ### IPA-104 diff --git a/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js b/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js index 96ce1f23a2..83deaa033c 100644 --- a/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js +++ b/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js @@ -42,9 +42,23 @@ export default (input, options, { path, documentInventory }) => { function checkViolations(pathKey, path, ignoredValues = []) { const violations = []; try { + // Don't filter out empty segments - they should be treated as violations const pathSegments = pathKey.split('/'); + // Check if there are consecutive slashes (empty segments) + if (pathKey.includes('//')) { + violations.push({ + message: `${ERROR_MESSAGE} Path '${pathKey}' contains double slashes (//) which is not allowed.`, + path: [...path, pathKey], + }); + } + pathSegments.forEach((segment) => { + // Skip validation for ignored values + if (ignoredValues.includes(segment)) { + return; + } + // Skip path parameter validation if it matches the expected format if (isPathParam(segment)) { // Extract parameter name without brackets @@ -59,11 +73,6 @@ function checkViolations(pathKey, path, ignoredValues = []) { return; } - // Skip validation for ignored values - if (ignoredValues.includes(segment)) { - return; - } - // For regular path segments, check if they contain custom method indicators // If they do, only validate the identifier part (before the colon) const colonIndex = segment.indexOf(':'); @@ -74,7 +83,16 @@ function checkViolations(pathKey, path, ignoredValues = []) { identifier = segment.substring(0, colonIndex); } - // Skip empty identifiers + // Empty identifiers (from double slashes) should be reported as violations + if (identifier.length === 0 && segment !== '') { + violations.push({ + message: `${ERROR_MESSAGE} Path '${pathKey}' contains an empty segment which is not valid.`, + path: [...path, pathKey], + }); + return; + } + + // Skip empty segments at the beginning/end (these are not from double slashes) if (identifier.length === 0) { return; }