Skip to content

Commit 8799cef

Browse files
CLOUDP-302984: IPA rule Get methods should return data for a single resource (#463)
1 parent 51ba101 commit 8799cef

File tree

8 files changed

+356
-15
lines changed

8 files changed

+356
-15
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import testRule from './__helpers__/testRule';
2+
import { DiagnosticSeverity } from '@stoplight/types';
3+
4+
const componentSchemas = {
5+
schemas: {
6+
Schema: {
7+
properties: {
8+
exampleProperty: {
9+
type: 'string',
10+
},
11+
},
12+
},
13+
PaginatedSchema: {
14+
type: 'array',
15+
},
16+
ArraySchema: {
17+
properties: {
18+
results: {
19+
type: 'array',
20+
},
21+
},
22+
},
23+
},
24+
};
25+
26+
testRule('xgen-IPA-104-get-method-returns-single-resource', [
27+
{
28+
name: 'valid resources',
29+
document: {
30+
paths: {
31+
'/resource': {
32+
get: {
33+
responses: {
34+
200: {
35+
content: {
36+
'application/vnd.atlas.2024-08-05+json': {
37+
schema: {
38+
$ref: '#/components/schemas/PaginatedSchema',
39+
},
40+
},
41+
},
42+
},
43+
},
44+
},
45+
},
46+
'/resource/{id}': {
47+
get: {
48+
responses: {
49+
200: {
50+
content: {
51+
'application/vnd.atlas.2024-08-05+json': {
52+
schema: {
53+
$ref: '#/components/schemas/Schema',
54+
},
55+
},
56+
},
57+
},
58+
},
59+
},
60+
},
61+
'/resource/{id}:getAllThings': {
62+
get: {
63+
responses: {
64+
200: {
65+
content: {
66+
'application/vnd.atlas.2024-08-05+json': {
67+
schema: {
68+
$ref: '#/components/schemas/ArraySchema',
69+
},
70+
},
71+
},
72+
},
73+
},
74+
},
75+
},
76+
'/singleton': {
77+
get: {
78+
responses: {
79+
200: {
80+
content: {
81+
'application/vnd.atlas.2024-08-05+json': {
82+
schema: {
83+
$ref: '#/components/schemas/Schema',
84+
},
85+
},
86+
},
87+
},
88+
},
89+
},
90+
},
91+
},
92+
components: componentSchemas,
93+
},
94+
errors: [],
95+
},
96+
{
97+
name: 'invalid resources',
98+
document: {
99+
paths: {
100+
'/arrayResource/{id}': {
101+
get: {
102+
responses: {
103+
200: {
104+
content: {
105+
'application/vnd.atlas.2024-08-05+json': {
106+
schema: {
107+
$ref: '#/components/schemas/ArraySchema',
108+
},
109+
},
110+
},
111+
},
112+
},
113+
},
114+
},
115+
'/paginatedResource/{id}': {
116+
get: {
117+
responses: {
118+
200: {
119+
content: {
120+
'application/vnd.atlas.2024-08-05+json': {
121+
schema: {
122+
$ref: '#/components/schemas/PaginatedSchema',
123+
},
124+
},
125+
},
126+
},
127+
},
128+
},
129+
},
130+
'/arraySingleton': {
131+
get: {
132+
responses: {
133+
200: {
134+
content: {
135+
'application/vnd.atlas.2024-08-05+json': {
136+
schema: {
137+
$ref: '#/components/schemas/ArraySchema',
138+
},
139+
},
140+
},
141+
},
142+
},
143+
},
144+
},
145+
'/paginatedSingleton': {
146+
get: {
147+
responses: {
148+
200: {
149+
content: {
150+
'application/vnd.atlas.2024-08-05+json': {
151+
schema: {
152+
$ref: '#/components/schemas/PaginatedSchema',
153+
},
154+
},
155+
},
156+
},
157+
},
158+
},
159+
},
160+
},
161+
components: componentSchemas,
162+
},
163+
errors: [
164+
{
165+
code: 'xgen-IPA-104-get-method-returns-single-resource',
166+
message:
167+
'Get methods should return data for a single resource. This method returns an array or a paginated response. http://go/ipa/104',
168+
path: [
169+
'paths',
170+
'/arrayResource/{id}',
171+
'get',
172+
'responses',
173+
'200',
174+
'content',
175+
'application/vnd.atlas.2024-08-05+json',
176+
],
177+
severity: DiagnosticSeverity.Warning,
178+
},
179+
{
180+
code: 'xgen-IPA-104-get-method-returns-single-resource',
181+
message:
182+
'Get methods should return data for a single resource. This method returns an array or a paginated response. http://go/ipa/104',
183+
path: [
184+
'paths',
185+
'/paginatedResource/{id}',
186+
'get',
187+
'responses',
188+
'200',
189+
'content',
190+
'application/vnd.atlas.2024-08-05+json',
191+
],
192+
severity: DiagnosticSeverity.Warning,
193+
},
194+
{
195+
code: 'xgen-IPA-104-get-method-returns-single-resource',
196+
message:
197+
'Get methods should return data for a single resource. This method returns an array or a paginated response. http://go/ipa/104',
198+
path: [
199+
'paths',
200+
'/arraySingleton',
201+
'get',
202+
'responses',
203+
'200',
204+
'content',
205+
'application/vnd.atlas.2024-08-05+json',
206+
],
207+
severity: DiagnosticSeverity.Warning,
208+
},
209+
{
210+
code: 'xgen-IPA-104-get-method-returns-single-resource',
211+
message:
212+
'Get methods should return data for a single resource. This method returns an array or a paginated response. http://go/ipa/104',
213+
path: [
214+
'paths',
215+
'/paginatedSingleton',
216+
'get',
217+
'responses',
218+
'200',
219+
'content',
220+
'application/vnd.atlas.2024-08-05+json',
221+
],
222+
severity: DiagnosticSeverity.Warning,
223+
},
224+
],
225+
},
226+
{
227+
name: 'invalid resources with exceptions',
228+
document: {
229+
paths: {
230+
'/arrayResource/{id}': {
231+
get: {
232+
responses: {
233+
200: {
234+
content: {
235+
'application/vnd.atlas.2024-08-05+json': {
236+
'x-xgen-IPA-exception': {
237+
'xgen-IPA-104-get-method-returns-single-resource': 'reason',
238+
},
239+
schema: {
240+
$ref: '#/components/schemas/ArraySchema',
241+
},
242+
},
243+
},
244+
},
245+
},
246+
},
247+
},
248+
'/paginatedSingleton': {
249+
get: {
250+
responses: {
251+
200: {
252+
content: {
253+
'application/vnd.atlas.2024-08-05+json': {
254+
'x-xgen-IPA-exception': {
255+
'xgen-IPA-104-get-method-returns-single-resource': 'reason',
256+
},
257+
schema: {
258+
$ref: '#/components/schemas/PaginatedSchema',
259+
},
260+
},
261+
},
262+
},
263+
},
264+
},
265+
},
266+
},
267+
components: componentSchemas,
268+
},
269+
errors: [],
270+
},
271+
]);

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
functions:
55
- eachResourceHasGetMethod
6+
- getMethodReturnsSingleResource
67

78
rules:
89
xgen-IPA-104-resource-has-GET:
@@ -13,3 +14,10 @@ rules:
1314
then:
1415
field: '@key'
1516
function: 'eachResourceHasGetMethod'
17+
xgen-IPA-104-get-method-returns-single-resource:
18+
description: 'The purpose of the get method is to return data from a single resource. http://go/ipa/104'
19+
message: '{{error}} http://go/ipa/104'
20+
severity: warn
21+
given: '$.paths[*].get'
22+
then:
23+
function: 'getMethodReturnsSingleResource'

tools/spectral/ipa/rulesets/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ For rule definitions, see [IPA-102.yaml](https://github.com/mongodb/openapi/blob
2828

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

31-
| Rule Name | Description | Severity |
32-
| ----------------------------- | --------------------------------------------------------------- | -------- |
33-
| xgen-IPA-104-resource-has-GET | APIs must provide a get method for resources. http://go/ipa/104 | warn |
31+
| Rule Name | Description | Severity |
32+
| ----------------------------------------------- | ----------------------------------------------------------------------------------------- | -------- |
33+
| xgen-IPA-104-resource-has-GET | APIs must provide a get method for resources. http://go/ipa/104 | warn |
34+
| xgen-IPA-104-get-method-returns-single-resource | The purpose of the get method is to return data from a single resource. http://go/ipa/104 | warn |
3435

3536
### IPA-109
3637

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { isChild, isCustomMethod, isSingletonResource, getResourcePaths } from './utils/resourceEvaluation.js';
2+
import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js';
3+
import { getAllSuccessfulResponseSchemas } from './utils/methodUtils.js';
4+
import { hasException } from './utils/exceptions.js';
5+
import { schemaIsArray, schemaIsPaginated } from './utils/schemaUtils.js';
6+
import { resolveObject } from './utils/componentUtils.js';
7+
8+
const RULE_NAME = 'xgen-IPA-104-get-method-returns-single-resource';
9+
const ERROR_MESSAGE_STANDARD_RESOURCE =
10+
'Get methods should return data for a single resource. This method returns an array or a paginated response.';
11+
12+
export default (input, _, { path, documentInventory }) => {
13+
const oas = documentInventory.resolved;
14+
const resourcePath = path[1];
15+
const resourcePaths = getResourcePaths(resourcePath, Object.keys(oas.paths));
16+
17+
if (isCustomMethod(resourcePath) || (!isChild(resourcePath) && !isSingletonResource(resourcePaths))) {
18+
return;
19+
}
20+
21+
const errors = [];
22+
23+
const responseSchemas = getAllSuccessfulResponseSchemas(input);
24+
responseSchemas.forEach(({ schemaPath, schema }) => {
25+
const fullPath = path.concat(schemaPath);
26+
const responseObject = resolveObject(oas, fullPath);
27+
28+
if (hasException(responseObject, RULE_NAME)) {
29+
collectException(responseObject, RULE_NAME, fullPath);
30+
} else if (schemaIsPaginated(schema) || schemaIsArray(schema)) {
31+
collectAndReturnViolation(fullPath, RULE_NAME, ERROR_MESSAGE_STANDARD_RESOURCE);
32+
errors.push({ path: fullPath, message: ERROR_MESSAGE_STANDARD_RESOURCE });
33+
} else {
34+
collectAdoption(fullPath, RULE_NAME);
35+
}
36+
});
37+
38+
return errors;
39+
};

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
isSingletonResource,
77
} from './utils/resourceEvaluation.js';
88
import { hasException } from './utils/exceptions.js';
9-
import { getAllSuccessfulGetResponseSchemas } from './utils/methodUtils.js';
9+
import { getAllSuccessfulResponseSchemas } from './utils/methodUtils.js';
1010
import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js';
1111

1212
const RULE_NAME = 'xgen-IPA-113-singleton-must-not-have-id';
@@ -28,8 +28,8 @@ export default (input, opts, { path, documentInventory }) => {
2828
const resourcePaths = getResourcePaths(resourcePath, Object.keys(oas.paths));
2929

3030
if (isSingletonResource(resourcePaths) && hasGetMethod(input)) {
31-
const resourceSchemas = getAllSuccessfulGetResponseSchemas(input);
32-
if (resourceSchemas.some((schema) => schemaHasIdProperty(schema))) {
31+
const resourceSchemas = getAllSuccessfulResponseSchemas(input['get']);
32+
if (resourceSchemas.some(({ schema }) => schemaHasIdProperty(schema))) {
3333
return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE);
3434
}
3535
}

tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import collector, { EntryType } from '../../../metrics/collector.js';
22
import { EXCEPTION_EXTENSION } from './exceptions.js';
3+
34
/**
45
* Collects a violation entry and returns formatted error data.
56
*
6-
* @param {string} path - The JSON path for the object where the rule violation occurred.
7+
* @param {Array<string>} path - The JSON path for the object where the rule violation occurred.
78
* @param {string} ruleName - The name of the rule that was violated.
89
* @param {string|Array<Object>} errorData - The error information. Can be either a string message or an array of error objects.
910
* @returns {Array<Object>} An array of error objects. Each object has a 'message' property.
@@ -25,7 +26,7 @@ export function collectAndReturnViolation(path, ruleName, errorData) {
2526
/**
2627
* Collects an adoption entry.
2728
*
28-
* @param {string} path - The JSON path for the object where the rule adoption occurred.
29+
* @param {Array<string>} path - The JSON path for the object where the rule adoption occurred.
2930
* @param {string} ruleName - The name of the rule that was adopted.
3031
*/
3132
export function collectAdoption(path, ruleName) {
@@ -36,7 +37,7 @@ export function collectAdoption(path, ruleName) {
3637
* Collects an exception entry.
3738
*
3839
* @param object the object to evaluate
39-
* @param {string} path - The JSON path for the object where the rule exception occurred.
40+
* @param {Array<string>} path - The JSON path for the object where the rule exception occurred.
4041
* @param {string} ruleName - The name of the rule that the exception is defined for.
4142
*/
4243
export function collectException(object, ruleName, path) {

0 commit comments

Comments
 (0)