From fa3b901730d098e14cacc94b05343ff2a6e6b0fe Mon Sep 17 00:00:00 2001 From: Lovisa Berggren Date: Fri, 7 Mar 2025 15:39:18 +0000 Subject: [PATCH 1/3] CLOUDP-302981: Add rule xgen-IPA-104-get-method-response-has-no-input-fields --- .../getMethodResponseHasNoInputFields.test.js | 218 ++++++++++++++++++ tools/spectral/ipa/rulesets/IPA-104.yaml | 9 + tools/spectral/ipa/rulesets/README.md | 13 +- .../getMethodResponseHasNoInputFields.js | 55 +++++ 4 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 tools/spectral/ipa/__tests__/getMethodResponseHasNoInputFields.test.js create mode 100644 tools/spectral/ipa/rulesets/functions/getMethodResponseHasNoInputFields.js diff --git a/tools/spectral/ipa/__tests__/getMethodResponseHasNoInputFields.test.js b/tools/spectral/ipa/__tests__/getMethodResponseHasNoInputFields.test.js new file mode 100644 index 0000000000..bd6afda00a --- /dev/null +++ b/tools/spectral/ipa/__tests__/getMethodResponseHasNoInputFields.test.js @@ -0,0 +1,218 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +const componentSchemas = { + schemas: { + ValidSchemaResponse: { + type: 'object', + properties: { + id: { + type: 'string', + readOnly: true, + }, + name: { + type: 'string', + }, + }, + }, + InvalidSchemaResponse: { + type: 'object', + properties: { + id: { + type: 'string', + readOnly: true, + }, + name: { + type: 'string', + writeOnly: true, + }, + }, + }, + }, +}; + +testRule('xgen-IPA-104-get-method-response-has-no-input-fields', [ + { + name: 'valid get responses', + document: { + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ValidSchemaResponse', + }, + }, + }, + }, + }, + }, + }, + '/resource/{id}/singleton': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + type: 'object', + properties: { + id: { + type: 'string', + readOnly: true, + }, + name: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [], + }, + { + name: 'invalid GET with body', + document: { + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/InvalidSchemaResponse', + }, + }, + }, + }, + }, + }, + }, + '/resource/{id}/singleton': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + type: 'object', + properties: { + id: { + type: 'string', + readOnly: true, + }, + name: { + type: 'string', + writeOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [ + { + code: 'xgen-IPA-104-get-method-response-has-no-input-fields', + message: + 'The get method response object must not include output fields (writeOnly properties). http://go/ipa/104', + path: [ + 'paths', + '/resource/{id}', + 'get', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2024-08-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-104-get-method-response-has-no-input-fields', + message: + 'The get method response object must not include output fields (writeOnly properties). http://go/ipa/104', + path: [ + 'paths', + '/resource/{id}/singleton', + 'get', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2024-08-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid with exception', + document: { + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-104-get-method-response-has-no-input-fields': 'reason', + }, + schema: { + $ref: '#/components/schemas/InvalidSchemaResponse', + }, + }, + }, + }, + }, + }, + }, + '/resource/{id}/singleton': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-104-get-method-response-has-no-input-fields': 'reason', + }, + schema: { + type: 'object', + properties: { + id: { + type: 'string', + readOnly: true, + }, + name: { + type: 'string', + writeOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-104.yaml b/tools/spectral/ipa/rulesets/IPA-104.yaml index dcf9e6461f..d20d3ce519 100644 --- a/tools/spectral/ipa/rulesets/IPA-104.yaml +++ b/tools/spectral/ipa/rulesets/IPA-104.yaml @@ -6,6 +6,7 @@ functions: - getMethodReturnsSingleResource - getMethodReturnsResponseSuffixedObject - getResponseCodeShouldBe200OK + - getMethodResponseHasNoInputFields rules: xgen-IPA-104-resource-has-GET: @@ -38,3 +39,11 @@ rules: then: field: '@key' function: 'getMethodReturnsResponseSuffixedObject' + xgen-IPA-104-get-method-response-has-no-input-fields: + description: 'The get method response object must not include fields available only on creation or update, ie output fields. http://go/ipa/104' + message: '{{error}} http://go/ipa/104' + severity: warn + given: '$.paths[*].get.responses[*].content' + then: + field: '@key' + function: 'getMethodResponseHasNoInputFields' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 619ef6572b..9ca66fe30c 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -28,12 +28,13 @@ For rule definitions, see [IPA-102.yaml](https://github.com/mongodb/openapi/blob For rule definitions, see [IPA-104.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-104.yaml). -| Rule Name | Description | Severity | -| -------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------- | -| xgen-IPA-104-resource-has-GET | APIs must provide a Get method for resources. http://go/ipa/104 | warn | -| xgen-IPA-104-get-method-returns-single-resource | The purpose of the Get method is to return data from a single resource. http://go/ipa/104 | warn | -| xgen-IPA-104-get-method-response-code-is-200 | The Get method must return a 200 OK response. http://go/ipa/104 | warn | -| xgen-IPA-104-get-method-returns-response-suffixed-object | The Get method of a resource should return a "Response" suffixed object. http://go/ipa/104 | warn | +| Rule Name | Description | Severity | +| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------- | +| xgen-IPA-104-resource-has-GET | APIs must provide a Get method for resources. http://go/ipa/104 | warn | +| xgen-IPA-104-get-method-returns-single-resource | The purpose of the Get method is to return data from a single resource. http://go/ipa/104 | warn | +| xgen-IPA-104-get-method-response-code-is-200 | The Get method must return a 200 OK response. http://go/ipa/104 | warn | +| xgen-IPA-104-get-method-returns-response-suffixed-object | The Get method of a resource should return a "Response" suffixed object. http://go/ipa/104 | warn | +| xgen-IPA-104-get-method-response-has-no-input-fields | The get method response object must not include fields available only on creation or update, ie output fields. http://go/ipa/104 | warn | ### IPA-106 diff --git a/tools/spectral/ipa/rulesets/functions/getMethodResponseHasNoInputFields.js b/tools/spectral/ipa/rulesets/functions/getMethodResponseHasNoInputFields.js new file mode 100644 index 0000000000..27b1a8931f --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/getMethodResponseHasNoInputFields.js @@ -0,0 +1,55 @@ +import { hasException } from './utils/exceptions.js'; +import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + getResourcePathItems, + isResourceCollectionIdentifier, + isSingleResourceIdentifier, + isSingletonResource, +} from './utils/resourceEvaluation.js'; +import { resolveObject } from './utils/componentUtils.js'; + +const RULE_NAME = 'xgen-IPA-104-get-method-response-has-no-input-fields'; +const ERROR_MESSAGE = 'The get method response object must not include output fields (writeOnly properties).'; + +export default (input, _, { path, documentInventory }) => { + const resourcePath = path[1]; + const responseCode = path[4]; + const oas = documentInventory.resolved; + const resourcePaths = getResourcePathItems(resourcePath, oas.paths); + const contentPerMediaType = resolveObject(oas, path); + + if ( + !contentPerMediaType.schema || + !responseCode.startsWith('2') || + !input.endsWith('json') || + (!isSingleResourceIdentifier(resourcePath) && + !(isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePaths))) + ) { + return; + } + + if (hasException(contentPerMediaType, RULE_NAME)) { + collectException(contentPerMediaType, RULE_NAME, path); + return; + } + + const errors = checkViolationsAndReturnErrors(contentPerMediaType, path); + + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + return collectAdoption(path, RULE_NAME); +}; + +function checkViolationsAndReturnErrors(contentPerMediaType, path) { + const schema = contentPerMediaType.schema; + const properties = schema.properties; + if (properties) { + for (const [value] of Object.entries(properties)) { + if (properties[value].writeOnly) { + return [{ path, message: ERROR_MESSAGE }]; + } + } + } + return []; +} From 2fb28e0dcc8c8ccd034e2fc2a4e2b90d02718860 Mon Sep 17 00:00:00 2001 From: Lovisa Berggren Date: Fri, 7 Mar 2025 16:24:46 +0000 Subject: [PATCH 2/3] CLOUDP-302981: Fix --- tools/spectral/ipa/rulesets/IPA-104.yaml | 2 +- tools/spectral/ipa/rulesets/README.md | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/spectral/ipa/rulesets/IPA-104.yaml b/tools/spectral/ipa/rulesets/IPA-104.yaml index d20d3ce519..986e263b6d 100644 --- a/tools/spectral/ipa/rulesets/IPA-104.yaml +++ b/tools/spectral/ipa/rulesets/IPA-104.yaml @@ -40,7 +40,7 @@ rules: field: '@key' function: 'getMethodReturnsResponseSuffixedObject' xgen-IPA-104-get-method-response-has-no-input-fields: - description: 'The get method response object must not include fields available only on creation or update, ie output fields. http://go/ipa/104' + description: 'The Get method response object must not include writeOnly properties (fields that should be used only on creation or update, ie output fields). http://go/ipa/104' message: '{{error}} http://go/ipa/104' severity: warn given: '$.paths[*].get.responses[*].content' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 9ca66fe30c..96ff51f3c1 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -28,13 +28,13 @@ For rule definitions, see [IPA-102.yaml](https://github.com/mongodb/openapi/blob For rule definitions, see [IPA-104.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-104.yaml). -| Rule Name | Description | Severity | -| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------- | -| xgen-IPA-104-resource-has-GET | APIs must provide a Get method for resources. http://go/ipa/104 | warn | -| xgen-IPA-104-get-method-returns-single-resource | The purpose of the Get method is to return data from a single resource. http://go/ipa/104 | warn | -| xgen-IPA-104-get-method-response-code-is-200 | The Get method must return a 200 OK response. http://go/ipa/104 | warn | -| xgen-IPA-104-get-method-returns-response-suffixed-object | The Get method of a resource should return a "Response" suffixed object. http://go/ipa/104 | warn | -| xgen-IPA-104-get-method-response-has-no-input-fields | The get method response object must not include fields available only on creation or update, ie output fields. http://go/ipa/104 | warn | +| Rule Name | Description | Severity | +| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| xgen-IPA-104-resource-has-GET | APIs must provide a Get method for resources. http://go/ipa/104 | warn | +| xgen-IPA-104-get-method-returns-single-resource | The purpose of the Get method is to return data from a single resource. http://go/ipa/104 | warn | +| xgen-IPA-104-get-method-response-code-is-200 | The Get method must return a 200 OK response. http://go/ipa/104 | warn | +| xgen-IPA-104-get-method-returns-response-suffixed-object | The Get method of a resource should return a "Response" suffixed object. http://go/ipa/104 | warn | +| xgen-IPA-104-get-method-response-has-no-input-fields | The Get method response object must not include writeOnly properties (fields that should be used only on creation or update, ie output fields). http://go/ipa/104 | warn | ### IPA-106 From 5d16680271deac9cca9d0deeda8a4bde6739c5cf Mon Sep 17 00:00:00 2001 From: Lovisa Berggren Date: Fri, 7 Mar 2025 17:00:41 +0000 Subject: [PATCH 3/3] CLOUDP-302981: Fix --- .../functions/operationIdFormatInflector.js | 75 ------------------- 1 file changed, 75 deletions(-) delete mode 100644 tools/spectral/ipa/rulesets/functions/operationIdFormatInflector.js diff --git a/tools/spectral/ipa/rulesets/functions/operationIdFormatInflector.js b/tools/spectral/ipa/rulesets/functions/operationIdFormatInflector.js deleted file mode 100644 index e14b859885..0000000000 --- a/tools/spectral/ipa/rulesets/functions/operationIdFormatInflector.js +++ /dev/null @@ -1,75 +0,0 @@ -import { getResourcePaths, isChild, isCustomMethod, isSingletonResource } from './utils/resourceEvaluation.js'; -import { - generateOperationIdForCustomMethod, - generateOperationIdForStandardMethod, -} from './utils/generateOperationId.js'; -import { singularize, pluralize } from 'ember-inflector'; - -const BASE_PATH = '/api/atlas/v2'; - -export default (input, _, { path, documentInventory }) => { - const operationId = input; - const oas = documentInventory.resolved; - const operationPath = path[1]; - const method = path[2]; - const resourcePaths = getResourcePaths(operationPath, Object.keys(oas.paths)); - - if (operationPath === BASE_PATH) { - const expectedOperationId = 'getSystemStatus'; - if (operationId !== expectedOperationId) { - return [ - { - message: `Invalid operation ID ${operationId}, please change to ${expectedOperationId}`, - }, - ]; - } - return; - } - - if (isCustomMethod(operationPath)) { - const expectedOperationId = generateOperationIdForCustomMethod(operationPath); - if (operationId !== expectedOperationId) { - return [ - { - message: `Invalid operation ID ${operationId}, please change to ${expectedOperationId}`, - }, - ]; - } - return; - } - - let expectedOperationId = ''; - switch (method) { - case 'get': - if (isChild(operationPath) || isSingletonResource(resourcePaths)) { - expectedOperationId = generateOperationIdForStandardMethod(operationPath, 'get'); - } else { - expectedOperationId = generateOperationIdForStandardMethod(operationPath, 'list'); - } - break; - case 'post': - expectedOperationId = generateOperationIdForStandardMethod(operationPath, 'create'); - break; - case 'patch': - expectedOperationId = generateOperationIdForStandardMethod(operationPath, 'update'); - break; - case 'put': - expectedOperationId = generateOperationIdForStandardMethod(operationPath, 'update'); - break; - case 'delete': - expectedOperationId = generateOperationIdForStandardMethod(operationPath, 'delete'); - break; - } - if (!expectedOperationId) { - console.error('Unsupported http method'); - return; - } - - if (operationId !== expectedOperationId) { - return [ - { - message: `Invalid operation ID ${operationId}, please change to ${expectedOperationId}`, - }, - ]; - } -};