Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@
"apache-arrow": "^19.0.1",
"dotenv": "^16.4.7",
"eslint-plugin-jest": "^28.10.0",
"lodash": "^4.17.21",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

"markdown-table": "^3.0.4",
"omit-deep-lodash": "^1.1.7",
"openapi-to-postmanv2": "4.25.0",
"parquet-wasm": "^0.6.1"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand Down
208 changes: 208 additions & 0 deletions tools/spectral/ipa/__tests__/utils/compareUtils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { describe, expect, it } from '@jest/globals';
import { isDeepEqual, omitDeep } from '../../rulesets/functions/utils/compareUtils';

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,
},
},
});
});
});
8 changes: 6 additions & 2 deletions tools/spectral/ipa/rulesets/IPA-106.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@ 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'
then:
function: 'createMethodShouldNotHaveQueryParameters'
xgen-IPA-106-create-method-request-body-is-get-method-response:
description: 'The Create method request should be a Get method response. http://go/ipa/106'
description: |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

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'
then:
field: '@key'
function: 'createMethodRequestBodyIsGetResponse'
functionOptions:
ignoredValues: ['readOnly', 'writeOnly']
12 changes: 7 additions & 5 deletions tools/spectral/ipa/rulesets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,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/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/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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { getResponseOfGetMethodByMediaType, isCustomMethodIdentifier } from './utils/resourceEvaluation.js';
import { resolveObject } from './utils/componentUtils.js';
import { isEqual } from 'lodash';
import omitDeep from 'omit-deep-lodash';
import { isDeepEqual, omitDeep } from './utils/compareUtils.js';
import { hasException } from './utils/exceptions.js';
import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js';

const RULE_NAME = 'xgen-IPA-106-create-method-request-body-is-get-method-response';
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;
Expand All @@ -34,7 +33,8 @@ export default (input, _, { path, documentInventory }) => {
const errors = checkViolationsAndReturnErrors(
path,
postMethodRequestContentPerMediaType,
getMethodResponseContentPerMediaType
getMethodResponseContentPerMediaType,
opts
);

if (errors.length !== 0) {
Expand All @@ -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')
!isDeepEqual(
omitDeep(postMethodRequestContentPerMediaType.schema, ...ignoredValues),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

omitDeep(getMethodResponseContentPerMediaType.schema, ...ignoredValues)
)
) {
errors.push({
Expand Down
Loading
Loading