Skip to content

Commit fa3b901

Browse files
CLOUDP-302981: Add rule xgen-IPA-104-get-method-response-has-no-input-fields
1 parent a988e11 commit fa3b901

File tree

4 files changed

+289
-6
lines changed

4 files changed

+289
-6
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import testRule from './__helpers__/testRule';
2+
import { DiagnosticSeverity } from '@stoplight/types';
3+
4+
const componentSchemas = {
5+
schemas: {
6+
ValidSchemaResponse: {
7+
type: 'object',
8+
properties: {
9+
id: {
10+
type: 'string',
11+
readOnly: true,
12+
},
13+
name: {
14+
type: 'string',
15+
},
16+
},
17+
},
18+
InvalidSchemaResponse: {
19+
type: 'object',
20+
properties: {
21+
id: {
22+
type: 'string',
23+
readOnly: true,
24+
},
25+
name: {
26+
type: 'string',
27+
writeOnly: true,
28+
},
29+
},
30+
},
31+
},
32+
};
33+
34+
testRule('xgen-IPA-104-get-method-response-has-no-input-fields', [
35+
{
36+
name: 'valid get responses',
37+
document: {
38+
paths: {
39+
'/resource/{id}': {
40+
get: {
41+
responses: {
42+
200: {
43+
content: {
44+
'application/vnd.atlas.2024-08-05+json': {
45+
schema: {
46+
$ref: '#/components/schemas/ValidSchemaResponse',
47+
},
48+
},
49+
},
50+
},
51+
},
52+
},
53+
},
54+
'/resource/{id}/singleton': {
55+
get: {
56+
responses: {
57+
200: {
58+
content: {
59+
'application/vnd.atlas.2024-08-05+json': {
60+
schema: {
61+
type: 'object',
62+
properties: {
63+
id: {
64+
type: 'string',
65+
readOnly: true,
66+
},
67+
name: {
68+
type: 'string',
69+
},
70+
},
71+
},
72+
},
73+
},
74+
},
75+
},
76+
},
77+
},
78+
},
79+
components: componentSchemas,
80+
},
81+
errors: [],
82+
},
83+
{
84+
name: 'invalid GET with body',
85+
document: {
86+
paths: {
87+
'/resource/{id}': {
88+
get: {
89+
responses: {
90+
200: {
91+
content: {
92+
'application/vnd.atlas.2024-08-05+json': {
93+
schema: {
94+
$ref: '#/components/schemas/InvalidSchemaResponse',
95+
},
96+
},
97+
},
98+
},
99+
},
100+
},
101+
},
102+
'/resource/{id}/singleton': {
103+
get: {
104+
responses: {
105+
200: {
106+
content: {
107+
'application/vnd.atlas.2024-08-05+json': {
108+
schema: {
109+
type: 'object',
110+
properties: {
111+
id: {
112+
type: 'string',
113+
readOnly: true,
114+
},
115+
name: {
116+
type: 'string',
117+
writeOnly: true,
118+
},
119+
},
120+
},
121+
},
122+
},
123+
},
124+
},
125+
},
126+
},
127+
},
128+
components: componentSchemas,
129+
},
130+
errors: [
131+
{
132+
code: 'xgen-IPA-104-get-method-response-has-no-input-fields',
133+
message:
134+
'The get method response object must not include output fields (writeOnly properties). http://go/ipa/104',
135+
path: [
136+
'paths',
137+
'/resource/{id}',
138+
'get',
139+
'responses',
140+
'200',
141+
'content',
142+
'application/vnd.atlas.2024-08-05+json',
143+
],
144+
severity: DiagnosticSeverity.Warning,
145+
},
146+
{
147+
code: 'xgen-IPA-104-get-method-response-has-no-input-fields',
148+
message:
149+
'The get method response object must not include output fields (writeOnly properties). http://go/ipa/104',
150+
path: [
151+
'paths',
152+
'/resource/{id}/singleton',
153+
'get',
154+
'responses',
155+
'200',
156+
'content',
157+
'application/vnd.atlas.2024-08-05+json',
158+
],
159+
severity: DiagnosticSeverity.Warning,
160+
},
161+
],
162+
},
163+
{
164+
name: 'invalid with exception',
165+
document: {
166+
paths: {
167+
'/resource/{id}': {
168+
get: {
169+
responses: {
170+
200: {
171+
content: {
172+
'application/vnd.atlas.2024-08-05+json': {
173+
'x-xgen-IPA-exception': {
174+
'xgen-IPA-104-get-method-response-has-no-input-fields': 'reason',
175+
},
176+
schema: {
177+
$ref: '#/components/schemas/InvalidSchemaResponse',
178+
},
179+
},
180+
},
181+
},
182+
},
183+
},
184+
},
185+
'/resource/{id}/singleton': {
186+
get: {
187+
responses: {
188+
200: {
189+
content: {
190+
'application/vnd.atlas.2024-08-05+json': {
191+
'x-xgen-IPA-exception': {
192+
'xgen-IPA-104-get-method-response-has-no-input-fields': 'reason',
193+
},
194+
schema: {
195+
type: 'object',
196+
properties: {
197+
id: {
198+
type: 'string',
199+
readOnly: true,
200+
},
201+
name: {
202+
type: 'string',
203+
writeOnly: true,
204+
},
205+
},
206+
},
207+
},
208+
},
209+
},
210+
},
211+
},
212+
},
213+
},
214+
components: componentSchemas,
215+
},
216+
errors: [],
217+
},
218+
]);

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ functions:
66
- getMethodReturnsSingleResource
77
- getMethodReturnsResponseSuffixedObject
88
- getResponseCodeShouldBe200OK
9+
- getMethodResponseHasNoInputFields
910

1011
rules:
1112
xgen-IPA-104-resource-has-GET:
@@ -38,3 +39,11 @@ rules:
3839
then:
3940
field: '@key'
4041
function: 'getMethodReturnsResponseSuffixedObject'
42+
xgen-IPA-104-get-method-response-has-no-input-fields:
43+
description: 'The get method response object must not include fields available only on creation or update, ie output fields. http://go/ipa/104'
44+
message: '{{error}} http://go/ipa/104'
45+
severity: warn
46+
given: '$.paths[*].get.responses[*].content'
47+
then:
48+
field: '@key'
49+
function: 'getMethodResponseHasNoInputFields'

tools/spectral/ipa/rulesets/README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ 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 |
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 |
35-
| xgen-IPA-104-get-method-response-code-is-200 | The Get method must return a 200 OK response. http://go/ipa/104 | warn |
36-
| xgen-IPA-104-get-method-returns-response-suffixed-object | The Get method of a resource should return a "Response" suffixed object. 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 |
35+
| xgen-IPA-104-get-method-response-code-is-200 | The Get method must return a 200 OK response. http://go/ipa/104 | warn |
36+
| xgen-IPA-104-get-method-returns-response-suffixed-object | The Get method of a resource should return a "Response" suffixed object. http://go/ipa/104 | warn |
37+
| xgen-IPA-104-get-method-response-has-no-input-fields | The get method response object must not include fields available only on creation or update, ie output fields. http://go/ipa/104 | warn |
3738

3839
### IPA-106
3940

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { hasException } from './utils/exceptions.js';
2+
import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js';
3+
import {
4+
getResourcePathItems,
5+
isResourceCollectionIdentifier,
6+
isSingleResourceIdentifier,
7+
isSingletonResource,
8+
} from './utils/resourceEvaluation.js';
9+
import { resolveObject } from './utils/componentUtils.js';
10+
11+
const RULE_NAME = 'xgen-IPA-104-get-method-response-has-no-input-fields';
12+
const ERROR_MESSAGE = 'The get method response object must not include output fields (writeOnly properties).';
13+
14+
export default (input, _, { path, documentInventory }) => {
15+
const resourcePath = path[1];
16+
const responseCode = path[4];
17+
const oas = documentInventory.resolved;
18+
const resourcePaths = getResourcePathItems(resourcePath, oas.paths);
19+
const contentPerMediaType = resolveObject(oas, path);
20+
21+
if (
22+
!contentPerMediaType.schema ||
23+
!responseCode.startsWith('2') ||
24+
!input.endsWith('json') ||
25+
(!isSingleResourceIdentifier(resourcePath) &&
26+
!(isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePaths)))
27+
) {
28+
return;
29+
}
30+
31+
if (hasException(contentPerMediaType, RULE_NAME)) {
32+
collectException(contentPerMediaType, RULE_NAME, path);
33+
return;
34+
}
35+
36+
const errors = checkViolationsAndReturnErrors(contentPerMediaType, path);
37+
38+
if (errors.length !== 0) {
39+
return collectAndReturnViolation(path, RULE_NAME, errors);
40+
}
41+
return collectAdoption(path, RULE_NAME);
42+
};
43+
44+
function checkViolationsAndReturnErrors(contentPerMediaType, path) {
45+
const schema = contentPerMediaType.schema;
46+
const properties = schema.properties;
47+
if (properties) {
48+
for (const [value] of Object.entries(properties)) {
49+
if (properties[value].writeOnly) {
50+
return [{ path, message: ERROR_MESSAGE }];
51+
}
52+
}
53+
}
54+
return [];
55+
}

0 commit comments

Comments
 (0)