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';