diff --git a/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsResponseSuffixedObject.test.js b/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsResponseSuffixedObject.test.js index fcb03eef76..b373421913 100644 --- a/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsResponseSuffixedObject.test.js +++ b/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsResponseSuffixedObject.test.js @@ -152,7 +152,7 @@ testRule('xgen-IPA-104-get-method-returns-response-suffixed-object', [ errors: [ { code: 'xgen-IPA-104-get-method-returns-response-suffixed-object', - message: 'The response body schema must reference a schema with a Response suffix.', + message: 'The schema must reference a schema with a Response suffix.', path: [ 'paths', '/resource/{id}', @@ -166,7 +166,7 @@ testRule('xgen-IPA-104-get-method-returns-response-suffixed-object', [ }, { code: 'xgen-IPA-104-get-method-returns-response-suffixed-object', - message: 'The response body schema must reference a schema with a Response suffix.', + message: 'The schema must reference a schema with a Response suffix.', path: [ 'paths', '/resource/{id}', @@ -180,7 +180,7 @@ testRule('xgen-IPA-104-get-method-returns-response-suffixed-object', [ }, { code: 'xgen-IPA-104-get-method-returns-response-suffixed-object', - message: 'The response body schema must reference a schema with a Response suffix.', + message: 'The schema must reference a schema with a Response suffix.', path: [ 'paths', '/resource/{id}', @@ -194,7 +194,7 @@ testRule('xgen-IPA-104-get-method-returns-response-suffixed-object', [ }, { code: 'xgen-IPA-104-get-method-returns-response-suffixed-object', - message: 'The response body schema must reference a schema with a Response suffix.', + message: 'The schema must reference a schema with a Response suffix.', path: [ 'paths', '/resource/{id}/singleton', diff --git a/tools/spectral/ipa/__tests__/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.test.js b/tools/spectral/ipa/__tests__/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.test.js index 3bdd45c897..b76e2592d5 100644 --- a/tools/spectral/ipa/__tests__/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.test.js +++ b/tools/spectral/ipa/__tests__/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.test.js @@ -154,31 +154,31 @@ testRule('xgen-IPA-106-create-method-request-body-is-request-suffixed-object', [ errors: [ { code: 'xgen-IPA-106-create-method-request-body-is-request-suffixed-object', - message: 'The response body schema must reference a schema with a Request suffix. ', + message: 'The schema must reference a schema with a Request suffix.', path: ['paths', '/resource', 'post', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-request-body-is-request-suffixed-object', - message: 'The response body schema must reference a schema with a Request suffix. ', + message: 'The schema must reference a schema with a Request suffix.', path: ['paths', '/resource', 'post', 'requestBody', 'content', 'application/vnd.atlas.2024-01-01+json'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-request-body-is-request-suffixed-object', - message: 'The response body schema must reference a schema with a Request suffix. ', + message: 'The schema must reference a schema with a Request suffix.', path: ['paths', '/resourceTwo', 'post', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-request-body-is-request-suffixed-object', - message: 'The response body schema must reference a schema with a Request suffix. ', + message: 'The schema must reference a schema with a Request suffix.', path: ['paths', '/resourceTwo', 'post', 'requestBody', 'content', 'application/vnd.atlas.2024-01-01+json'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-request-body-is-request-suffixed-object', - message: 'The response body schema is defined inline and must reference a predefined schema. ', + message: 'The schema is defined inline and must reference a predefined schema.', path: ['paths', '/resourceThree', 'post', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], severity: DiagnosticSeverity.Warning, }, diff --git a/tools/spectral/ipa/__tests__/IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject.test.js b/tools/spectral/ipa/__tests__/IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject.test.js new file mode 100644 index 0000000000..28e584939c --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject.test.js @@ -0,0 +1,300 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +const componentSchemas = { + schemas: { + SchemaUpdateRequest: { + type: 'object', + }, + Schema: { + type: 'object', + }, + }, +}; + +testRule('xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object', [ + { + name: 'valid schema names names', + document: { + paths: { + '/resource/{id}': { + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaUpdateRequest', + }, + }, + 'application/vnd.atlas.2025-01-01+json': { + schema: { + type: 'array', + items: { + $ref: '#/components/schemas/SchemaUpdateRequest', + }, + }, + }, + }, + }, + }, + put: { + requestBody: { + content: { + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaUpdateRequest', + }, + }, + 'application/vnd.atlas.2025-01-01+json': { + schema: { + type: 'array', + items: { + $ref: '#/components/schemas/SchemaUpdateRequest', + }, + }, + }, + }, + }, + }, + }, + '/resourceTwo/{id}': { + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + type: 'string', + }, + }, + }, + }, + }, + '/resource/{id}/singleton': { + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/SchemaUpdateRequest', + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [], + }, + { + name: 'invalid resources', + document: { + paths: { + '/resource/{id}': { + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/Schema', + }, + }, + 'application/vnd.atlas.2025-01-01+json': { + schema: { + type: 'array', + items: { + $ref: '#/components/schemas/Schema', + }, + }, + }, + }, + }, + }, + put: { + requestBody: { + content: { + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/Schema', + }, + }, + 'application/vnd.atlas.2025-01-01+json': { + schema: { + type: 'array', + items: { + $ref: '#/components/schemas/Schema', + }, + }, + }, + }, + }, + }, + }, + '/resource/{id}/singleton': { + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/Schema', + }, + }, + }, + }, + }, + put: { + requestBody: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/Schema', + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [ + { + code: 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object', + message: 'The schema must reference a schema with a UpdateRequest suffix.', + path: ['paths', '/resource/{id}', 'patch', 'requestBody', 'content', 'application/vnd.atlas.2024-01-01+json'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object', + message: 'The schema must reference a schema with a UpdateRequest suffix.', + path: ['paths', '/resource/{id}', 'patch', 'requestBody', 'content', 'application/vnd.atlas.2025-01-01+json'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object', + message: 'The schema must reference a schema with a UpdateRequest suffix.', + path: ['paths', '/resource/{id}', 'put', 'requestBody', 'content', 'application/vnd.atlas.2024-01-01+json'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object', + message: 'The schema must reference a schema with a UpdateRequest suffix.', + path: ['paths', '/resource/{id}', 'put', 'requestBody', 'content', 'application/vnd.atlas.2025-01-01+json'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object', + message: 'The schema must reference a schema with a UpdateRequest suffix.', + path: [ + 'paths', + '/resource/{id}/singleton', + 'patch', + 'requestBody', + 'content', + 'application/vnd.atlas.2024-08-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object', + message: 'The schema must reference a schema with a UpdateRequest suffix.', + path: [ + 'paths', + '/resource/{id}/singleton', + 'put', + 'requestBody', + 'content', + 'application/vnd.atlas.2024-08-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid resources with exceptions', + document: { + paths: { + '/resource/{id}': { + patch: { + requestBody: { + 'application/vnd.atlas.2024-01-01+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object': 'reason', + }, + schema: { + $ref: '#/components/schemas/Schema', + }, + }, + 'application/vnd.atlas.2025-01-01+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object': 'reason', + }, + schema: { + type: 'array', + items: { + $ref: '#/components/schemas/Schema', + }, + }, + }, + }, + }, + put: { + requestBody: { + 'application/vnd.atlas.2024-01-01+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object': 'reason', + }, + schema: { + $ref: '#/components/schemas/Schema', + }, + }, + 'application/vnd.atlas.2025-01-01+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object': 'reason', + }, + schema: { + type: 'array', + items: { + $ref: '#/components/schemas/Schema', + }, + }, + }, + }, + }, + }, + '/resource/{id}/singleton': { + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object': 'reason', + }, + schema: { + $ref: '#/components/schemas/Schema', + }, + }, + }, + }, + }, + put: { + requestBody: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object': 'reason', + }, + schema: { + $ref: '#/components/schemas/Schema', + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-107.yaml b/tools/spectral/ipa/rulesets/IPA-107.yaml index a221a7fc5c..8540479785 100644 --- a/tools/spectral/ipa/rulesets/IPA-107.yaml +++ b/tools/spectral/ipa/rulesets/IPA-107.yaml @@ -7,6 +7,7 @@ functions: - IPA107UpdateMethodResponseIsGetMethodResponse - IPA107UpdateMethodRequestHasNoReadonlyFields - IPA107UpdateMethodRequestBodyIsGetResponse + - IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject rules: xgen-IPA-107-update-must-not-have-query-params: @@ -81,7 +82,7 @@ rules: Validation checks the PATCH/PUT methods for single resource paths. - Validation ignores resources without a Get method. - - `readOnly:true` properties of Get method response will be ignored. + - `readOnly:true` properties of Get method response will be ignored. - `writeOnly:true` properties of Update method request will be ignored. - Property comparison is based on `type` and `name` matching. - `oneOf` and `discriminator` definitions must match exactly. @@ -91,3 +92,19 @@ rules: then: field: '@key' function: 'IPA107UpdateMethodRequestBodyIsGetResponse' + xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object: + description: | + The Update method request schema should reference an `UpdateRequest` suffixed object. + + ##### Implementation details + Rule checks for the following conditions: + - Applies to PUT/PATCH methods on single resource paths and singleton resources + - Applies only to JSON content types + - Validation only applies to schema references to a predefined schema (not inline) + - Confirms the referenced schema name ends with "Request" suffix + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object' + severity: warn + given: '$.paths[*][put,patch].requestBody.content' + then: + field: '@key' + function: 'IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 81f73824df..2027e5ecf4 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -327,11 +327,23 @@ The request body must contain the resource being updated, i.e. the resource or p Validation checks the PATCH/PUT methods for single resource paths. - Validation ignores resources without a Get method. - - `readOnly:true` properties of Get method response will be ignored. + - `readOnly:true` properties of Get method response will be ignored. - `writeOnly:true` properties of Update method request will be ignored. - Property comparison is based on `type` and `name` matching. - `oneOf` and `discriminator` definitions must match exactly. +#### xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object + + ![warn](https://img.shields.io/badge/warning-yellow) +The Update method request schema should reference an `UpdateRequest` suffixed object. + +##### Implementation details +Rule checks for the following conditions: + - Applies to PUT/PATCH methods on single resource paths and singleton resources + - Applies only to JSON content types + - Validation only applies to schema references to a predefined schema (not inline) + - Confirms the referenced schema name ends with "Request" suffix + ### IPA-108 diff --git a/tools/spectral/ipa/rulesets/functions/IPA104GetMethodReturnsResponseSuffixedObject.js b/tools/spectral/ipa/rulesets/functions/IPA104GetMethodReturnsResponseSuffixedObject.js index 627a19fb75..2a7dbbb0e4 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA104GetMethodReturnsResponseSuffixedObject.js +++ b/tools/spectral/ipa/rulesets/functions/IPA104GetMethodReturnsResponseSuffixedObject.js @@ -6,17 +6,10 @@ import { } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; -import { getSchemaRef } from './utils/methodUtils.js'; +import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { checkSchemaRefSuffixAndReturnErrors } from './utils/validations.js'; const RULE_NAME = 'xgen-IPA-104-get-method-returns-response-suffixed-object'; -const ERROR_MESSAGE_SCHEMA_NAME = 'The response body schema must reference a schema with a Response suffix.'; -const ERROR_MESSAGE_SCHEMA_REF = 'The response body schema is defined inline and must reference a predefined schema.'; export default (input, _, { path, documentInventory }) => { const resourcePath = path[1]; @@ -41,26 +34,10 @@ export default (input, _, { path, documentInventory }) => { return; } - const errors = checkViolationsAndReturnErrors(contentPerMediaType, path); + const errors = checkSchemaRefSuffixAndReturnErrors(path, contentPerMediaType, 'Response', RULE_NAME); + if (errors.length !== 0) { return collectAndReturnViolation(path, RULE_NAME, errors); } collectAdoption(path, RULE_NAME); }; - -function checkViolationsAndReturnErrors(contentPerMediaType, path) { - try { - const schema = contentPerMediaType.schema; - const schemaRef = getSchemaRef(schema); - - if (!schemaRef) { - return [{ path, message: ERROR_MESSAGE_SCHEMA_REF }]; - } - if (!schemaRef.endsWith('Response')) { - return [{ path, message: ERROR_MESSAGE_SCHEMA_NAME }]; - } - return []; - } catch (e) { - handleInternalError(RULE_NAME, path, e); - } -} diff --git a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.js b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.js index 02ae1213c1..e6d70de3c0 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.js +++ b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.js @@ -1,10 +1,5 @@ import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; import { getResourcePathItems, isCustomMethodIdentifier, @@ -12,11 +7,9 @@ import { isSingletonResource, } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; -import { getSchemaRef } from './utils/methodUtils.js'; +import { checkSchemaRefSuffixAndReturnErrors } from './utils/validations.js'; const RULE_NAME = 'xgen-IPA-106-create-method-request-body-is-request-suffixed-object'; -const ERROR_MESSAGE_SCHEMA_NAME = 'The response body schema must reference a schema with a Request suffix.'; -const ERROR_MESSAGE_SCHEMA_REF = 'The response body schema is defined inline and must reference a predefined schema.'; export default (input, _, { path, documentInventory }) => { const oas = documentInventory.unresolved; @@ -39,26 +32,10 @@ export default (input, _, { path, documentInventory }) => { return; } - const errors = checkViolationsAndReturnErrors(contentPerMediaType, path); + const errors = checkSchemaRefSuffixAndReturnErrors(path, contentPerMediaType, 'Request', RULE_NAME); + if (errors.length !== 0) { return collectAndReturnViolation(path, RULE_NAME, errors); } collectAdoption(path, RULE_NAME); }; - -function checkViolationsAndReturnErrors(contentPerMediaType, path) { - try { - const schema = contentPerMediaType.schema; - const schemaRef = getSchemaRef(schema); - - if (!schemaRef) { - return [{ path, message: ERROR_MESSAGE_SCHEMA_REF }]; - } - if (!schemaRef.endsWith('Request')) { - return [{ path, message: ERROR_MESSAGE_SCHEMA_NAME }]; - } - return []; - } catch (e) { - handleInternalError(RULE_NAME, path, e); - } -} diff --git a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject.js b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject.js new file mode 100644 index 0000000000..8ab9778188 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject.js @@ -0,0 +1,39 @@ +import { hasException } from './utils/exceptions.js'; +import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { resolveObject } from './utils/componentUtils.js'; +import { + getResourcePathItems, + isResourceCollectionIdentifier, + isSingleResourceIdentifier, + isSingletonResource, +} from './utils/resourceEvaluation.js'; +import { checkSchemaRefSuffixAndReturnErrors } from './utils/validations.js'; + +const RULE_NAME = 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object'; + +export default (input, _, { path, documentInventory }) => { + const oas = documentInventory.unresolved; + const resourcePath = path[1]; + const contentPerMediaType = resolveObject(oas, path); + const resourcePathItems = getResourcePathItems(resourcePath, oas.paths); + + if ( + !input.endsWith('json') || + !contentPerMediaType.schema || + (!isSingleResourceIdentifier(resourcePath) && + !(isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePathItems))) + ) { + return; + } + + if (hasException(contentPerMediaType, RULE_NAME)) { + collectException(contentPerMediaType, RULE_NAME, path); + return; + } + + const errors = checkSchemaRefSuffixAndReturnErrors(path, contentPerMediaType, 'UpdateRequest', 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 a6b147ce36..78bf93c918 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/validations.js +++ b/tools/spectral/ipa/rulesets/functions/utils/validations.js @@ -161,3 +161,41 @@ export function checkRequestResponseResourceEqualityAndReturnErrors( return errors; } + +/** + * Checks if a request or response content for a media type references a schema with a specific suffix. + * Returns errors if the schema doesn't reference a schema, or if the schema name does not end with the expected suffix. + * Ready to be used in a custom validation function. + * + * @param {string[]} path the path to the object being evaluated + * @param {object} contentPerMediaType the content to evaluate for a specific media type (unresolved) + * @param {string} expectedSuffix the expected suffix to evaluate the schema name for + * @param {string} ruleName the rule name + * @returns {[{path, message: string}]} the errors found, or an empty array in case of no errors + */ +export function checkSchemaRefSuffixAndReturnErrors(path, contentPerMediaType, expectedSuffix, ruleName) { + try { + const schema = contentPerMediaType.schema; + const schemaRef = getSchemaRef(schema); + + if (!schemaRef) { + return [ + { + path, + message: 'The schema is defined inline and must reference a predefined schema.', + }, + ]; + } + if (!schemaRef.endsWith(expectedSuffix)) { + return [ + { + path, + message: `The schema must reference a schema with a ${expectedSuffix} suffix.`, + }, + ]; + } + return []; + } catch (e) { + handleInternalError(ruleName, path, e); + } +}