diff --git a/tools/spectral/ipa/__tests__/IPA110CollectionsRequestHasItemsPerPageQueryParam.test.js b/tools/spectral/ipa/__tests__/IPA110CollectionsRequestHasItemsPerPageQueryParam.test.js new file mode 100644 index 0000000000..ab5bc7c3b7 --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA110CollectionsRequestHasItemsPerPageQueryParam.test.js @@ -0,0 +1,253 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +const parameters = { + parameters: { + pageNum: { + name: 'pageNum', + in: 'query', + schema: { + type: 'integer', + default: 1, + }, + }, + itemsPerPage: { + name: 'itemsPerPage', + in: 'query', + schema: { + type: 'integer', + default: 100, + }, + }, + }, +}; + +testRule('xgen-IPA-110-collections-request-has-itemsPerPage-query-param', [ + { + name: 'valid examples', + document: { + paths: { + '/resources': { + get: { + parameters: [ + { + name: 'itemsPerPage', + in: 'query', + schema: { + type: 'integer', + default: 100, + }, + }, + ], + }, + }, + 'resources/{resourceId}': { + get: {}, + }, + '/resourcesTwo': { + get: { + parameters: [ + { + $ref: '#/components/parameters/itemsPerPage', + }, + ], + }, + }, + 'resourcesTwo/{resourceId}': { + get: {}, + }, + }, + components: parameters, + }, + errors: [], + }, + { + name: 'invalid - missing parameters', + document: { + paths: { + '/resources': { + get: {}, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-request-has-itemsPerPage-query-param', + message: 'List method is missing query parameters.', + path: ['paths', '/resources', 'get'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid - missing itemsPerPage parameter', + document: { + paths: { + '/resources': { + get: { + parameters: [ + { + name: 'pageNum', + in: 'query', + schema: { + type: 'integer', + default: 1, + }, + }, + ], + }, + }, + 'resources/{resourceId}': { + get: {}, + }, + '/resourcesTwo': { + get: { + parameters: [ + { + $ref: '#/components/parameters/pageNum', + }, + ], + }, + }, + 'resourcesTwo/{resourceId}': { + get: {}, + }, + }, + components: parameters, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-request-has-itemsPerPage-query-param', + message: 'List method is missing a itemsPerPage query parameter.', + path: ['paths', '/resources', 'get'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-110-collections-request-has-itemsPerPage-query-param', + message: 'List method is missing a itemsPerPage query parameter.', + path: ['paths', '/resourcesTwo', 'get'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid - itemsPerPage parameter is required', + document: { + paths: { + '/resources': { + get: { + parameters: [ + { + name: 'itemsPerPage', + in: 'query', + required: true, + schema: { + type: 'integer', + default: 100, + }, + }, + ], + }, + }, + 'resources/{resourceId}': { + get: {}, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-request-has-itemsPerPage-query-param', + message: 'itemsPerPage query parameter of List method must not be required.', + path: ['paths', '/resources', 'get'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid - itemsPerPage parameter without default value', + document: { + paths: { + '/resources': { + get: { + parameters: [ + { + name: 'itemsPerPage', + in: 'query', + schema: { + type: 'integer', + }, + }, + ], + }, + }, + 'resources/{resourceId}': { + get: {}, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-request-has-itemsPerPage-query-param', + message: 'itemsPerPage query parameter of List method must have a default value defined.', + path: ['paths', '/resources', 'get'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid - wrong default value', + document: { + paths: { + '/resources': { + get: { + parameters: [ + { + name: 'itemsPerPage', + in: 'query', + schema: { + type: 'integer', + default: 0, + }, + }, + ], + }, + }, + 'resources/{resourceId}': { + get: {}, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-request-has-itemsPerPage-query-param', + message: 'itemsPerPage query parameter of List method must have a default value of 100.', + path: ['paths', '/resources', 'get'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'exception', + document: { + paths: { + '/resourcesTwo': { + get: { + parameters: [ + { + $ref: '#/components/parameters/pageNum', + }, + ], + 'x-xgen-IPA-exception': { + 'xgen-IPA-110-collections-request-has-itemsPerPage-query-param': 'Reason', + }, + }, + }, + 'resourcesTwo/{resourceId}': { + get: {}, + }, + }, + components: parameters, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/__tests__/IPA110CollectionsRequestHasPageNumQueryParam.test.js b/tools/spectral/ipa/__tests__/IPA110CollectionsRequestHasPageNumQueryParam.test.js new file mode 100644 index 0000000000..8f8a05918a --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA110CollectionsRequestHasPageNumQueryParam.test.js @@ -0,0 +1,253 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +const parameters = { + parameters: { + pageNum: { + name: 'pageNum', + in: 'query', + schema: { + type: 'integer', + default: 1, + }, + }, + itemsPerPage: { + name: 'itemsPerPage', + in: 'query', + schema: { + type: 'integer', + default: 100, + }, + }, + }, +}; + +testRule('xgen-IPA-110-collections-request-has-pageNum-query-param', [ + { + name: 'valid examples', + document: { + paths: { + '/resources': { + get: { + parameters: [ + { + name: 'pageNum', + in: 'query', + schema: { + type: 'integer', + default: 1, + }, + }, + ], + }, + }, + 'resources/{resourceId}': { + get: {}, + }, + '/resourcesTwo': { + get: { + parameters: [ + { + $ref: '#/components/parameters/pageNum', + }, + ], + }, + }, + 'resourcesTwo/{resourceId}': { + get: {}, + }, + }, + components: parameters, + }, + errors: [], + }, + { + name: 'invalid - missing parameters', + document: { + paths: { + '/resources': { + get: {}, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-request-has-pageNum-query-param', + message: 'List method is missing query parameters.', + path: ['paths', '/resources', 'get'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid - missing pageNum parameter', + document: { + paths: { + '/resources': { + get: { + parameters: [ + { + name: 'itemsPerPage', + in: 'query', + schema: { + type: 'integer', + default: 100, + }, + }, + ], + }, + }, + 'resources/{resourceId}': { + get: {}, + }, + '/resourcesTwo': { + get: { + parameters: [ + { + $ref: '#/components/parameters/itemsPerPage', + }, + ], + }, + }, + 'resourcesTwo/{resourceId}': { + get: {}, + }, + }, + components: parameters, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-request-has-pageNum-query-param', + message: 'List method is missing a pageNum query parameter.', + path: ['paths', '/resources', 'get'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-110-collections-request-has-pageNum-query-param', + message: 'List method is missing a pageNum query parameter.', + path: ['paths', '/resourcesTwo', 'get'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid - pageNum parameter is required', + document: { + paths: { + '/resources': { + get: { + parameters: [ + { + name: 'pageNum', + in: 'query', + required: true, + schema: { + type: 'integer', + default: 1, + }, + }, + ], + }, + }, + 'resources/{resourceId}': { + get: {}, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-request-has-pageNum-query-param', + message: 'pageNum query parameter of List method must not be required.', + path: ['paths', '/resources', 'get'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid - pageNum parameter without default value', + document: { + paths: { + '/resources': { + get: { + parameters: [ + { + name: 'pageNum', + in: 'query', + schema: { + type: 'integer', + }, + }, + ], + }, + }, + 'resources/{resourceId}': { + get: {}, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-request-has-pageNum-query-param', + message: 'pageNum query parameter of List method must have a default value defined.', + path: ['paths', '/resources', 'get'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid - wrong default value', + document: { + paths: { + '/resources': { + get: { + parameters: [ + { + name: 'pageNum', + in: 'query', + schema: { + type: 'integer', + default: 0, + }, + }, + ], + }, + }, + 'resources/{resourceId}': { + get: {}, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-request-has-pageNum-query-param', + message: 'pageNum query parameter of List method must have a default value of 1.', + path: ['paths', '/resources', 'get'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'exception', + document: { + paths: { + '/resourcesTwo': { + get: { + parameters: [ + { + $ref: '#/components/parameters/itemsPerPage', + }, + ], + 'x-xgen-IPA-exception': { + 'xgen-IPA-110-collections-request-has-pageNum-query-param': 'Reason', + }, + }, + }, + 'resourcesTwo/{resourceId}': { + get: {}, + }, + }, + components: parameters, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-110.yaml b/tools/spectral/ipa/rulesets/IPA-110.yaml index 876e64f1b8..6007a703b3 100644 --- a/tools/spectral/ipa/rulesets/IPA-110.yaml +++ b/tools/spectral/ipa/rulesets/IPA-110.yaml @@ -4,6 +4,8 @@ functions: - IPA110CollectionsUsePaginatedPrefix - IPA110CollectionsResponseDefineResultsArray + - IPA110CollectionsRequestHasItemsPerPageQueryParam + - IPA110CollectionsRequestHasPageNumQueryParam rules: xgen-IPA-110-collections-use-paginated-prefix: @@ -35,3 +37,37 @@ rules: then: field: '@key' function: 'IPA110CollectionsResponseDefineResultsArray' + xgen-IPA-110-collections-request-has-itemsPerPage-query-param: + description: | + The request should support an integer itemsPerPage query parameter allowing users to specify the maximum number of results to return per page. + itemsPerPage must not be required + itemsPerPage default value should be 100. + + ##### Implementation details + Rule checks for the following conditions: + - Only applies to List methods (GET on resource collection paths) + - Verifies the operation includes itemsPerPage query parameter + - Verifies the itemsPerPage query parameter is not required + - Verifies the itemsPerPage query parameter has a default value of 100 + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-110-collections-request-has-itemsPerPage-query-param' + severity: warn + given: '$.paths[*].get' + then: + function: 'IPA110CollectionsRequestHasItemsPerPageQueryParam' + xgen-IPA-110-collections-request-has-pageNum-query-param: + description: | + The request should support an integer pageNum query parameter allowing users to specify the maximum number of results to return per page. + pageNum must not be required + pageNum default value should be 1. + + ##### Implementation details + Rule checks for the following conditions: + - Only applies to List methods (GET on resource collection paths) + - Verifies the operation includes pageNum query parameter + - Verifies the pageNum query parameter is not required + - Verifies the pageNum query parameter has a default value of 1 + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-110-collections-request-has-pageNum-query-param' + severity: warn + given: '$.paths[*].get' + then: + function: 'IPA110CollectionsRequestHasPageNumQueryParam' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index bbc876b77a..f07c5d3dac 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -472,6 +472,34 @@ Rule checks for the following conditions: - Only applies to List methods (GET operations that return collections of resources) - Verifies the 200 response schema has the required results fields +#### xgen-IPA-110-collections-request-has-itemsPerPage-query-param + + ![warn](https://img.shields.io/badge/warning-yellow) +The request should support an integer itemsPerPage query parameter allowing users to specify the maximum number of results to return per page. +itemsPerPage must not be required +itemsPerPage default value should be 100. + +##### Implementation details +Rule checks for the following conditions: + - Only applies to List methods (GET on resource collection paths) + - Verifies the operation includes itemsPerPage query parameter + - Verifies the itemsPerPage query parameter is not required + - Verifies the itemsPerPage query parameter has a default value of 100 + +#### xgen-IPA-110-collections-request-has-pageNum-query-param + + ![warn](https://img.shields.io/badge/warning-yellow) +The request should support an integer pageNum query parameter allowing users to specify the maximum number of results to return per page. +pageNum must not be required +pageNum default value should be 1. + +##### Implementation details +Rule checks for the following conditions: + - Only applies to List methods (GET on resource collection paths) + - Verifies the operation includes pageNum query parameter + - Verifies the pageNum query parameter is not required + - Verifies the pageNum query parameter has a default value of 1 + ### IPA-112 diff --git a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasItemsPerPageQueryParam.js b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasItemsPerPageQueryParam.js new file mode 100644 index 0000000000..4e593d97ab --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasItemsPerPageQueryParam.js @@ -0,0 +1,36 @@ +import { hasException } from './utils/exceptions.js'; +import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + getResourcePathItems, + isResourceCollectionIdentifier, + isSingletonResource, +} from './utils/resourceEvaluation.js'; +import { checkPaginationQueryParameterAndReturnErrors } from './utils/validations.js'; + +const RULE_NAME = 'xgen-IPA-110-collections-request-has-itemsPerPage-query-param'; + +export default (input, _, { path, documentInventory }) => { + const oas = documentInventory.resolved; + const resourcePath = path[1]; + + if ( + !isResourceCollectionIdentifier(resourcePath) || + (isResourceCollectionIdentifier(resourcePath) && isSingletonResource(getResourcePathItems(resourcePath, oas.paths))) + ) { + return; + } + + // Check for exception + if (hasException(input, RULE_NAME)) { + collectException(input, RULE_NAME, path); + return; + } + + const errors = checkPaginationQueryParameterAndReturnErrors(input, path, 'itemsPerPage', 100, RULE_NAME); + + if (errors.length > 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; diff --git a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasPageNumQueryParam.js b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasPageNumQueryParam.js new file mode 100644 index 0000000000..7af9021dd4 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasPageNumQueryParam.js @@ -0,0 +1,36 @@ +import { hasException } from './utils/exceptions.js'; +import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + getResourcePathItems, + isResourceCollectionIdentifier, + isSingletonResource, +} from './utils/resourceEvaluation.js'; +import { checkPaginationQueryParameterAndReturnErrors } from './utils/validations.js'; + +const RULE_NAME = 'xgen-IPA-110-collections-request-has-pageNum-query-param'; + +export default (input, _, { path, documentInventory }) => { + const oas = documentInventory.resolved; + const resourcePath = path[1]; + + if ( + !isResourceCollectionIdentifier(resourcePath) || + (isResourceCollectionIdentifier(resourcePath) && isSingletonResource(getResourcePathItems(resourcePath, oas.paths))) + ) { + return; + } + + // Check for exception + if (hasException(input, RULE_NAME)) { + collectException(input, RULE_NAME, path); + return; + } + + const errors = checkPaginationQueryParameterAndReturnErrors(input, path, 'pageNum', 1, RULE_NAME); + + if (errors.length > 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; diff --git a/tools/spectral/ipa/rulesets/functions/utils/validations.js b/tools/spectral/ipa/rulesets/functions/utils/validations.js index 78bf93c918..58187c9730 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/validations.js +++ b/tools/spectral/ipa/rulesets/functions/utils/validations.js @@ -199,3 +199,73 @@ export function checkSchemaRefSuffixAndReturnErrors(path, contentPerMediaType, e handleInternalError(ruleName, path, e); } } + +/** + * Checks if a list method has the required pagination query parameter with correct configuration + * + * @param {Object} operation - The OpenAPI operation object to check + * @param {string[]} path - The path to the operation + * @param {string} paramName - The name of the parameter to check ('pageNum' or 'itemsPerPage') + * @param {number} defaultValue - The expected default value (1 for pageNum, 100 for itemsPerPage) + * @param {string} ruleName - The rule name for error handling + * @returns {Array} - Array of error objects or empty array if no errors + */ +export function checkPaginationQueryParameterAndReturnErrors(operation, path, paramName, defaultValue, ruleName) { + try { + const parameters = operation.parameters; + + if (!parameters) { + return [ + { + path, + message: `List method is missing query parameters.`, + }, + ]; + } + + const param = parameters.find( + (p) => p.name === paramName && p.in === 'query' && p.schema && p.schema.type === 'integer' + ); + + if (!param) { + return [ + { + path, + message: `List method is missing a ${paramName} query parameter.`, + }, + ]; + } + + if (param.required === true) { + return [ + { + path, + message: `${paramName} query parameter of List method must not be required.`, + }, + ]; + } + + if (param.schema.default === undefined) { + return [ + { + path, + message: `${paramName} query parameter of List method must have a default value defined.`, + }, + ]; + } + + if (param.schema.default !== defaultValue) { + return [ + { + path, + message: `${paramName} query parameter of List method must have a default value of ${defaultValue}.`, + }, + ]; + } + + return []; + } catch (e) { + handleInternalError(ruleName, path, e); + return []; + } +}