From 84f0579a29d8b2308c8c591b6987104299592de2 Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 11 Mar 2025 15:23:04 +0000 Subject: [PATCH 01/10] CLOUDP-304053: IPA-106:Create - The resource must be the request body (implement deepObjectComparison without third party dependencies) --- package-lock.json | 14 ----- package.json | 2 - ...eateMethodRequestBodyIsGetResponse.test.js | 2 +- tools/spectral/ipa/rulesets/README.md | 10 +-- .../createMethodRequestBodyIsGetResponse.js | 16 ++--- .../rulesets/functions/utils/compareUtils.js | 63 +++++++++++++++++++ 6 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 tools/spectral/ipa/rulesets/functions/utils/compareUtils.js diff --git a/package-lock.json b/package-lock.json index ea0b6c6987..24879fd05d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,7 @@ "apache-arrow": "^19.0.1", "dotenv": "^16.4.7", "eslint-plugin-jest": "^28.10.0", - "lodash": "^4.17.21", "markdown-table": "^3.0.4", - "omit-deep-lodash": "^1.1.7", "openapi-to-postmanv2": "4.25.0", "parquet-wasm": "^0.6.1" }, @@ -9900,18 +9898,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/omit-deep-lodash": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/omit-deep-lodash/-/omit-deep-lodash-1.1.7.tgz", - "integrity": "sha512-9m9gleSMoxq3YO8aCq5pGUrqG9rKF0w/P70JHQ1ymjUQA/3+fVa2Stju9XORJKLmyLYEO3zzX40MJYaYl5Og4w==", - "license": "MIT", - "dependencies": { - "lodash": "~4.17.21" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/package.json b/package.json index f2869d9065..cc0ae52d21 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,7 @@ "apache-arrow": "^19.0.1", "dotenv": "^16.4.7", "eslint-plugin-jest": "^28.10.0", - "lodash": "^4.17.21", "markdown-table": "^3.0.4", - "omit-deep-lodash": "^1.1.7", "openapi-to-postmanv2": "4.25.0", "parquet-wasm": "^0.6.1" }, diff --git a/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js b/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js index 10927b9265..b320736887 100644 --- a/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js +++ b/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js @@ -99,7 +99,7 @@ testRule('xgen-IPA-106-create-method-request-body-is-get-method-response', [ content: { 'application/vnd.atlas.2023-01-01+json': { schema: { - $ref: '#/components/schemas/SchemaOne', + type: 'string', }, }, 'application/vnd.atlas.2024-01-01+json': { diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 60f7eb3756..d7dd8ba0f0 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -50,11 +50,11 @@ For rule definitions, see [IPA-105.yaml](https://github.com/mongodb/openapi/blob For rule definitions, see [IPA-106.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-106.yaml). -| Rule Name | Description | Severity | -| ------------------------------------------------------------------ | -------------------------------------------------------------------------------- | -------- | -| xgen-IPA-106-create-method-request-body-is-request-suffixed-object | The Create method request should be a Request suffixed object. http://go/ipa/106 | warn | -| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/xxx | warn | -| xgen-IPA-106-create-method-request-body-is-get-method-response | The Create method request should be a Get method response. http://go/ipa/106 | warn | +| Rule Name | Description | Severity | +| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| xgen-IPA-106-create-method-request-body-is-request-suffixed-object | The Create method request should be a Request suffixed object. http://go/ipa/106 | warn | +| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/xxx | warn | +| xgen-IPA-106-create-method-request-body-is-get-method-response | Request body content of the Create method and response content of the Get method should refer to the same resource. readOnly/writeOnly properties will be ignored. http://go/ipa/106 | warn | ### IPA-108 diff --git a/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js b/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js index 3ade0cfdb5..ca182e8d6b 100644 --- a/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js +++ b/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js @@ -1,7 +1,6 @@ import { getResponseOfGetMethodByMediaType, isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; -import { isEqual } from 'lodash'; -import omitDeep from 'omit-deep-lodash'; +import { isEqual, omitDeep } from './utils/compareUtils.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; @@ -9,7 +8,7 @@ const RULE_NAME = 'xgen-IPA-106-create-method-request-body-is-get-method-respons const ERROR_MESSAGE = 'The request body schema properties must match the response body schema properties of the Get method.'; -export default (input, _, { path, documentInventory }) => { +export default (input, opts, { path, documentInventory }) => { const oas = documentInventory.resolved; const resourcePath = path[1]; let mediaType = input; @@ -34,7 +33,8 @@ export default (input, _, { path, documentInventory }) => { const errors = checkViolationsAndReturnErrors( path, postMethodRequestContentPerMediaType, - getMethodResponseContentPerMediaType + getMethodResponseContentPerMediaType, + opts ); if (errors.length !== 0) { @@ -47,14 +47,16 @@ export default (input, _, { path, documentInventory }) => { function checkViolationsAndReturnErrors( path, postMethodRequestContentPerMediaType, - getMethodResponseContentPerMediaType + getMethodResponseContentPerMediaType, + opts ) { const errors = []; + const ignoredValues = opts?.ignoredValues || []; if ( !isEqual( - omitDeep(postMethodRequestContentPerMediaType.schema, 'readOnly', 'writeOnly'), - omitDeep(getMethodResponseContentPerMediaType.schema, 'readOnly', 'writeOnly') + omitDeep(postMethodRequestContentPerMediaType.schema, ...ignoredValues), + omitDeep(getMethodResponseContentPerMediaType.schema, ...ignoredValues) ) ) { errors.push({ diff --git a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js new file mode 100644 index 0000000000..783b4eb930 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js @@ -0,0 +1,63 @@ +// utils/compareUtils.js + +/** + * Deep equality check between two values + * @param {*} value1 First value to compare + * @param {*} value2 Second value to compare + * @returns {boolean} Whether the values are deeply equal + */ +export function isEqual(value1, value2) { + // If the values are strictly equal (including handling null/undefined) + if (value1 === value2) return true; + + // If either value is null or not an object, they're not equal (we already checked strict equality) + if (value1 == null || value2 == null || typeof value1 !== 'object' || typeof value2 !== 'object') { + return false; + } + + const keys1 = Object.keys(value1); + const keys2 = Object.keys(value2); + + // Different number of properties + if (keys1.length !== keys2.length) return false; + + // Check that all properties in value1 exist in value2 and are equal + for (const key of keys1) { + if (!keys2.includes(key)) return false; + + // Recursive equality check for nested objects + if (!isEqual(value1[key], value2[key])) return false; + } + + return true; +} + +/** + * Deep clone an object while omitting specific properties + * @param {object} obj Object to clone and omit properties from + * @param {...string} keys Properties to omit + * @returns {object} New object without the specified properties + */ +export function omitDeep(obj, ...keys) { + if (!obj || typeof obj !== 'object') return obj; + + // Handle arrays + if (Array.isArray(obj)) { + return obj.map((item) => omitDeep(item, ...keys)); + } + + // Handle regular objects + return Object.entries(obj).reduce((result, [key, value]) => { + // Skip properties that should be omitted + if (keys.includes(key)) return result; + + // Handle nested objects/arrays recursively + if (value && typeof value === 'object') { + result[key] = omitDeep(value, ...keys); + } else { + result[key] = value; + } + + return result; + }, {}); +} From 5a98e13b64353c451219deff226aed53c3c2f0a8 Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 11 Mar 2025 15:23:04 +0000 Subject: [PATCH 02/10] CLOUDP-304053: IPA-106:Create - The resource must be the request body (implement deepObjectComparison without third party dependencies) --- package-lock.json | 14 ----- package.json | 2 - ...eateMethodRequestBodyIsGetResponse.test.js | 2 +- tools/spectral/ipa/rulesets/README.md | 10 +-- .../createMethodRequestBodyIsGetResponse.js | 16 ++--- .../rulesets/functions/utils/compareUtils.js | 63 +++++++++++++++++++ 6 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 tools/spectral/ipa/rulesets/functions/utils/compareUtils.js diff --git a/package-lock.json b/package-lock.json index ea0b6c6987..24879fd05d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,7 @@ "apache-arrow": "^19.0.1", "dotenv": "^16.4.7", "eslint-plugin-jest": "^28.10.0", - "lodash": "^4.17.21", "markdown-table": "^3.0.4", - "omit-deep-lodash": "^1.1.7", "openapi-to-postmanv2": "4.25.0", "parquet-wasm": "^0.6.1" }, @@ -9900,18 +9898,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/omit-deep-lodash": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/omit-deep-lodash/-/omit-deep-lodash-1.1.7.tgz", - "integrity": "sha512-9m9gleSMoxq3YO8aCq5pGUrqG9rKF0w/P70JHQ1ymjUQA/3+fVa2Stju9XORJKLmyLYEO3zzX40MJYaYl5Og4w==", - "license": "MIT", - "dependencies": { - "lodash": "~4.17.21" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/package.json b/package.json index f2869d9065..cc0ae52d21 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,7 @@ "apache-arrow": "^19.0.1", "dotenv": "^16.4.7", "eslint-plugin-jest": "^28.10.0", - "lodash": "^4.17.21", "markdown-table": "^3.0.4", - "omit-deep-lodash": "^1.1.7", "openapi-to-postmanv2": "4.25.0", "parquet-wasm": "^0.6.1" }, diff --git a/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js b/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js index 10927b9265..b320736887 100644 --- a/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js +++ b/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js @@ -99,7 +99,7 @@ testRule('xgen-IPA-106-create-method-request-body-is-get-method-response', [ content: { 'application/vnd.atlas.2023-01-01+json': { schema: { - $ref: '#/components/schemas/SchemaOne', + type: 'string', }, }, 'application/vnd.atlas.2024-01-01+json': { diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 60f7eb3756..d7dd8ba0f0 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -50,11 +50,11 @@ For rule definitions, see [IPA-105.yaml](https://github.com/mongodb/openapi/blob For rule definitions, see [IPA-106.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-106.yaml). -| Rule Name | Description | Severity | -| ------------------------------------------------------------------ | -------------------------------------------------------------------------------- | -------- | -| xgen-IPA-106-create-method-request-body-is-request-suffixed-object | The Create method request should be a Request suffixed object. http://go/ipa/106 | warn | -| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/xxx | warn | -| xgen-IPA-106-create-method-request-body-is-get-method-response | The Create method request should be a Get method response. http://go/ipa/106 | warn | +| Rule Name | Description | Severity | +| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| xgen-IPA-106-create-method-request-body-is-request-suffixed-object | The Create method request should be a Request suffixed object. http://go/ipa/106 | warn | +| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/xxx | warn | +| xgen-IPA-106-create-method-request-body-is-get-method-response | Request body content of the Create method and response content of the Get method should refer to the same resource. readOnly/writeOnly properties will be ignored. http://go/ipa/106 | warn | ### IPA-108 diff --git a/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js b/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js index 3ade0cfdb5..ca182e8d6b 100644 --- a/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js +++ b/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js @@ -1,7 +1,6 @@ import { getResponseOfGetMethodByMediaType, isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; -import { isEqual } from 'lodash'; -import omitDeep from 'omit-deep-lodash'; +import { isEqual, omitDeep } from './utils/compareUtils.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; @@ -9,7 +8,7 @@ const RULE_NAME = 'xgen-IPA-106-create-method-request-body-is-get-method-respons const ERROR_MESSAGE = 'The request body schema properties must match the response body schema properties of the Get method.'; -export default (input, _, { path, documentInventory }) => { +export default (input, opts, { path, documentInventory }) => { const oas = documentInventory.resolved; const resourcePath = path[1]; let mediaType = input; @@ -34,7 +33,8 @@ export default (input, _, { path, documentInventory }) => { const errors = checkViolationsAndReturnErrors( path, postMethodRequestContentPerMediaType, - getMethodResponseContentPerMediaType + getMethodResponseContentPerMediaType, + opts ); if (errors.length !== 0) { @@ -47,14 +47,16 @@ export default (input, _, { path, documentInventory }) => { function checkViolationsAndReturnErrors( path, postMethodRequestContentPerMediaType, - getMethodResponseContentPerMediaType + getMethodResponseContentPerMediaType, + opts ) { const errors = []; + const ignoredValues = opts?.ignoredValues || []; if ( !isEqual( - omitDeep(postMethodRequestContentPerMediaType.schema, 'readOnly', 'writeOnly'), - omitDeep(getMethodResponseContentPerMediaType.schema, 'readOnly', 'writeOnly') + omitDeep(postMethodRequestContentPerMediaType.schema, ...ignoredValues), + omitDeep(getMethodResponseContentPerMediaType.schema, ...ignoredValues) ) ) { errors.push({ diff --git a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js new file mode 100644 index 0000000000..783b4eb930 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js @@ -0,0 +1,63 @@ +// utils/compareUtils.js + +/** + * Deep equality check between two values + * @param {*} value1 First value to compare + * @param {*} value2 Second value to compare + * @returns {boolean} Whether the values are deeply equal + */ +export function isEqual(value1, value2) { + // If the values are strictly equal (including handling null/undefined) + if (value1 === value2) return true; + + // If either value is null or not an object, they're not equal (we already checked strict equality) + if (value1 == null || value2 == null || typeof value1 !== 'object' || typeof value2 !== 'object') { + return false; + } + + const keys1 = Object.keys(value1); + const keys2 = Object.keys(value2); + + // Different number of properties + if (keys1.length !== keys2.length) return false; + + // Check that all properties in value1 exist in value2 and are equal + for (const key of keys1) { + if (!keys2.includes(key)) return false; + + // Recursive equality check for nested objects + if (!isEqual(value1[key], value2[key])) return false; + } + + return true; +} + +/** + * Deep clone an object while omitting specific properties + * @param {object} obj Object to clone and omit properties from + * @param {...string} keys Properties to omit + * @returns {object} New object without the specified properties + */ +export function omitDeep(obj, ...keys) { + if (!obj || typeof obj !== 'object') return obj; + + // Handle arrays + if (Array.isArray(obj)) { + return obj.map((item) => omitDeep(item, ...keys)); + } + + // Handle regular objects + return Object.entries(obj).reduce((result, [key, value]) => { + // Skip properties that should be omitted + if (keys.includes(key)) return result; + + // Handle nested objects/arrays recursively + if (value && typeof value === 'object') { + result[key] = omitDeep(value, ...keys); + } else { + result[key] = value; + } + + return result; + }, {}); +} From 39462eb477b7e7c45d487532f3b118af2521177d Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 11 Mar 2025 15:27:07 +0000 Subject: [PATCH 03/10] ipa docs fix --- tools/spectral/ipa/rulesets/IPA-106.yaml | 2 +- tools/spectral/ipa/rulesets/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/spectral/ipa/rulesets/IPA-106.yaml b/tools/spectral/ipa/rulesets/IPA-106.yaml index df3317e5cd..e0c7263d73 100644 --- a/tools/spectral/ipa/rulesets/IPA-106.yaml +++ b/tools/spectral/ipa/rulesets/IPA-106.yaml @@ -16,7 +16,7 @@ rules: field: '@key' function: 'createMethodRequestBodyIsRequestSuffixedObject' xgen-IPA-106-create-method-should-not-have-query-parameters: - description: 'Create operations should not use query parameters. http://go/ipa/xxx' + description: 'Create operations should not use query parameters. http://go/ipa/106' message: '{{error}} http://go/ipa/106' severity: warn given: '$.paths[*].post' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index d7dd8ba0f0..0d0897cff9 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -53,7 +53,7 @@ For rule definitions, see [IPA-106.yaml](https://github.com/mongodb/openapi/blob | Rule Name | Description | Severity | | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | xgen-IPA-106-create-method-request-body-is-request-suffixed-object | The Create method request should be a Request suffixed object. http://go/ipa/106 | warn | -| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/xxx | warn | +| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/106 | warn | | xgen-IPA-106-create-method-request-body-is-get-method-response | Request body content of the Create method and response content of the Get method should refer to the same resource. readOnly/writeOnly properties will be ignored. http://go/ipa/106 | warn | ### IPA-108 From 2390d68b0702d28c24df67f2c48c6974a1afe1d0 Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 11 Mar 2025 15:32:06 +0000 Subject: [PATCH 04/10] ipa docs fix --- tools/spectral/ipa/rulesets/IPA-106.yaml | 5 +++-- tools/spectral/ipa/rulesets/README.md | 12 +++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tools/spectral/ipa/rulesets/IPA-106.yaml b/tools/spectral/ipa/rulesets/IPA-106.yaml index e0c7263d73..97c8b0beef 100644 --- a/tools/spectral/ipa/rulesets/IPA-106.yaml +++ b/tools/spectral/ipa/rulesets/IPA-106.yaml @@ -23,8 +23,9 @@ rules: then: function: 'createMethodShouldNotHaveQueryParameters' xgen-IPA-106-create-method-request-body-is-get-method-response: - description: 'Request body content of the Create method and response content of the Get method should refer to the same resource. - readOnly/writeOnly properties will be ignored. http://go/ipa/106' + description: | + Request body content of the Create method and response content of the Get method should refer to the same resource. + readOnly/writeOnly properties will be ignored. http://go/ipa/106 message: '{{error}} http://go/ipa/106' severity: warn given: '$.paths[*].post.requestBody.content' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 0d0897cff9..f1533e19a8 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -50,11 +50,11 @@ For rule definitions, see [IPA-105.yaml](https://github.com/mongodb/openapi/blob For rule definitions, see [IPA-106.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-106.yaml). -| Rule Name | Description | Severity | -| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| xgen-IPA-106-create-method-request-body-is-request-suffixed-object | The Create method request should be a Request suffixed object. http://go/ipa/106 | warn | -| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/106 | warn | -| xgen-IPA-106-create-method-request-body-is-get-method-response | Request body content of the Create method and response content of the Get method should refer to the same resource. readOnly/writeOnly properties will be ignored. http://go/ipa/106 | warn | +| Rule Name | Description | Severity | +| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | +| xgen-IPA-106-create-method-request-body-is-request-suffixed-object | The Create method request should be a Request suffixed object. http://go/ipa/106 | warn | +| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/106 | warn | +| xgen-IPA-106-create-method-request-body-is-get-method-response | Request body content of the Create method and response content of the Get method should refer to the same resource. readOnly/writeOnly properties will be ignored. http://go/ipa/106 | warn | ### IPA-108 @@ -91,5 +91,3 @@ For rule definitions, see [IPA-123.yaml](https://github.com/mongodb/openapi/blob | Rule Name | Description | Severity | | ------------------------------------------------- | ------------------------------------------------------- | -------- | | xgen-IPA-123-enum-values-must-be-upper-snake-case | Enum values must be UPPER_SNAKE_CASE. http://go/ipa/123 | error | - - From 7b04e62bf6c5abeaf61fc79c0319e44f318a95f7 Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 11 Mar 2025 15:33:41 +0000 Subject: [PATCH 05/10] ipa docs fix --- tools/spectral/ipa/rulesets/README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index f1533e19a8..d9e4b25504 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -50,11 +50,13 @@ For rule definitions, see [IPA-105.yaml](https://github.com/mongodb/openapi/blob For rule definitions, see [IPA-106.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-106.yaml). -| Rule Name | Description | Severity | -| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | -| xgen-IPA-106-create-method-request-body-is-request-suffixed-object | The Create method request should be a Request suffixed object. http://go/ipa/106 | warn | -| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/106 | warn | -| xgen-IPA-106-create-method-request-body-is-get-method-response | Request body content of the Create method and response content of the Get method should refer to the same resource. readOnly/writeOnly properties will be ignored. http://go/ipa/106 | warn | +| Rule Name | Description | Severity | +| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| xgen-IPA-106-create-method-request-body-is-request-suffixed-object | The Create method request should be a Request suffixed object. http://go/ipa/106 | warn | +| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/106 | warn | +| xgen-IPA-106-create-method-request-body-is-get-method-response | Request body content of the Create method and response content of the Get method should refer to the same resource. +readOnly/writeOnly properties will be ignored. http://go/ipa/106 + | warn | ### IPA-108 @@ -91,3 +93,5 @@ For rule definitions, see [IPA-123.yaml](https://github.com/mongodb/openapi/blob | Rule Name | Description | Severity | | ------------------------------------------------- | ------------------------------------------------------- | -------- | | xgen-IPA-123-enum-values-must-be-upper-snake-case | Enum values must be UPPER_SNAKE_CASE. http://go/ipa/123 | error | + + From 51ceddebeafe20f156614d5071bb744e14c07d9a Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 11 Mar 2025 15:39:13 +0000 Subject: [PATCH 06/10] address the comments --- .../functions/createMethodRequestBodyIsGetResponse.js | 4 ++-- tools/spectral/ipa/rulesets/functions/utils/compareUtils.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js b/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js index ca182e8d6b..7fae75826f 100644 --- a/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js +++ b/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js @@ -1,6 +1,6 @@ import { getResponseOfGetMethodByMediaType, isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; -import { isEqual, omitDeep } from './utils/compareUtils.js'; +import { isDeepEqual, omitDeep } from './utils/compareUtils.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; @@ -54,7 +54,7 @@ function checkViolationsAndReturnErrors( const ignoredValues = opts?.ignoredValues || []; if ( - !isEqual( + !isDeepEqual( omitDeep(postMethodRequestContentPerMediaType.schema, ...ignoredValues), omitDeep(getMethodResponseContentPerMediaType.schema, ...ignoredValues) ) diff --git a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js index 783b4eb930..e641feb61f 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js @@ -6,7 +6,7 @@ * @param {*} value2 Second value to compare * @returns {boolean} Whether the values are deeply equal */ -export function isEqual(value1, value2) { +export function isDeepEqual(value1, value2) { // If the values are strictly equal (including handling null/undefined) if (value1 === value2) return true; @@ -26,7 +26,7 @@ export function isEqual(value1, value2) { if (!keys2.includes(key)) return false; // Recursive equality check for nested objects - if (!isEqual(value1[key], value2[key])) return false; + if (!isDeepEqual(value1[key], value2[key])) return false; } return true; From b14115c80dc2735b1e3b3d605060589867d6f906 Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 11 Mar 2025 15:44:01 +0000 Subject: [PATCH 07/10] address the comments --- tools/spectral/ipa/rulesets/functions/utils/compareUtils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js index e641feb61f..57d20ee327 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js @@ -2,6 +2,7 @@ /** * Deep equality check between two values + * Does not handle circular references * @param {*} value1 First value to compare * @param {*} value2 Second value to compare * @returns {boolean} Whether the values are deeply equal From fb66eb4800e6d08a3c215b0700c34f642e125d7d Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 11 Mar 2025 16:20:51 +0000 Subject: [PATCH 08/10] address the comments --- .../ipa/__tests__/utils/compareUtils.test.js | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 tools/spectral/ipa/__tests__/utils/compareUtils.test.js diff --git a/tools/spectral/ipa/__tests__/utils/compareUtils.test.js b/tools/spectral/ipa/__tests__/utils/compareUtils.test.js new file mode 100644 index 0000000000..cc72f76d6c --- /dev/null +++ b/tools/spectral/ipa/__tests__/utils/compareUtils.test.js @@ -0,0 +1,209 @@ +// compareUtils.it.js +import { describe, expect, it } from '@jest/globals'; +import { isDeepEqual, omitDeep } from './compareUtils.js'; + +describe('isDeepEqual', () => { + it('handles primitive values', () => { + expect(isDeepEqual(1, 1)).toBe(true); + expect(isDeepEqual('hello', 'hello')).toBe(true); + expect(isDeepEqual(true, true)).toBe(true); + expect(isDeepEqual(null, null)).toBe(true); + expect(isDeepEqual(undefined, undefined)).toBe(true); + + expect(isDeepEqual(1, 2)).toBe(false); + expect(isDeepEqual('hello', 'world')).toBe(false); + expect(isDeepEqual(true, false)).toBe(false); + expect(isDeepEqual(null, undefined)).toBe(false); + expect(isDeepEqual(1, '1')).toBe(false); + }); + + it('handles simple objects', () => { + expect(isDeepEqual({}, {})).toBe(true); + expect(isDeepEqual({ a: 1 }, { a: 1 })).toBe(true); + expect(isDeepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true); + + expect(isDeepEqual({ a: 1 }, { a: 2 })).toBe(false); + expect(isDeepEqual({ a: 1 }, { b: 1 })).toBe(false); + expect(isDeepEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false); + }); + + it('handles arrays', () => { + expect(isDeepEqual([], [])).toBe(true); + expect(isDeepEqual([1, 2], [1, 2])).toBe(true); + + expect(isDeepEqual([1, 2], [2, 1])).toBe(false); + expect(isDeepEqual([1, 2], [1, 2, 3])).toBe(false); + }); + + it('handles nested objects', () => { + expect(isDeepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })).toBe(true); + + expect(isDeepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 3 } })).toBe(false); + + expect(isDeepEqual({ a: 1, b: { c: 2, d: 3 } }, { a: 1, b: { c: 2 } })).toBe(false); + }); + + it('handles nested arrays', () => { + expect(isDeepEqual({ a: [1, 2, { b: 3 }] }, { a: [1, 2, { b: 3 }] })).toBe(true); + + expect(isDeepEqual({ a: [1, 2, { b: 3 }] }, { a: [1, 2, { b: 4 }] })).toBe(false); + }); + + it('handles mixed types', () => { + expect(isDeepEqual({ a: 1 }, [1])).toBe(false); + expect(isDeepEqual({ a: 1 }, null)).toBe(false); + expect(isDeepEqual(null, { a: 1 })).toBe(false); + }); +}); + +describe('omitDeep', () => { + it('handles primitives', () => { + expect(omitDeep(1, 'any')).toBe(1); + expect(omitDeep('hello', 'any')).toBe('hello'); + expect(omitDeep(null, 'any')).toBe(null); + expect(omitDeep(undefined, 'any')).toBe(undefined); + }); + + it('handles shallow objects', () => { + expect(omitDeep({ a: 1, b: 2 }, 'a')).toEqual({ b: 2 }); + expect(omitDeep({ a: 1, b: 2 }, 'c')).toEqual({ a: 1, b: 2 }); + expect(omitDeep({ a: 1, b: 2 }, 'a', 'b')).toEqual({}); + }); + + it('handles arrays', () => { + expect( + omitDeep( + [ + { a: 1, b: 2 }, + { a: 3, b: 4 }, + ], + 'a' + ) + ).toEqual([{ b: 2 }, { b: 4 }]); + }); + + it('handles nested objects', () => { + const input = { + a: 1, + b: { + c: 2, + d: 3, + e: { + f: 4, + g: 5, + }, + }, + h: 6, + }; + + const expected = { + a: 1, + b: { + d: 3, + e: { + g: 5, + }, + }, + h: 6, + }; + + expect(omitDeep(input, 'c', 'f')).toEqual(expected); + }); + + it('handles deeply nested arrays', () => { + const input = { + items: [ + { id: 1, name: 'item1', metadata: { created: '2023', readOnly: true } }, + { id: 2, name: 'item2', metadata: { created: '2023', readOnly: true } }, + ], + }; + + const expected = { + items: [ + { id: 1, name: 'item1', metadata: { created: '2023' } }, + { id: 2, name: 'item2', metadata: { created: '2023' } }, + ], + }; + + expect(omitDeep(input, 'readOnly')).toEqual(expected); + }); + + it('handles complex schemas', () => { + const schema = { + type: 'object', + properties: { + name: { type: 'string' }, + id: { type: 'string', readOnly: true }, + details: { + type: 'object', + properties: { + createdAt: { type: 'string', readOnly: true }, + description: { type: 'string' }, + }, + }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + itemId: { type: 'string', readOnly: true }, + itemName: { type: 'string' }, + }, + }, + }, + }, + }; + + const expected = { + type: 'object', + properties: { + name: { type: 'string' }, + id: { type: 'string' }, + details: { + type: 'object', + properties: { + createdAt: { type: 'string' }, + description: { type: 'string' }, + }, + }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + itemId: { type: 'string' }, + itemName: { type: 'string' }, + }, + }, + }, + }, + }; + + expect(omitDeep(schema, 'readOnly')).toEqual(expected); + }); + + it('handles multiple keys to omit', () => { + const input = { + a: 1, + b: 2, + c: { + d: 3, + e: 4, + f: { + g: 5, + h: 6, + }, + }, + }; + + expect(omitDeep(input, 'a', 'e', 'g')).toEqual({ + b: 2, + c: { + d: 3, + f: { + h: 6, + }, + }, + }); + }); +}); From 72113377ff81d3112dafbf6ae5442c362db32ac1 Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 11 Mar 2025 16:21:35 +0000 Subject: [PATCH 09/10] import fix --- tools/spectral/ipa/__tests__/utils/compareUtils.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/spectral/ipa/__tests__/utils/compareUtils.test.js b/tools/spectral/ipa/__tests__/utils/compareUtils.test.js index cc72f76d6c..5bb71e4b3f 100644 --- a/tools/spectral/ipa/__tests__/utils/compareUtils.test.js +++ b/tools/spectral/ipa/__tests__/utils/compareUtils.test.js @@ -1,6 +1,6 @@ // compareUtils.it.js import { describe, expect, it } from '@jest/globals'; -import { isDeepEqual, omitDeep } from './compareUtils.js'; +import { isDeepEqual, omitDeep } from '../../rulesets/functions/utils/compareUtils'; describe('isDeepEqual', () => { it('handles primitive values', () => { From 63fd0137a1d7043c142c92ee2433b92f970fee6e Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 11 Mar 2025 16:21:52 +0000 Subject: [PATCH 10/10] import fix --- tools/spectral/ipa/__tests__/utils/compareUtils.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/spectral/ipa/__tests__/utils/compareUtils.test.js b/tools/spectral/ipa/__tests__/utils/compareUtils.test.js index 5bb71e4b3f..3b722616a1 100644 --- a/tools/spectral/ipa/__tests__/utils/compareUtils.test.js +++ b/tools/spectral/ipa/__tests__/utils/compareUtils.test.js @@ -1,4 +1,3 @@ -// compareUtils.it.js import { describe, expect, it } from '@jest/globals'; import { isDeepEqual, omitDeep } from '../../rulesets/functions/utils/compareUtils';