diff --git a/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js b/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js new file mode 100644 index 0000000000..9f67d12763 --- /dev/null +++ b/tools/spectral/ipa/__tests__/collectionIdentifierCamelCase.test.js @@ -0,0 +1,257 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +testRule('xgen-IPA-102-collection-identifier-camelCase', [ + { + name: 'valid camelCase identifiers', + document: { + paths: { + '/api/v2/atlas/test': {}, + '/users': {}, + '/resourceGroups': {}, + '/userProfiles': {}, + '/api/v1/test': {}, + }, + }, + errors: [], + }, + { + name: 'valid camelCase with path parameters', + document: { + paths: { + '/resourceGroups/{groupId}': {}, + '/users/{userId}/userProfiles': {}, + }, + }, + errors: [], + }, + { + name: 'valid paths with custom methods (only checking identifier part)', + document: { + paths: { + '/resources:any_Custom_Method': {}, + }, + }, + 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 resource path with invalid casing but valid custom method', + document: { + paths: { + '/Resources:createResource': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-collection-identifier-camelCase', + message: + "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, + }, + ], + }, + { + 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: [], + }, + { + 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 766f67c1c4..756f3412e3 100644 --- a/tools/spectral/ipa/rulesets/IPA-102.yaml +++ b/tools/spectral/ipa/rulesets/IPA-102.yaml @@ -2,6 +2,27 @@ # http://go/ipa/102 rules: + xgen-IPA-102-collection-identifier-camelCase: + 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
+ - Double slashes (//) are not allowed in paths
+ http://go/ipa/102 + message: '{{error}} http://go/ipa/102' + severity: warn + given: $.paths + 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' message: '{{error}} http://go/ipa/102' @@ -23,3 +44,4 @@ rules: functions: - collectionIdentifierPattern - eachPathAlternatesBetweenResourceNameAndPathParam + - collectionIdentifierCamelCase diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index ab7314dc90..fdb1f83cee 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -20,10 +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-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 new file mode 100644 index 0000000000..83deaa033c --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/collectionIdentifierCamelCase.js @@ -0,0 +1,118 @@ +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'; + +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} options - Rule configuration options + * @param {object} context - The context object containing the path and documentInventory + */ +export default (input, options, { 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; + } + + // 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); + } + + return collectAdoption(path, RULE_NAME); +}; + +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 + 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; + } + + // 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); + } + + // 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; + } + + // 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], + }); + } + }); + } 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 ae10e80e63..18114a8481 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js @@ -1,8 +1,19 @@ +/** + * Checks if the object has results property + * @param {Object} 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 {Object} schema + * @returns + */ export function schemaIsArray(schema) { const fields = Object.keys(schema); return fields.includes('type') && schema['type'] === 'array';