Skip to content

Commit b163e0f

Browse files
CLOUDP-304053: IPA-106:Create - The resource must be the request body (implement deepObjectComparison without third party dependencies) (#522)
1 parent 62d11fb commit b163e0f

File tree

8 files changed

+296
-32
lines changed

8 files changed

+296
-32
lines changed

package-lock.json

Lines changed: 0 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@
3131
"apache-arrow": "^19.0.1",
3232
"dotenv": "^16.4.7",
3333
"eslint-plugin-jest": "^28.10.0",
34-
"lodash": "^4.17.21",
3534
"markdown-table": "^3.0.4",
36-
"omit-deep-lodash": "^1.1.7",
3735
"openapi-to-postmanv2": "4.25.0",
3836
"parquet-wasm": "^0.6.1"
3937
},

tools/spectral/ipa/__tests__/createMethodRequestBodyIsGetResponse.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ testRule('xgen-IPA-106-create-method-request-body-is-get-method-response', [
9999
content: {
100100
'application/vnd.atlas.2023-01-01+json': {
101101
schema: {
102-
$ref: '#/components/schemas/SchemaOne',
102+
type: 'string',
103103
},
104104
},
105105
'application/vnd.atlas.2024-01-01+json': {
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { describe, expect, it } from '@jest/globals';
2+
import { isDeepEqual, omitDeep } from '../../rulesets/functions/utils/compareUtils';
3+
4+
describe('isDeepEqual', () => {
5+
it('handles primitive values', () => {
6+
expect(isDeepEqual(1, 1)).toBe(true);
7+
expect(isDeepEqual('hello', 'hello')).toBe(true);
8+
expect(isDeepEqual(true, true)).toBe(true);
9+
expect(isDeepEqual(null, null)).toBe(true);
10+
expect(isDeepEqual(undefined, undefined)).toBe(true);
11+
12+
expect(isDeepEqual(1, 2)).toBe(false);
13+
expect(isDeepEqual('hello', 'world')).toBe(false);
14+
expect(isDeepEqual(true, false)).toBe(false);
15+
expect(isDeepEqual(null, undefined)).toBe(false);
16+
expect(isDeepEqual(1, '1')).toBe(false);
17+
});
18+
19+
it('handles simple objects', () => {
20+
expect(isDeepEqual({}, {})).toBe(true);
21+
expect(isDeepEqual({ a: 1 }, { a: 1 })).toBe(true);
22+
expect(isDeepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true);
23+
24+
expect(isDeepEqual({ a: 1 }, { a: 2 })).toBe(false);
25+
expect(isDeepEqual({ a: 1 }, { b: 1 })).toBe(false);
26+
expect(isDeepEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
27+
});
28+
29+
it('handles arrays', () => {
30+
expect(isDeepEqual([], [])).toBe(true);
31+
expect(isDeepEqual([1, 2], [1, 2])).toBe(true);
32+
33+
expect(isDeepEqual([1, 2], [2, 1])).toBe(false);
34+
expect(isDeepEqual([1, 2], [1, 2, 3])).toBe(false);
35+
});
36+
37+
it('handles nested objects', () => {
38+
expect(isDeepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })).toBe(true);
39+
40+
expect(isDeepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 3 } })).toBe(false);
41+
42+
expect(isDeepEqual({ a: 1, b: { c: 2, d: 3 } }, { a: 1, b: { c: 2 } })).toBe(false);
43+
});
44+
45+
it('handles nested arrays', () => {
46+
expect(isDeepEqual({ a: [1, 2, { b: 3 }] }, { a: [1, 2, { b: 3 }] })).toBe(true);
47+
48+
expect(isDeepEqual({ a: [1, 2, { b: 3 }] }, { a: [1, 2, { b: 4 }] })).toBe(false);
49+
});
50+
51+
it('handles mixed types', () => {
52+
expect(isDeepEqual({ a: 1 }, [1])).toBe(false);
53+
expect(isDeepEqual({ a: 1 }, null)).toBe(false);
54+
expect(isDeepEqual(null, { a: 1 })).toBe(false);
55+
});
56+
});
57+
58+
describe('omitDeep', () => {
59+
it('handles primitives', () => {
60+
expect(omitDeep(1, 'any')).toBe(1);
61+
expect(omitDeep('hello', 'any')).toBe('hello');
62+
expect(omitDeep(null, 'any')).toBe(null);
63+
expect(omitDeep(undefined, 'any')).toBe(undefined);
64+
});
65+
66+
it('handles shallow objects', () => {
67+
expect(omitDeep({ a: 1, b: 2 }, 'a')).toEqual({ b: 2 });
68+
expect(omitDeep({ a: 1, b: 2 }, 'c')).toEqual({ a: 1, b: 2 });
69+
expect(omitDeep({ a: 1, b: 2 }, 'a', 'b')).toEqual({});
70+
});
71+
72+
it('handles arrays', () => {
73+
expect(
74+
omitDeep(
75+
[
76+
{ a: 1, b: 2 },
77+
{ a: 3, b: 4 },
78+
],
79+
'a'
80+
)
81+
).toEqual([{ b: 2 }, { b: 4 }]);
82+
});
83+
84+
it('handles nested objects', () => {
85+
const input = {
86+
a: 1,
87+
b: {
88+
c: 2,
89+
d: 3,
90+
e: {
91+
f: 4,
92+
g: 5,
93+
},
94+
},
95+
h: 6,
96+
};
97+
98+
const expected = {
99+
a: 1,
100+
b: {
101+
d: 3,
102+
e: {
103+
g: 5,
104+
},
105+
},
106+
h: 6,
107+
};
108+
109+
expect(omitDeep(input, 'c', 'f')).toEqual(expected);
110+
});
111+
112+
it('handles deeply nested arrays', () => {
113+
const input = {
114+
items: [
115+
{ id: 1, name: 'item1', metadata: { created: '2023', readOnly: true } },
116+
{ id: 2, name: 'item2', metadata: { created: '2023', readOnly: true } },
117+
],
118+
};
119+
120+
const expected = {
121+
items: [
122+
{ id: 1, name: 'item1', metadata: { created: '2023' } },
123+
{ id: 2, name: 'item2', metadata: { created: '2023' } },
124+
],
125+
};
126+
127+
expect(omitDeep(input, 'readOnly')).toEqual(expected);
128+
});
129+
130+
it('handles complex schemas', () => {
131+
const schema = {
132+
type: 'object',
133+
properties: {
134+
name: { type: 'string' },
135+
id: { type: 'string', readOnly: true },
136+
details: {
137+
type: 'object',
138+
properties: {
139+
createdAt: { type: 'string', readOnly: true },
140+
description: { type: 'string' },
141+
},
142+
},
143+
items: {
144+
type: 'array',
145+
items: {
146+
type: 'object',
147+
properties: {
148+
itemId: { type: 'string', readOnly: true },
149+
itemName: { type: 'string' },
150+
},
151+
},
152+
},
153+
},
154+
};
155+
156+
const expected = {
157+
type: 'object',
158+
properties: {
159+
name: { type: 'string' },
160+
id: { type: 'string' },
161+
details: {
162+
type: 'object',
163+
properties: {
164+
createdAt: { type: 'string' },
165+
description: { type: 'string' },
166+
},
167+
},
168+
items: {
169+
type: 'array',
170+
items: {
171+
type: 'object',
172+
properties: {
173+
itemId: { type: 'string' },
174+
itemName: { type: 'string' },
175+
},
176+
},
177+
},
178+
},
179+
};
180+
181+
expect(omitDeep(schema, 'readOnly')).toEqual(expected);
182+
});
183+
184+
it('handles multiple keys to omit', () => {
185+
const input = {
186+
a: 1,
187+
b: 2,
188+
c: {
189+
d: 3,
190+
e: 4,
191+
f: {
192+
g: 5,
193+
h: 6,
194+
},
195+
},
196+
};
197+
198+
expect(omitDeep(input, 'a', 'e', 'g')).toEqual({
199+
b: 2,
200+
c: {
201+
d: 3,
202+
f: {
203+
h: 6,
204+
},
205+
},
206+
});
207+
});
208+
});

tools/spectral/ipa/rulesets/IPA-106.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@ rules:
1616
field: '@key'
1717
function: 'createMethodRequestBodyIsRequestSuffixedObject'
1818
xgen-IPA-106-create-method-should-not-have-query-parameters:
19-
description: 'Create operations should not use query parameters. http://go/ipa/xxx'
19+
description: 'Create operations should not use query parameters. http://go/ipa/106'
2020
message: '{{error}} http://go/ipa/106'
2121
severity: warn
2222
given: '$.paths[*].post'
2323
then:
2424
function: 'createMethodShouldNotHaveQueryParameters'
2525
xgen-IPA-106-create-method-request-body-is-get-method-response:
26-
description: 'The Create method request should be a Get method response. http://go/ipa/106'
26+
description: |
27+
Request body content of the Create method and response content of the Get method should refer to the same resource.
28+
readOnly/writeOnly properties will be ignored. http://go/ipa/106
2729
message: '{{error}} http://go/ipa/106'
2830
severity: warn
2931
given: '$.paths[*].post.requestBody.content'
3032
then:
3133
field: '@key'
3234
function: 'createMethodRequestBodyIsGetResponse'
35+
functionOptions:
36+
ignoredValues: ['readOnly', 'writeOnly']

tools/spectral/ipa/rulesets/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ For rule definitions, see [IPA-105.yaml](https://github.com/mongodb/openapi/blob
5252

5353
For rule definitions, see [IPA-106.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-106.yaml).
5454

55-
| Rule Name | Description | Severity |
56-
| ------------------------------------------------------------------ | -------------------------------------------------------------------------------- | -------- |
57-
| 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 |
58-
| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/xxx | warn |
59-
| 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 |
55+
| Rule Name | Description | Severity |
56+
| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
57+
| 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 |
58+
| xgen-IPA-106-create-method-should-not-have-query-parameters | Create operations should not use query parameters. http://go/ipa/106 | warn |
59+
| 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.
60+
readOnly/writeOnly properties will be ignored. http://go/ipa/106
61+
| warn |
6062

6163
### IPA-108
6264

tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsGetResponse.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { getResponseOfGetMethodByMediaType, isCustomMethodIdentifier } from './utils/resourceEvaluation.js';
22
import { resolveObject } from './utils/componentUtils.js';
3-
import { isEqual } from 'lodash';
4-
import omitDeep from 'omit-deep-lodash';
3+
import { isDeepEqual, omitDeep } from './utils/compareUtils.js';
54
import { hasException } from './utils/exceptions.js';
65
import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js';
76

87
const RULE_NAME = 'xgen-IPA-106-create-method-request-body-is-get-method-response';
98
const ERROR_MESSAGE =
109
'The request body schema properties must match the response body schema properties of the Get method.';
1110

12-
export default (input, _, { path, documentInventory }) => {
11+
export default (input, opts, { path, documentInventory }) => {
1312
const oas = documentInventory.resolved;
1413
const resourcePath = path[1];
1514
let mediaType = input;
@@ -34,7 +33,8 @@ export default (input, _, { path, documentInventory }) => {
3433
const errors = checkViolationsAndReturnErrors(
3534
path,
3635
postMethodRequestContentPerMediaType,
37-
getMethodResponseContentPerMediaType
36+
getMethodResponseContentPerMediaType,
37+
opts
3838
);
3939

4040
if (errors.length !== 0) {
@@ -47,14 +47,16 @@ export default (input, _, { path, documentInventory }) => {
4747
function checkViolationsAndReturnErrors(
4848
path,
4949
postMethodRequestContentPerMediaType,
50-
getMethodResponseContentPerMediaType
50+
getMethodResponseContentPerMediaType,
51+
opts
5152
) {
5253
const errors = [];
5354

55+
const ignoredValues = opts?.ignoredValues || [];
5456
if (
55-
!isEqual(
56-
omitDeep(postMethodRequestContentPerMediaType.schema, 'readOnly', 'writeOnly'),
57-
omitDeep(getMethodResponseContentPerMediaType.schema, 'readOnly', 'writeOnly')
57+
!isDeepEqual(
58+
omitDeep(postMethodRequestContentPerMediaType.schema, ...ignoredValues),
59+
omitDeep(getMethodResponseContentPerMediaType.schema, ...ignoredValues)
5860
)
5961
) {
6062
errors.push({

0 commit comments

Comments
 (0)