diff --git a/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js b/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js index 520493d295..640e55a4ac 100644 --- a/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js +++ b/tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js @@ -6,12 +6,15 @@ const componentSchemas = { SchemaOne: { type: 'string', }, - SchemaTwo: { + SchemaTwoRequest: { type: 'object', properties: { name: { type: 'string', - readOnly: true, + writeOnly: true, + }, + otherThing: { + type: 'string', }, }, }, @@ -29,12 +32,15 @@ const componentSchemas = { }, }, }, - SchemaFour: { + SchemaTwoResponse: { type: 'object', properties: { name: { type: 'string', - writeOnly: true, + readOnly: true, + }, + otherThing: { + type: 'string', }, }, }, @@ -132,12 +138,12 @@ testRule('xgen-IPA-106-create-method-request-body-is-get-method-response', [ content: { 'application/vnd.atlas.2023-01-01+json': { schema: { - $ref: '#/components/schemas/SchemaTwo', + $ref: '#/components/schemas/SchemaTwoRequest', }, }, 'application/vnd.atlas.2024-01-01+json': { schema: { - $ref: '#/components/schemas/SchemaTwo', + $ref: '#/components/schemas/SchemaTwoRequest', }, }, }, @@ -151,7 +157,12 @@ testRule('xgen-IPA-106-create-method-request-body-is-get-method-response', [ content: { 'application/vnd.atlas.2023-01-01+json': { schema: { - $ref: '#/components/schemas/SchemaFour', + $ref: '#/components/schemas/SchemaTwoResponse', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaTwoResponse', }, }, }, @@ -244,7 +255,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/SchemaTwo', + $ref: '#/components/schemas/SchemaTwoRequest', }, }, }, @@ -258,12 +269,12 @@ testRule('xgen-IPA-106-create-method-request-body-is-get-method-response', [ content: { 'application/vnd.atlas.2023-01-01+json': { schema: { - $ref: '#/components/schemas/SchemaTwo', + $ref: '#/components/schemas/SchemaTwoResponse', }, }, 'application/vnd.atlas.2024-01-01+json': { schema: { - $ref: '#/components/schemas/SchemaTwo', + $ref: '#/components/schemas/SchemaTwoResponse', }, }, }, @@ -382,56 +393,56 @@ testRule('xgen-IPA-106-create-method-request-body-is-get-method-response', [ { code: 'xgen-IPA-106-create-method-request-body-is-get-method-response', message: - 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa/106', + 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa-spectral#IPA-106', 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-get-method-response', message: - 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa/106', + 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa-spectral#IPA-106', 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-get-method-response', message: - 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa/106', + 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa-spectral#IPA-106', 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-get-method-response', message: - 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa/106', + 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa-spectral#IPA-106', 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-get-method-response', message: - 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa/106', + 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa-spectral#IPA-106', path: ['paths', '/resourceThree', 'post', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-request-body-is-get-method-response', message: - 'Could not validate that the Create request body schema matches the response schema of the Get method. The Get method does not have a schema. http://go/ipa/106', + 'Could not validate that the Create request body schema matches the response schema of the Get method. The Get method does not have a schema. http://go/ipa-spectral#IPA-106', path: ['paths', '/resourceFour', 'post', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-request-body-is-get-method-response', message: - 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa/106', + 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa-spectral#IPA-106', path: ['paths', '/resourceCircular', 'post', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-request-body-is-get-method-response', message: - 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa/106', + 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa-spectral#IPA-106', path: ['paths', '/resourceCircular', 'post', 'requestBody', 'content', 'application/vnd.atlas.2024-01-01+json'], severity: DiagnosticSeverity.Warning, }, @@ -481,14 +492,14 @@ testRule('xgen-IPA-106-create-method-request-body-is-get-method-response', [ { code: 'xgen-IPA-106-create-method-request-body-is-get-method-response', message: - 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa/106', + 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa-spectral#IPA-106', path: ['paths', '/animalResource', 'post', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-request-body-is-get-method-response', message: - 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa/106', + 'The request body schema properties must match the response body schema properties of the Get method. http://go/ipa-spectral#IPA-106', path: ['paths', '/animalResource', 'post', 'requestBody', 'content', 'application/vnd.atlas.2024-01-01+json'], severity: DiagnosticSeverity.Warning, }, @@ -511,14 +522,6 @@ testRule('xgen-IPA-106-create-method-request-body-is-get-method-response', [ 'xgen-IPA-106-create-method-request-body-is-get-method-response': 'reason', }, }, - 'application/vnd.atlas.2024-01-01+json': { - schema: { - $ref: '#/components/schemas/SchemaOne', - }, - 'x-xgen-IPA-exception': { - 'xgen-IPA-106-create-method-request-body-is-get-method-response': 'reason', - }, - }, }, }, }, @@ -530,7 +533,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/SchemaTwo', + $ref: '#/components/schemas/SchemaThree', }, }, }, @@ -544,7 +547,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/SchemaTwo', + $ref: '#/components/schemas/SchemaTwoRequest', }, 'x-xgen-IPA-exception': { 'xgen-IPA-106-create-method-request-body-is-get-method-response': 'reason', @@ -552,7 +555,7 @@ testRule('xgen-IPA-106-create-method-request-body-is-get-method-response', [ }, 'application/vnd.atlas.2024-01-01+json': { schema: { - $ref: '#/components/schemas/SchemaTwo', + $ref: '#/components/schemas/SchemaTwoRequest', }, 'x-xgen-IPA-exception': { 'xgen-IPA-106-create-method-request-body-is-get-method-response': 'reason', @@ -569,43 +572,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/SchemaThree', - }, - }, - }, - }, - }, - }, - }, - '/resourceThree': { - post: { - requestBody: { - content: { - 'application/vnd.atlas.2023-01-01+json': { - schema: { - $ref: '#/components/schemas/SchemaOne', - }, - 'x-xgen-IPA-exception': { - 'xgen-IPA-106-create-method-request-body-is-get-method-response': 'reason', - }, - }, - 'application/vnd.atlas.2024-01-01+json': { - schema: { - $ref: '#/components/schemas/SchemaThree', - }, - }, - }, - }, - }, - }, - '/resourceThree/{id}': { - get: { - responses: { - 200: { - content: { - 'application/vnd.atlas.2023-01-01+json': { - schema: { - $ref: '#/components/schemas/SchemaThree', + $ref: '#/components/schemas/SchemaTwoResponse', }, }, }, diff --git a/tools/spectral/ipa/__tests__/createMethodRequestBodyIsRequestSuffixedObject.test.js b/tools/spectral/ipa/__tests__/createMethodRequestBodyIsRequestSuffixedObject.test.js index 6f6164d26f..fa73dba7f7 100644 --- a/tools/spectral/ipa/__tests__/createMethodRequestBodyIsRequestSuffixedObject.test.js +++ b/tools/spectral/ipa/__tests__/createMethodRequestBodyIsRequestSuffixedObject.test.js @@ -154,31 +154,36 @@ 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. http://go/ipa/106', + message: + 'The response body schema must reference a schema with a Request suffix. http://go/ipa-spectral#IPA-106', 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. http://go/ipa/106', + message: + 'The response body schema must reference a schema with a Request suffix. http://go/ipa-spectral#IPA-106', 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. http://go/ipa/106', + message: + 'The response body schema must reference a schema with a Request suffix. http://go/ipa-spectral#IPA-106', 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. http://go/ipa/106', + message: + 'The response body schema must reference a schema with a Request suffix. http://go/ipa-spectral#IPA-106', 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. http://go/ipa/106', + message: + 'The response body schema is defined inline and must reference a predefined schema. http://go/ipa-spectral#IPA-106', path: ['paths', '/resourceThree', 'post', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], severity: DiagnosticSeverity.Warning, }, diff --git a/tools/spectral/ipa/__tests__/createMethodRequestHasNoReadonlyFields.test.js b/tools/spectral/ipa/__tests__/createMethodRequestHasNoReadonlyFields.test.js index 7e73746b7e..58c98dd8af 100644 --- a/tools/spectral/ipa/__tests__/createMethodRequestHasNoReadonlyFields.test.js +++ b/tools/spectral/ipa/__tests__/createMethodRequestHasNoReadonlyFields.test.js @@ -152,14 +152,14 @@ testRule('xgen-IPA-106-create-method-request-has-no-readonly-fields', [ { code: 'xgen-IPA-106-create-method-request-has-no-readonly-fields', message: - 'The Create method request object must not include input fields (readOnly properties). Found readOnly property at: id. http://go/ipa/106', + 'The Create method request object must not include input fields (readOnly properties). Found readOnly property at: id. http://go/ipa-spectral#IPA-106', path: ['paths', '/resource', 'post', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-request-has-no-readonly-fields', message: - 'The Create method request object must not include input fields (readOnly properties). Found readOnly property at one of the inline schemas. http://go/ipa/106', + 'The Create method request object must not include input fields (readOnly properties). Found readOnly property at one of the inline schemas. http://go/ipa-spectral#IPA-106', path: ['paths', '/resource', 'post', 'requestBody', 'content', 'application/vnd.atlas.2024-01-01+json'], severity: DiagnosticSeverity.Warning, }, @@ -192,7 +192,7 @@ testRule('xgen-IPA-106-create-method-request-has-no-readonly-fields', [ { code: 'xgen-IPA-106-create-method-request-has-no-readonly-fields', message: - 'The Create method request object must not include input fields (readOnly properties). Found readOnly property at: user.userId. http://go/ipa/106', + 'The Create method request object must not include input fields (readOnly properties). Found readOnly property at: user.userId. http://go/ipa-spectral#IPA-106', path: ['paths', '/resource', 'post', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], severity: DiagnosticSeverity.Warning, }, @@ -225,7 +225,7 @@ testRule('xgen-IPA-106-create-method-request-has-no-readonly-fields', [ { code: 'xgen-IPA-106-create-method-request-has-no-readonly-fields', message: - 'The Create method request object must not include input fields (readOnly properties). Found readOnly property at: items.items.itemId. http://go/ipa/106', + 'The Create method request object must not include input fields (readOnly properties). Found readOnly property at: items.items.itemId. http://go/ipa-spectral#IPA-106', path: ['paths', '/resource', 'post', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], severity: DiagnosticSeverity.Warning, }, diff --git a/tools/spectral/ipa/__tests__/createMethodResponseCodeIs201Created.test.js b/tools/spectral/ipa/__tests__/createMethodResponseCodeIs201Created.test.js index 2cfc382847..34d21ff71f 100644 --- a/tools/spectral/ipa/__tests__/createMethodResponseCodeIs201Created.test.js +++ b/tools/spectral/ipa/__tests__/createMethodResponseCodeIs201Created.test.js @@ -74,21 +74,21 @@ testRule('xgen-IPA-106-create-method-response-code-is-201', [ { code: 'xgen-IPA-106-create-method-response-code-is-201', message: - 'The Create method must return a 201 Created response. This method either lacks a 201 Created response or defines a different 2xx status code. http://go/ipa/106', + 'The Create method must return a 201 Created response. This method either lacks a 201 Created response or defines a different 2xx status code. http://go/ipa-spectral#IPA-106', path: ['paths', '/resourceOne', 'post'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-response-code-is-201', message: - 'The Create method must return a 201 Created response. This method either lacks a 201 Created response or defines a different 2xx status code. http://go/ipa/106', + 'The Create method must return a 201 Created response. This method either lacks a 201 Created response or defines a different 2xx status code. http://go/ipa-spectral#IPA-106', path: ['paths', '/resourceTwo', 'post'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-response-code-is-201', message: - 'The Create method must return a 201 Created response. This method either lacks a 201 Created response or defines a different 2xx status code. http://go/ipa/106', + 'The Create method must return a 201 Created response. This method either lacks a 201 Created response or defines a different 2xx status code. http://go/ipa-spectral#IPA-106', path: ['paths', '/resourceThree', 'post'], severity: DiagnosticSeverity.Warning, }, diff --git a/tools/spectral/ipa/__tests__/createMethodShouldNotHaveQueryParameters.test.js b/tools/spectral/ipa/__tests__/createMethodShouldNotHaveQueryParameters.test.js index 59ce2f3081..8346a7a4b8 100644 --- a/tools/spectral/ipa/__tests__/createMethodShouldNotHaveQueryParameters.test.js +++ b/tools/spectral/ipa/__tests__/createMethodShouldNotHaveQueryParameters.test.js @@ -120,19 +120,21 @@ testRule('xgen-IPA-106-create-method-should-not-have-query-parameters', [ errors: [ { code: 'xgen-IPA-106-create-method-should-not-have-query-parameters', - message: 'Create operations should not have query parameters. Found [filter]. http://go/ipa/106', + message: 'Create operations should not have query parameters. Found [filter]. http://go/ipa-spectral#IPA-106', path: ['paths', '/resource', 'post'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-should-not-have-query-parameters', - message: 'Create operations should not have query parameters. Found [query-param]. http://go/ipa/106', + message: + 'Create operations should not have query parameters. Found [query-param]. http://go/ipa-spectral#IPA-106', path: ['paths', '/resourceTwo', 'post'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-should-not-have-query-parameters', - message: 'Create operations should not have query parameters. Found [query-param-2]. http://go/ipa/106', + message: + 'Create operations should not have query parameters. Found [query-param-2]. http://go/ipa-spectral#IPA-106', path: ['paths', '/resourceTwo', 'post'], severity: DiagnosticSeverity.Warning, }, diff --git a/tools/spectral/ipa/__tests__/utils/compareUtils.test.js b/tools/spectral/ipa/__tests__/utils/compareUtils.test.js index 3b722616a1..8cb9b5d091 100644 --- a/tools/spectral/ipa/__tests__/utils/compareUtils.test.js +++ b/tools/spectral/ipa/__tests__/utils/compareUtils.test.js @@ -1,5 +1,11 @@ import { describe, expect, it } from '@jest/globals'; -import { isDeepEqual, omitDeep } from '../../rulesets/functions/utils/compareUtils'; +import { + isDeepEqual, + omitDeep, + removePropertyByFlag, + removeReadOnlyProperties, + removeWriteOnlyProperties, +} from '../../rulesets/functions/utils/compareUtils'; describe('isDeepEqual', () => { it('handles primitive values', () => { @@ -55,84 +61,152 @@ describe('isDeepEqual', () => { }); }); -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); +describe('removePropertyByFlag', () => { + it('handles primitive values', () => { + expect(removePropertyByFlag(1, 'readOnly')).toBe(1); + expect(removePropertyByFlag('hello', 'readOnly')).toBe('hello'); + expect(removePropertyByFlag(true, 'readOnly')).toBe(true); + expect(removePropertyByFlag(null, 'readOnly')).toBe(null); + expect(removePropertyByFlag(undefined, 'readOnly')).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 empty objects', () => { + expect(removePropertyByFlag({}, 'readOnly')).toEqual({}); }); - it('handles arrays', () => { - expect( - omitDeep( - [ - { a: 1, b: 2 }, - { a: 3, b: 4 }, - ], - 'a' - ) - ).toEqual([{ b: 2 }, { b: 4 }]); + it('removes properties with flagged attributes', () => { + const input = { + type: 'object', + properties: { + id: { type: 'string', readOnly: true }, + name: { type: 'string' }, + password: { type: 'string', test: true }, + }, + }; + + const expectedReadOnly = { + type: 'object', + properties: { + name: { type: 'string' }, + password: { type: 'string', test: true }, + }, + }; + + const expectedTest = { + type: 'object', + properties: { + id: { type: 'string', readOnly: true }, + name: { type: 'string' }, + }, + }; + + expect(removePropertyByFlag(input, 'readOnly')).toEqual(expectedReadOnly); + expect(removePropertyByFlag(input, 'test')).toEqual(expectedTest); }); - it('handles nested objects', () => { + it('processes nested objects', () => { const input = { - a: 1, - b: { - c: 2, - d: 3, - e: { - f: 4, - g: 5, + type: 'object', + properties: { + user: { + type: 'object', + properties: { + id: { type: 'string', readOnly: true }, + name: { type: 'string' }, + metadata: { + type: 'object', + properties: { + createdAt: { type: 'string', readOnly: true }, + updatedAt: { type: 'string', readOnly: true }, + notes: { type: 'string' }, + }, + }, + }, }, }, - h: 6, }; const expected = { - a: 1, - b: { - d: 3, - e: { - g: 5, + type: 'object', + properties: { + user: { + type: 'object', + properties: { + name: { type: 'string' }, + metadata: { + type: 'object', + properties: { + notes: { type: 'string' }, + }, + }, + }, }, }, - h: 6, }; - expect(omitDeep(input, 'c', 'f')).toEqual(expected); + expect(removePropertyByFlag(input, 'readOnly')).toEqual(expected); }); - it('handles deeply nested arrays', () => { + it('processes arrays', () => { const input = { - items: [ - { id: 1, name: 'item1', metadata: { created: '2023', readOnly: true } }, - { id: 2, name: 'item2', metadata: { created: '2023', readOnly: true } }, - ], + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string', readOnly: true }, + name: { type: 'string' }, + }, + }, }; const expected = { - items: [ - { id: 1, name: 'item1', metadata: { created: '2023' } }, - { id: 2, name: 'item2', metadata: { created: '2023' } }, - ], + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }, }; - expect(omitDeep(input, 'readOnly')).toEqual(expected); + expect(removePropertyByFlag(input, 'readOnly')).toEqual(expected); }); - it('handles complex schemas', () => { - const schema = { + it('handles array of objects', () => { + const input = [ + { id: 1, readOnly: true }, + { id: 2, name: 'test' }, + { + id: 3, + properties: { + secret: { type: 'string', readOnly: true }, + visible: { type: 'string' }, + }, + }, + ]; + + const expected = [ + { id: 1 }, + { id: 2, name: 'test' }, + { + id: 3, + properties: { + visible: { type: 'string' }, + }, + }, + ]; + + expect(removePropertyByFlag(input, 'readOnly')).toEqual(expected); + }); +}); + +describe('removeReadOnlyProperties', () => { + it('removes readOnly properties from schema', () => { + const input = { type: 'object', properties: { - name: { type: 'string' }, id: { type: 'string', readOnly: true }, + name: { type: 'string' }, details: { type: 'object', properties: { @@ -140,16 +214,6 @@ describe('omitDeep', () => { description: { type: 'string' }, }, }, - items: { - type: 'array', - items: { - type: 'object', - properties: { - itemId: { type: 'string', readOnly: true }, - itemName: { type: 'string' }, - }, - }, - }, }, }; @@ -157,52 +221,96 @@ describe('omitDeep', () => { 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); + expect(removeReadOnlyProperties(input)).toEqual(expected); }); +}); - it('handles multiple keys to omit', () => { +describe('removeWriteOnlyProperties', () => { + it('removes writeOnly properties from schema', () => { const input = { - a: 1, - b: 2, - c: { - d: 3, - e: 4, - f: { - g: 5, - h: 6, + type: 'object', + properties: { + id: { type: 'string' }, + password: { type: 'string', writeOnly: true }, + details: { + type: 'object', + properties: { + secretKey: { type: 'string', writeOnly: true }, + description: { type: 'string' }, + }, }, }, }; - expect(omitDeep(input, 'a', 'e', 'g')).toEqual({ - b: 2, - c: { - d: 3, - f: { - h: 6, + const expected = { + type: 'object', + properties: { + id: { type: 'string' }, + details: { + type: 'object', + properties: { + description: { type: 'string' }, + }, }, }, + }; + + expect(removeWriteOnlyProperties(input)).toEqual(expected); + }); +}); + +describe('schema compatibility use cases', () => { + it('request and response schema comparison', () => { + const requestSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + password: { type: 'string', writeOnly: true }, + }, + required: ['name', 'email', 'password'], + }; + + const responseSchema = { + type: 'object', + properties: { + id: { type: 'string', readOnly: true }, + name: { type: 'string' }, + email: { type: 'string' }, + createdAt: { type: 'string', readOnly: true }, + }, + required: ['id', 'name', 'email', 'createdAt'], + }; + + const filteredRequest = removeWriteOnlyProperties(requestSchema); + const filteredResponse = removeReadOnlyProperties(responseSchema); + + // Verify filtered schemas + expect(filteredRequest).toEqual({ + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + }, + required: ['name', 'email', 'password'], + }); + + expect(filteredResponse).toEqual({ + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + }, + required: ['id', 'name', 'email', 'createdAt'], }); }); }); diff --git a/tools/spectral/ipa/rulesets/IPA-106.yaml b/tools/spectral/ipa/rulesets/IPA-106.yaml index 458c0e93c3..b9090c2d2c 100644 --- a/tools/spectral/ipa/rulesets/IPA-106.yaml +++ b/tools/spectral/ipa/rulesets/IPA-106.yaml @@ -1,5 +1,5 @@ # IPA-106: Create -# http://go/ipa/106 +# http://go/ipa-spectral#IPA-106 functions: - createMethodRequestBodyIsRequestSuffixedObject @@ -11,9 +11,12 @@ functions: rules: xgen-IPA-106-create-method-request-body-is-request-suffixed-object: description: >- - The Create method request should be a Request suffixed object. http://go/ipa/106 - This rule applies only to POST requests targeting resource collection URIs. - message: '{{error}} http://go/ipa/106' + The Create method request should be a Request suffixed object. + + ##### Implementation details + + Validation checks the POST method for resource collection paths. + message: '{{error}} http://go/ipa-spectral#IPA-106' severity: warn given: '$.paths[*].post.requestBody.content' then: @@ -21,9 +24,12 @@ rules: function: 'createMethodRequestBodyIsRequestSuffixedObject' xgen-IPA-106-create-method-should-not-have-query-parameters: description: >- - Create operations should not use query parameters. http://go/ipa/106 - This rule applies only to POST requests targeting resource collection URIs. - message: '{{error}} http://go/ipa/106' + Create operations should not use query parameters. + + ##### Implementation details + + Validation checks the POST method for resource collection paths. + message: '{{error}} http://go/ipa-spectral#IPA-106' severity: warn given: '$.paths[*].post' then: @@ -32,22 +38,27 @@ rules: ignoredValues: ['pretty', 'envelope'] 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. http://go/ipa/106 - readOnly/writeOnly properties will be ignored. - This rule applies only to POST requests targeting resource collection URIs. - message: '{{error}} http://go/ipa/106' + Request body content of the Create method and response content of the Get method should refer to the same resource. + + ##### Implementation details + + Validation checks the POST method for resource collection paths. + - `readOnly:true` properties of Get method response will be ignored. + - `writeOnly:true` properties of Create method request will be ignored. + message: '{{error}} http://go/ipa-spectral#IPA-106' severity: warn given: '$.paths[*].post.requestBody.content' then: field: '@key' function: 'createMethodRequestBodyIsGetResponse' - functionOptions: - ignoredValues: ['readOnly', 'writeOnly'] xgen-IPA-106-create-method-request-has-no-readonly-fields: description: >- - Create method Request object must not include fields with readOnly:true. http://go/ipa/106 - This rule applies only to POST requests targeting resource collection URIs. - message: '{{error}} http://go/ipa/106' + Create method Request object must not include fields with readOnly:true. + + ##### Implementation details + + Validation checks the POST method for resource collection paths. + message: '{{error}} http://go/ipa-spectral#IPA-106' severity: warn given: '$.paths[*].post.requestBody.content' then: @@ -55,9 +66,12 @@ rules: function: 'createMethodRequestHasNoReadonlyFields' xgen-IPA-106-create-method-response-code-is-201: description: >- - Create methods must return a 201 Created response code. http://go/ipa/106 - This rule applies only to POST requests targeting resource collection URIs. - message: '{{error}} http://go/ipa/106' + Create methods must return a 201 Created response code. + + ##### Implementation details + + Validation checks the POST method for resource collection paths. + message: '{{error}} http://go/ipa-spectral#IPA-106' severity: warn given: '$.paths[*].post' then: diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index eba172975b..2f778cd003 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -123,23 +123,35 @@ Rule is based on [http://go/ipa/IPA-106](http://go/ipa/IPA-106). #### xgen-IPA-106-create-method-request-body-is-request-suffixed-object ![warn](https://img.shields.io/badge/warning-yellow) -The Create method request should be a Request suffixed object. http://go/ipa/106 This rule applies only to POST requests targeting resource collection URIs. +The Create method request should be a Request suffixed object. +##### Implementation details +Validation checks the POST method for resource collection paths. #### xgen-IPA-106-create-method-should-not-have-query-parameters ![warn](https://img.shields.io/badge/warning-yellow) -Create operations should not use query parameters. http://go/ipa/106 This rule applies only to POST requests targeting resource collection URIs. +Create operations should not use query parameters. +##### Implementation details +Validation checks the POST method for resource collection paths. #### xgen-IPA-106-create-method-request-body-is-get-method-response ![warn](https://img.shields.io/badge/warning-yellow) -Request body content of the Create method and response content of the Get method should refer to the same resource. http://go/ipa/106 readOnly/writeOnly properties will be ignored. This rule applies only to POST requests targeting resource collection URIs. +Request body content of the Create method and response content of the Get method should refer to the same resource. +##### Implementation details +Validation checks the POST method for resource collection paths. + - `readOnly:true` properties of Get method response will be ignored. + - `writeOnly:true` properties of Create method request will be ignored. #### xgen-IPA-106-create-method-request-has-no-readonly-fields ![warn](https://img.shields.io/badge/warning-yellow) -Create method Request object must not include fields with readOnly:true. http://go/ipa/106 This rule applies only to POST requests targeting resource collection URIs. +Create method Request object must not include fields with readOnly:true. +##### Implementation details +Validation checks the POST method for resource collection paths. #### xgen-IPA-106-create-method-response-code-is-201 ![warn](https://img.shields.io/badge/warning-yellow) -Create methods must return a 201 Created response code. http://go/ipa/106 This rule applies only to POST requests targeting resource collection URIs. +Create methods must return a 201 Created response code. +##### Implementation details +Validation checks the POST method for resource collection paths. ### IPA-107 diff --git a/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js b/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js index 8feda5ff13..519ec920f5 100644 --- a/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js +++ b/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js @@ -5,7 +5,7 @@ import { isSingletonResource, } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; -import { isDeepEqual, omitDeep } from './utils/compareUtils.js'; +import { isDeepEqual, removeReadOnlyProperties, removeWriteOnlyProperties } from './utils/compareUtils.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; import { getResponseOfGetMethodByMediaType } from './utils/methodUtils.js'; @@ -14,7 +14,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, opts, { path, documentInventory }) => { +export default (input, _, { path, documentInventory }) => { const oas = documentInventory.resolved; const resourcePath = path[1]; let mediaType = input; @@ -42,8 +42,7 @@ export default (input, opts, { path, documentInventory }) => { const errors = checkViolationsAndReturnErrors( path, postMethodRequestContentPerMediaType, - getMethodResponseContentPerMediaType, - opts + getMethodResponseContentPerMediaType ); if (errors.length !== 0) { @@ -56,12 +55,10 @@ export default (input, opts, { path, documentInventory }) => { function checkViolationsAndReturnErrors( path, postMethodRequestContentPerMediaType, - getMethodResponseContentPerMediaType, - opts + getMethodResponseContentPerMediaType ) { const errors = []; - const ignoredValues = opts?.ignoredValues || []; if (!getMethodResponseContentPerMediaType.schema) { return [ { @@ -70,12 +67,12 @@ function checkViolationsAndReturnErrors( }, ]; } - if ( - !isDeepEqual( - omitDeep(postMethodRequestContentPerMediaType.schema, ...ignoredValues), - omitDeep(getMethodResponseContentPerMediaType.schema, ...ignoredValues) - ) - ) { + + // Create filtered versions of schemas by removing properties with appropriate flags + const filteredCreateRequestSchema = removeWriteOnlyProperties(postMethodRequestContentPerMediaType); + const filteredGetResponseSchema = removeReadOnlyProperties(getMethodResponseContentPerMediaType); + + if (!isDeepEqual(filteredCreateRequestSchema, filteredGetResponseSchema)) { errors.push({ path, message: ERROR_MESSAGE, diff --git a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js index 57d20ee327..888255c5b9 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js @@ -34,31 +34,57 @@ export function isDeepEqual(value1, value2) { } /** - * 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 + * Recursively removes properties with given flags from schema + * @param {object} schema The schema to process + * @param {string} propertyToRemove The property type to remove (boolean property) + * @returns {object} New schema with specified properties removed */ -export function omitDeep(obj, ...keys) { - if (!obj || typeof obj !== 'object') return obj; +export function removePropertyByFlag(schema, propertyToRemove) { + if (!schema || typeof schema !== 'object') return schema; - // Handle arrays - if (Array.isArray(obj)) { - return obj.map((item) => omitDeep(item, ...keys)); + if (Array.isArray(schema)) { + return schema.map((item) => removePropertyByFlag(item, propertyToRemove)); } - // Handle regular objects - return Object.entries(obj).reduce((result, [key, value]) => { - // Skip properties that should be omitted - if (keys.includes(key)) return result; + const result = {}; - // Handle nested objects/arrays recursively - if (value && typeof value === 'object') { - result[key] = omitDeep(value, ...keys); + // Handle regular object properties + for (const [key, value] of Object.entries(schema)) { + // Skip this property if it's marked with the flag we're removing + if (key === propertyToRemove && value === true) continue; + + // Handle properties object specially + if (key === 'properties' && typeof value === 'object') { + result[key] = {}; + for (const [propName, propValue] of Object.entries(value)) { + // Skip properties marked with the flag we're removing + if (propValue[propertyToRemove] === true) continue; + result[key][propName] = removePropertyByFlag(propValue, propertyToRemove); + } + } else if (typeof value === 'object') { + result[key] = removePropertyByFlag(value, propertyToRemove); } else { result[key] = value; } + } + + return result; +} + +/** + * Recursively removes properties with readOnly: true flag from schema + * @param {object} schema The schema to process + * @returns {object} New schema with readOnly properties removed + */ +export function removeReadOnlyProperties(schema) { + return removePropertyByFlag(schema, 'readOnly'); +} - return result; - }, {}); +/** + * Recursively removes properties with writeOnly: true flag from schema + * @param {object} schema The schema to process + * @returns {object} New schema with writeOnly properties removed + */ +export function removeWriteOnlyProperties(schema) { + return removePropertyByFlag(schema, 'writeOnly'); }