diff --git a/tools/spectral/ipa/__tests__/IPA124ArrayMaxItems.test.js b/tools/spectral/ipa/__tests__/IPA124ArrayMaxItems.test.js new file mode 100644 index 0000000000..211616b255 --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA124ArrayMaxItems.test.js @@ -0,0 +1,403 @@ +import testRule from './__helpers__/testRule.js'; +import { DiagnosticSeverity } from '@stoplight/types'; + +testRule('xgen-IPA-124-array-max-items', [ + { + name: 'valid array with maxItems set to 100', + document: { + components: { + schemas: { + ValidSchema: { + type: 'object', + properties: { + arrayProperty: { + type: 'array', + maxItems: 100, + items: { + type: 'string', + }, + }, + anotherArrayProperty: { + type: 'array', + maxItems: 50, + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, + { + name: 'invalid array missing maxItems', + document: { + components: { + schemas: { + InvalidSchema: { + type: 'object', + properties: { + arrayProperty: { + type: 'array', + items: { + type: 'string', + }, + }, + links: { + type: 'array', + items: { + type: 'object', + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-124-array-max-items', + message: 'Array must have maxItems property defined to enforce an upper bound on the number of items.', + path: ['components', 'schemas', 'InvalidSchema', 'properties', 'arrayProperty'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid array with incorrect maxItems value', + document: { + components: { + schemas: { + InvalidSchema: { + type: 'object', + properties: { + arrayProperty: { + type: 'array', + maxItems: 101, + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-124-array-max-items', + message: + 'The maxItems value for arrays must be set to 100 or below, found: 101. If the array field has the chance of being too large, the API should use a sub-resource instead.', + path: ['components', 'schemas', 'InvalidSchema', 'properties', 'arrayProperty'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'array with exception should be skipped', + document: { + components: { + schemas: { + ExceptionSchema: { + type: 'object', + properties: { + arrayProperty: { + type: 'array', + items: { + type: 'string', + }, + 'x-xgen-IPA-exception': { + 'xgen-IPA-124-array-max-items': 'Reason', + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, + { + name: 'nested arrays should all be checked', + document: { + components: { + schemas: { + NestedArrays: { + type: 'object', + properties: { + outerArray: { + type: 'array', + maxItems: 500, + arrayProperty: { + type: 'array', + maxItems: 500, + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-124-array-max-items', + message: + 'The maxItems value for arrays must be set to 100 or below, found: 500. If the array field has the chance of being too large, the API should use a sub-resource instead.', + path: ['components', 'schemas', 'NestedArrays', 'properties', 'outerArray'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-124-array-max-items', + message: + 'The maxItems value for arrays must be set to 100 or below, found: 500. If the array field has the chance of being too large, the API should use a sub-resource instead.', + path: ['components', 'schemas', 'NestedArrays', 'properties', 'outerArray', 'arrayProperty'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'arrays in request/response bodies should be checked', + document: { + paths: { + '/api/resources': { + post: { + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + arrayProperty: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-124-array-max-items', + message: 'Array must have maxItems property defined to enforce an upper bound on the number of items.', + path: [ + 'paths', + '/api/resources', + 'post', + 'requestBody', + 'content', + 'application/json', + 'schema', + 'properties', + 'arrayProperty', + ], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'parameter arrays should be checked', + document: { + paths: { + '/api/resources': { + get: { + parameters: [ + { + name: 'ids', + in: 'query', + schema: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + ], + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-124-array-max-items', + message: 'Array must have maxItems property defined to enforce an upper bound on the number of items.', + path: ['paths', '/api/resources', 'get', 'parameters', '0', 'schema'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'arrays with property names in the ignore list should be skipped', + document: { + components: { + schemas: { + IgnoredSchema: { + type: 'object', + properties: { + links: { + type: 'array', + items: { + type: 'object', + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, + { + name: 'arrays in response content should be checked - invalid case', + document: { + paths: { + '/api/resources': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + type: 'array', + items: { + type: 'object', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-124-array-max-items', + message: 'Array must have maxItems property defined to enforce an upper bound on the number of items.', + path: [ + 'paths', + '/api/resources', + 'get', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2023-01-01+json', + 'schema', + ], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'arrays in response content should be checked - valid case', + document: { + paths: { + '/api/resources': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + type: 'array', + maxItems: 100, + items: { + type: 'object', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, + { + name: 'arrays in response content with excessive maxItems', + document: { + paths: { + '/api/resources': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + type: 'array', + maxItems: 500, + items: { + type: 'object', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-124-array-max-items', + message: + 'The maxItems value for arrays must be set to 100 or below, found: 500. If the array field has the chance of being too large, the API should use a sub-resource instead.', + path: [ + 'paths', + '/api/resources', + 'get', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2023-01-01+json', + 'schema', + ], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'arrays in response content with exception should be skipped', + document: { + paths: { + '/api/resources': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + type: 'array', + items: { + type: 'object', + }, + 'x-xgen-IPA-exception': { + 'xgen-IPA-124-array-max-items': 'Large response array is necessary', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/ipa-spectral.yaml b/tools/spectral/ipa/ipa-spectral.yaml index e45c7540fa..ea85043bca 100644 --- a/tools/spectral/ipa/ipa-spectral.yaml +++ b/tools/spectral/ipa/ipa-spectral.yaml @@ -16,6 +16,7 @@ extends: - ./rulesets/IPA-119.yaml - ./rulesets/IPA-121.yaml - ./rulesets/IPA-123.yaml + - ./rulesets/IPA-124.yaml - ./rulesets/IPA-125.yaml overrides: diff --git a/tools/spectral/ipa/rulesets/IPA-123.yaml b/tools/spectral/ipa/rulesets/IPA-123.yaml index 61b0fc8bef..a599e95352 100644 --- a/tools/spectral/ipa/rulesets/IPA-123.yaml +++ b/tools/spectral/ipa/rulesets/IPA-123.yaml @@ -32,6 +32,7 @@ rules: - Validates that each enum array has 20 or fewer values - Reusable enum schemas will be ignored - Skips validation if the schema has an exception defined for this rule + - This validation threshold can be adjusted by changing the functionOptions.maxEnumValues parameter message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-123-allowable-enum-values-should-not-exceed-20' severity: warn resolved: false diff --git a/tools/spectral/ipa/rulesets/IPA-124.yaml b/tools/spectral/ipa/rulesets/IPA-124.yaml new file mode 100644 index 0000000000..bac3432f43 --- /dev/null +++ b/tools/spectral/ipa/rulesets/IPA-124.yaml @@ -0,0 +1,30 @@ +# IPA-124: Repeated Fields +# http://go/ipa/124 + +functions: + - IPA124ArrayMaxItems + +rules: + xgen-IPA-124-array-max-items: + description: | + Array fields must have a maxItems property defined to enforce an upper bound on the number of items (recommended max: 100). If the array field has the chance of being too large, the API should use a sub-resource instead. + + ##### Implementation details + Rule checks for the following conditions: + + - All schema objects with type 'array' must have a maxItems property + - The maxItems value must be set below the threshold of 100 + + ##### Function options + - maxItems: Required integer parameter specifying the maximum allowed array size (default: 100) + - ignore: Required array parameter listing property names to be exempted from validation + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-124-array-max-items' + severity: warn + resolved: false + given: $..[?(@ && @.type === "array")] + then: + function: IPA124ArrayMaxItems + functionOptions: + maxItems: 100 + ignore: + - links diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 7038d47085..27e7c81fa2 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -841,6 +841,28 @@ Rule checks for the following conditions: - Validates that each enum array has 20 or fewer values - Reusable enum schemas will be ignored - Skips validation if the schema has an exception defined for this rule + - This validation threshold can be adjusted by changing the functionOptions.maxEnumValues parameter + + + +### IPA-124 + +Rules are based on [http://go/ipa/IPA-124](http://go/ipa/IPA-124). + +#### xgen-IPA-124-array-max-items + + ![warn](https://img.shields.io/badge/warning-yellow) +Array fields must have a maxItems property defined to enforce an upper bound on the number of items (recommended max: 100). If the array field has the chance of being too large, the API should use a sub-resource instead. + +##### Implementation details +Rule checks for the following conditions: + + - All schema objects with type 'array' must have a maxItems property + - The maxItems value must be set below the threshold of 100 + +##### Function options + - maxItems: Required integer parameter specifying the maximum allowed array size (default: 100) + - ignore: Required array parameter listing property names to be exempted from validation diff --git a/tools/spectral/ipa/rulesets/functions/IPA124ArrayMaxItems.js b/tools/spectral/ipa/rulesets/functions/IPA124ArrayMaxItems.js new file mode 100644 index 0000000000..91429dd3ae --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA124ArrayMaxItems.js @@ -0,0 +1,72 @@ +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; +import { hasException } from './utils/exceptions.js'; + +const RULE_NAME = 'xgen-IPA-124-array-max-items'; + +/** + * Checks if array fields have maxItems defined and set to 100 + * + * @param {object} input - The array schema object from the OpenAPI spec + * @param {object} options - Rule configuration options + * @param {object} context - The context object containing the path and documentInventory + */ +export default (input, { maxItems, ignore = [] }, { path }) => { + // Check for exception at the schema level + if (hasException(input, RULE_NAME)) { + collectException(input, RULE_NAME, path); + return; + } + + let propertyName; + if (path.includes('parameters')) { + propertyName = input.name; + } else if (path.includes('content')) { + propertyName = null; + } else { + propertyName = path[path.length - 1]; + } + + // Check if the parameter name is in the ignore list + if (ignore.includes(propertyName)) { + return; + } + + const errors = checkViolationsAndReturnErrors(input, path, maxItems); + if (errors.length > 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; + +function checkViolationsAndReturnErrors(input, path, maxItems) { + try { + // Check if maxItems is defined + if (input.maxItems === undefined) { + return [ + { + message: `Array must have maxItems property defined to enforce an upper bound on the number of items.`, + path, + }, + ]; + } + // Check if maxItems is larger than the recommended value + else if (input.maxItems > maxItems) { + return [ + { + message: `The maxItems value for arrays must be set to ${maxItems} or below, found: ${input.maxItems}. If the array field has the chance of being too large, the API should use a sub-resource instead.`, + path, + }, + ]; + } + + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +}