api-quality-spectral-ruleset 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apq-spectral.yaml +117 -45
- package/functions/apq-check-examples-coverage.js +54 -0
- package/functions/apq-path-depth.js +20 -0
- package/functions/apq-response-media-type.js +43 -0
- package/package.json +1 -1
- package/functions/apq-default-value.js +0 -23
- package/functions/apq-naming-convention.js +0 -21
- package/functions/apq-properties-schema-format.js +0 -29
package/apq-spectral.yaml
CHANGED
|
@@ -10,8 +10,11 @@ functions:
|
|
|
10
10
|
- apq-response-headers
|
|
11
11
|
- apq-require-response-on-path-params
|
|
12
12
|
- apq-required-fields-exist
|
|
13
|
+
- apq-response-media-type
|
|
13
14
|
- apq-security-check
|
|
14
15
|
- apq-custom-field
|
|
16
|
+
- apq-check-examples-coverage
|
|
17
|
+
- apq-path-depth
|
|
15
18
|
extends:
|
|
16
19
|
- spectral:asyncapi
|
|
17
20
|
- spectral:oas
|
|
@@ -109,19 +112,21 @@ rules:
|
|
|
109
112
|
description: "Default response media type should be defined for responses."
|
|
110
113
|
message: "OAR010: Default response media type is mandatory."
|
|
111
114
|
severity: warn
|
|
112
|
-
given: "$.paths[*][get,post,put,patch,delete]
|
|
115
|
+
given: "$.paths[*][get,post,put,patch,delete]"
|
|
113
116
|
then:
|
|
114
|
-
|
|
115
|
-
function: truthy
|
|
117
|
+
function: apq-response-media-type
|
|
116
118
|
apiq:OAR011:
|
|
117
119
|
description: "URLs should follow the specified naming convention: all literal path segments must be in kebab-case."
|
|
118
120
|
message: "OAR011: The resource names with more than two words must be compliant with the standard naming convention (kebab-case)."
|
|
121
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR011.md"
|
|
119
122
|
severity: error
|
|
120
|
-
given:
|
|
123
|
+
given:
|
|
124
|
+
- "$.paths[*]~"
|
|
125
|
+
- "$.servers[*].url"
|
|
121
126
|
then:
|
|
122
127
|
function: pattern
|
|
123
128
|
functionOptions:
|
|
124
|
-
match: "^(
|
|
129
|
+
match: "^(https?:\\/\\/[^\\/]+)?(\\/(?:[a-z0-9]+(?:-[a-z0-9]+)*|\\{[^}]+\\}))*\\/?$"
|
|
125
130
|
apiq:OAR012:
|
|
126
131
|
description: "Path params, query params, object names and property names should follow the configured naming convention. You can configure snake_case (default), kebab-case, camelCase or UpperCamelCase"
|
|
127
132
|
message: "OAR012: Path params, query params, object names and property names must follow the configured naming convention."
|
|
@@ -147,13 +152,16 @@ rules:
|
|
|
147
152
|
function: truthy
|
|
148
153
|
apiq:OAR014:
|
|
149
154
|
description: "Resources depth level should be below the non-suggested range."
|
|
150
|
-
message:
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
message: "OAR014: The number of parts of the path must be less than 4."
|
|
156
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR014.md"
|
|
157
|
+
severity: warn
|
|
158
|
+
given: "$.paths.*~"
|
|
153
159
|
then:
|
|
154
|
-
function:
|
|
160
|
+
function: apq-path-depth
|
|
155
161
|
functionOptions:
|
|
156
|
-
|
|
162
|
+
maxDepth: 3
|
|
163
|
+
ignoreSegments:
|
|
164
|
+
- me
|
|
157
165
|
apiq:OAR015:
|
|
158
166
|
description: "Resources depth level should be smaller than 5."
|
|
159
167
|
message: "OAR015: Path exceeds the maximum allowed depth level of 5."
|
|
@@ -168,24 +176,29 @@ rules:
|
|
|
168
176
|
message: "OAR016: Numeric types must use a valid format for their type."
|
|
169
177
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR016.md"
|
|
170
178
|
severity: error
|
|
171
|
-
given:
|
|
172
|
-
- "$..[?(@.type == 'integer' && @.format)]"
|
|
173
|
-
- "$..[?(@.type == 'number' && @.format)]"
|
|
179
|
+
given: "$..[?(@.type=='number' || @.type=='integer')]"
|
|
174
180
|
then:
|
|
175
181
|
function: schema
|
|
176
182
|
functionOptions:
|
|
177
183
|
schema:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
required: [format]
|
|
185
|
+
allOf:
|
|
186
|
+
- if:
|
|
187
|
+
properties:
|
|
188
|
+
type:
|
|
189
|
+
const: integer
|
|
190
|
+
then:
|
|
191
|
+
properties:
|
|
192
|
+
format:
|
|
193
|
+
enum: [int32, int64]
|
|
194
|
+
- if:
|
|
195
|
+
properties:
|
|
196
|
+
type:
|
|
197
|
+
const: number
|
|
198
|
+
then:
|
|
199
|
+
properties:
|
|
200
|
+
format:
|
|
201
|
+
enum: [float, double]
|
|
189
202
|
apiq:OAR017:
|
|
190
203
|
description: "Resource path should alternate static and parametrized parts."
|
|
191
204
|
message: "OAR017: Resource path should alternate static and parametrized parts."
|
|
@@ -261,11 +274,30 @@ rules:
|
|
|
261
274
|
apiq:OAR022:
|
|
262
275
|
description: "$orderby must be defined as a query parameter in all operations."
|
|
263
276
|
message: "OAR022: $orderby must be defined as a query parameter in this operation."
|
|
277
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR022.md"
|
|
264
278
|
severity: warn
|
|
265
|
-
given: "$.paths[
|
|
279
|
+
given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/))].get"
|
|
266
280
|
then:
|
|
267
|
-
|
|
268
|
-
|
|
281
|
+
function: schema
|
|
282
|
+
functionOptions:
|
|
283
|
+
schema:
|
|
284
|
+
if:
|
|
285
|
+
properties:
|
|
286
|
+
responses:
|
|
287
|
+
required: ["206"]
|
|
288
|
+
then:
|
|
289
|
+
required: ["parameters"]
|
|
290
|
+
properties:
|
|
291
|
+
parameters:
|
|
292
|
+
type: array
|
|
293
|
+
contains:
|
|
294
|
+
type: object
|
|
295
|
+
properties:
|
|
296
|
+
name:
|
|
297
|
+
const: "$orderby"
|
|
298
|
+
in:
|
|
299
|
+
const: query
|
|
300
|
+
required: [name, in]
|
|
269
301
|
apiq:OAR023:
|
|
270
302
|
description: "$total must be defined as a query parameter in all operations."
|
|
271
303
|
message: "OAR023: $total must be defined as a query parameter in this operation."
|
|
@@ -285,11 +317,30 @@ rules:
|
|
|
285
317
|
apiq:OAR025:
|
|
286
318
|
description: "$limit must be defined as a query parameter in all operations."
|
|
287
319
|
message: "OAR025: $limit must be defined as a query parameter in this operation."
|
|
320
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR025.md"
|
|
288
321
|
severity: error
|
|
289
|
-
given: "$.paths[
|
|
322
|
+
given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/))].get"
|
|
290
323
|
then:
|
|
291
|
-
|
|
292
|
-
|
|
324
|
+
function: schema
|
|
325
|
+
functionOptions:
|
|
326
|
+
schema:
|
|
327
|
+
if:
|
|
328
|
+
properties:
|
|
329
|
+
responses:
|
|
330
|
+
required: ["206"]
|
|
331
|
+
then:
|
|
332
|
+
required: ["parameters"]
|
|
333
|
+
properties:
|
|
334
|
+
parameters:
|
|
335
|
+
type: array
|
|
336
|
+
contains:
|
|
337
|
+
type: object
|
|
338
|
+
properties:
|
|
339
|
+
name:
|
|
340
|
+
const: "$limit"
|
|
341
|
+
in:
|
|
342
|
+
const: query
|
|
343
|
+
required: [name, in]
|
|
293
344
|
apiq:OAR026:
|
|
294
345
|
description: "The $total parameter default value should be false."
|
|
295
346
|
message: "OAR026: The $total parameter default value should be false."
|
|
@@ -358,14 +409,14 @@ rules:
|
|
|
358
409
|
function: truthy
|
|
359
410
|
apiq:OAR031:
|
|
360
411
|
description: The examples can help developers to understand the response data structure and representation."
|
|
361
|
-
message: "OAR031: Examples -
|
|
412
|
+
message: "OAR031: Examples - Parameters, Request Body and Responses must have an examples defined."
|
|
362
413
|
severity: error
|
|
363
414
|
given:
|
|
364
415
|
- $.paths.*.*.parameters.*
|
|
365
|
-
- $.paths
|
|
416
|
+
- $.paths.*.*.requestBody
|
|
417
|
+
- $.paths.*.*.responses[?(@property !== "204")]
|
|
366
418
|
then:
|
|
367
|
-
|
|
368
|
-
function: truthy
|
|
419
|
+
function: apq-check-examples-coverage
|
|
369
420
|
apiq:OAR032:
|
|
370
421
|
description: "Ambiguous path parts not encouraged."
|
|
371
422
|
message: "OAR032: When reading a URL, it must be possible to interpret which resources it is referring to in a concrete way. Ambiguous names in resources must be avoided."
|
|
@@ -820,16 +871,32 @@ rules:
|
|
|
820
871
|
- required: ["enum"]
|
|
821
872
|
apiq:OAR076:
|
|
822
873
|
description: "Schema should use well-defined type and format."
|
|
823
|
-
message: "OAR076:
|
|
824
|
-
|
|
825
|
-
|
|
874
|
+
message: "OAR076: Schema should use well-defined type and format."
|
|
875
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR076.md"
|
|
876
|
+
severity: error
|
|
877
|
+
given: "$..[?(@.type=='number' || @.type=='integer')]"
|
|
826
878
|
then:
|
|
827
|
-
|
|
828
|
-
function: enumeration
|
|
879
|
+
function: schema
|
|
829
880
|
functionOptions:
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
881
|
+
schema:
|
|
882
|
+
required: [format]
|
|
883
|
+
allOf:
|
|
884
|
+
- if:
|
|
885
|
+
properties:
|
|
886
|
+
type:
|
|
887
|
+
const: integer
|
|
888
|
+
then:
|
|
889
|
+
properties:
|
|
890
|
+
format:
|
|
891
|
+
enum: [int32, int64]
|
|
892
|
+
- if:
|
|
893
|
+
properties:
|
|
894
|
+
type:
|
|
895
|
+
const: number
|
|
896
|
+
then:
|
|
897
|
+
properties:
|
|
898
|
+
format:
|
|
899
|
+
enum: [float, double]
|
|
833
900
|
apiq:OAR077:
|
|
834
901
|
description: "All parameters in query must be snake_case."
|
|
835
902
|
message: "OAR077: All parameters in query must be snake_case."
|
|
@@ -966,9 +1033,12 @@ rules:
|
|
|
966
1033
|
apiq:OAR091:
|
|
967
1034
|
description: "Parameters must contain only $ref references."
|
|
968
1035
|
message: "OAR091: Parameters must contain only $ref references."
|
|
1036
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR091.md"
|
|
969
1037
|
severity: error
|
|
970
|
-
|
|
1038
|
+
resolved: false
|
|
1039
|
+
given: "$.paths[*][get,post,put,patch,delete].parameters[*]"
|
|
971
1040
|
then:
|
|
1041
|
+
field: $ref
|
|
972
1042
|
function: truthy
|
|
973
1043
|
apiq:OAR092:
|
|
974
1044
|
description: "RequestBody must contain a $ref."
|
|
@@ -1034,11 +1104,13 @@ rules:
|
|
|
1034
1104
|
description: "Last path part must be the API version, indicated with the prefix 'v' and the version number as integer."
|
|
1035
1105
|
message: "OAR100: Last path part must be the API version, indicated with the prefix 'v' and the version number as integer."
|
|
1036
1106
|
severity: "error"
|
|
1037
|
-
given:
|
|
1107
|
+
given:
|
|
1108
|
+
- "$.servers[*].url"
|
|
1109
|
+
- "$.basePath"
|
|
1038
1110
|
then:
|
|
1039
1111
|
function: pattern
|
|
1040
1112
|
functionOptions:
|
|
1041
|
-
match: "^https?://[^/]
|
|
1113
|
+
match: "^(https?://[^/]+)?(?:/[^/]+)*/v[0-9]+$"
|
|
1042
1114
|
apiq:OAR101:
|
|
1043
1115
|
description: "The first part of the path should be one of the allowed paths (e.g., '/hello')."
|
|
1044
1116
|
message: "OAR101: The first part of the path should be one of the allowed paths."
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} given
|
|
3
|
+
* @param {object} options
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = (given, options, context) => {
|
|
7
|
+
if (!given || typeof given !== 'object') return [];
|
|
8
|
+
|
|
9
|
+
const hasExample = (node) => {
|
|
10
|
+
if (!node || typeof node !== 'object') return false;
|
|
11
|
+
|
|
12
|
+
if (node.example !== undefined || node.examples !== undefined) return true;
|
|
13
|
+
|
|
14
|
+
if (node.content && typeof node.content === 'object') {
|
|
15
|
+
return Object.values(node.content).some(mediaType => hasExample(mediaType));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (node.schema) {
|
|
19
|
+
return checkSchema(node.schema);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (node.properties || node.items || node.type) {
|
|
23
|
+
return checkSchema(node);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return false;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const checkSchema = (schema) => {
|
|
30
|
+
if (!schema || typeof schema !== 'object') return false;
|
|
31
|
+
|
|
32
|
+
if (schema.example !== undefined || schema.examples !== undefined) return true;
|
|
33
|
+
|
|
34
|
+
if (schema.properties && typeof schema.properties === 'object') {
|
|
35
|
+
return Object.values(schema.properties).some(prop => checkSchema(prop));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (schema.items) {
|
|
39
|
+
return checkSchema(schema.items);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (!hasExample(given)) {
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
message: context.rule.message
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [];
|
|
54
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} targetVal
|
|
3
|
+
* @param {object} options
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = (targetVal, options, context) => {
|
|
7
|
+
const maxDepth = options.maxDepth ?? 3
|
|
8
|
+
const ignore = options.ignoreSegments ?? []
|
|
9
|
+
|
|
10
|
+
const segments = targetVal
|
|
11
|
+
.split('/')
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.filter(segment => !ignore.includes(segment))
|
|
14
|
+
|
|
15
|
+
if (segments.length > maxDepth) {
|
|
16
|
+
return [
|
|
17
|
+
{ message: context.rule.message }
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} given
|
|
3
|
+
* @param {object} options
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const NO_BODY_CODES = new Set(['204']);
|
|
8
|
+
|
|
9
|
+
module.exports = (given, options, context) => {
|
|
10
|
+
const errors = [];
|
|
11
|
+
if (!given) return errors;
|
|
12
|
+
|
|
13
|
+
const root = context.document.parserResult.data;
|
|
14
|
+
|
|
15
|
+
if (root.swagger) {
|
|
16
|
+
const produces = given.produces ?? root.produces;
|
|
17
|
+
|
|
18
|
+
if (!produces || !Array.isArray(produces) || !produces.includes('application/json')) {
|
|
19
|
+
errors.push({
|
|
20
|
+
message: context.rule.message,
|
|
21
|
+
path: [...context.path]
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return errors;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const responses = given.responses;
|
|
29
|
+
if (!responses) return errors;
|
|
30
|
+
|
|
31
|
+
for (const [statusCode, response] of Object.entries(responses)) {
|
|
32
|
+
if (NO_BODY_CODES.has(statusCode)) continue;
|
|
33
|
+
|
|
34
|
+
if (!response?.content?.['application/json']) {
|
|
35
|
+
errors.push({
|
|
36
|
+
message: context.rule.message,
|
|
37
|
+
path: [...context.path, 'responses', statusCode]
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return errors;
|
|
43
|
+
};
|
package/package.json
CHANGED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @param {object} given
|
|
3
|
-
* @param {object} options
|
|
4
|
-
* @param {string} options.type
|
|
5
|
-
* @param {string} options.match
|
|
6
|
-
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
7
|
-
*/
|
|
8
|
-
module.exports = (given, options, context) => {
|
|
9
|
-
const errors = [];
|
|
10
|
-
if (!given) return errors;
|
|
11
|
-
|
|
12
|
-
if (!given.type || given.type.toString() !== options.type.toString()) {
|
|
13
|
-
errors.push({
|
|
14
|
-
message: context.rule.message,
|
|
15
|
-
});
|
|
16
|
-
} else if (!given.default || given.default.toString() !== options.match.toString()) {
|
|
17
|
-
errors.push({
|
|
18
|
-
message: context.rule.message,
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return errors;
|
|
23
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
const { pattern } = require("@stoplight/spectral-functions");
|
|
2
|
-
|
|
3
|
-
const patterns = {
|
|
4
|
-
kebabCase: /^(\/|[a-z0-9-.]+|{[a-zA-Z0-9_]+})+$/,
|
|
5
|
-
camelCase: /^[a-z]+([A-Z][a-z0-9]+)*$/,
|
|
6
|
-
snakeCase: /^[a-z]+(_[a-z0-9]+)*$/,
|
|
7
|
-
pascalCase: /^[A-Z]+([A-Z][a-z0-9]+)*$/
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @param {object} given
|
|
12
|
-
* @param {object} options
|
|
13
|
-
* @param {string} options.pattern
|
|
14
|
-
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
15
|
-
*/
|
|
16
|
-
module.exports = (given, options, context) => {
|
|
17
|
-
const errors = [];
|
|
18
|
-
if (!given) return errors;
|
|
19
|
-
|
|
20
|
-
return pattern(given, { match: patterns[options.pattern] }, context);
|
|
21
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// Check that format is valid for a schema type.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @param {object} given
|
|
5
|
-
* @param {object} options
|
|
6
|
-
* @param {Array<string>} options.formats
|
|
7
|
-
* @param {Array<string>} options.properties
|
|
8
|
-
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
9
|
-
*/
|
|
10
|
-
module.exports = function checkTypeAndFormat(given, options, context) {
|
|
11
|
-
if (given === null || typeof given !== "object") {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const errors = [];
|
|
16
|
-
const path = context.path || [];
|
|
17
|
-
const name = path.slice(-1)[0].toString();
|
|
18
|
-
|
|
19
|
-
if (options.properties.includes(name)) {
|
|
20
|
-
if (!given.format || !options.formats.includes(given.format)) {
|
|
21
|
-
errors.push({
|
|
22
|
-
message: context.rule.message,
|
|
23
|
-
path: [...path, name]
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return errors;
|
|
29
|
-
};
|