api-quality-spectral-ruleset 1.3.0-beta.4 → 1.3.0-beta.5
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
CHANGED
|
@@ -17,7 +17,9 @@ functions:
|
|
|
17
17
|
- apq-check-examples-coverage
|
|
18
18
|
- apq-path-depth
|
|
19
19
|
- apq-forbidden-characters
|
|
20
|
+
- apq-valid-response-schema
|
|
20
21
|
- apq-path-param-query-conflict
|
|
22
|
+
- apq-schema-format
|
|
21
23
|
- apq-security-required-response
|
|
22
24
|
- apq-check-ambiguous-path
|
|
23
25
|
- apq-password-format
|
|
@@ -218,7 +220,7 @@ rules:
|
|
|
218
220
|
then:
|
|
219
221
|
function: apq-alternate-paths
|
|
220
222
|
functionOptions:
|
|
221
|
-
except: ["me"]
|
|
223
|
+
except: ["me", "get", "search"]
|
|
222
224
|
apiq:OAR018:
|
|
223
225
|
description: Operation not recommended for resource path depending on HTTP verb
|
|
224
226
|
message: "OAR018: Operation not recommended for resource path: {{path}} ({{verb}})"
|
|
@@ -282,18 +284,44 @@ rules:
|
|
|
282
284
|
description: "$expand must be defined as a query parameter in collection operations (excluding detail endpoints and health checks)."
|
|
283
285
|
message: "OAR020: $expand must be defined as a query parameter in this operation."
|
|
284
286
|
severity: warn
|
|
285
|
-
given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/) && !@property.match(/status|health|ping/))].get
|
|
287
|
+
given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/) && !@property.match(/status|health|ping/))].get"
|
|
286
288
|
then:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
+
function: schema
|
|
290
|
+
functionOptions:
|
|
291
|
+
schema:
|
|
292
|
+
required: ["parameters"]
|
|
293
|
+
properties:
|
|
294
|
+
parameters:
|
|
295
|
+
type: array
|
|
296
|
+
contains:
|
|
297
|
+
type: object
|
|
298
|
+
properties:
|
|
299
|
+
name:
|
|
300
|
+
const: "$expand"
|
|
301
|
+
in:
|
|
302
|
+
const: query
|
|
303
|
+
required: [name, in]
|
|
289
304
|
apiq:OAR021:
|
|
290
305
|
description: "$exclude must be defined as a query parameter in collection operations (excluding detail endpoints and health checks)."
|
|
291
306
|
message: "OAR021: $exclude must be defined as a query parameter in this operation."
|
|
292
307
|
severity: warn
|
|
293
|
-
given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/) && !@property.match(/status|health|ping/))].get
|
|
308
|
+
given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/) && !@property.match(/status|health|ping/))].get"
|
|
294
309
|
then:
|
|
295
|
-
|
|
296
|
-
|
|
310
|
+
function: schema
|
|
311
|
+
functionOptions:
|
|
312
|
+
schema:
|
|
313
|
+
required: ["parameters"]
|
|
314
|
+
properties:
|
|
315
|
+
parameters:
|
|
316
|
+
type: array
|
|
317
|
+
contains:
|
|
318
|
+
type: object
|
|
319
|
+
properties:
|
|
320
|
+
name:
|
|
321
|
+
const: "$exclude"
|
|
322
|
+
in:
|
|
323
|
+
const: query
|
|
324
|
+
required: [name, in]
|
|
297
325
|
apiq:OAR022:
|
|
298
326
|
description: "$orderby must be defined as a query parameter in all operations."
|
|
299
327
|
message: "OAR022: $orderby must be defined as a query parameter in this operation."
|
|
@@ -496,24 +524,18 @@ rules:
|
|
|
496
524
|
notMatch: "^(Cookie|Set-Cookie)$"
|
|
497
525
|
apiq:OAR037:
|
|
498
526
|
description: "Schema should use well-defined type and format."
|
|
499
|
-
message: "OAR037: String schemas must specify a valid format (date, date-time, password, byte, binary, email, uuid, uri, hostname, ipv4, ipv6, HEX, json, xml, or base64)."
|
|
527
|
+
message: "OAR037: String schemas must specify a valid format (date, date-time, password, byte, binary, email, uuid, uri, hostname, ipv4, ipv6, HEX, HEX(16), json, xml, or base64)."
|
|
500
528
|
severity: error
|
|
501
|
-
given: "$..
|
|
529
|
+
given: "$..[?(@.type=='string')]"
|
|
502
530
|
then:
|
|
503
|
-
|
|
504
|
-
function: pattern
|
|
505
|
-
functionOptions:
|
|
506
|
-
match: "^(date|date-time|password|byte|binary|email|uuid|uri|hostname|ipv4|ipv6|HEX|json|xml|base64)$"
|
|
531
|
+
function: apq-schema-format
|
|
507
532
|
apiq:OAR038:
|
|
508
|
-
description: "
|
|
509
|
-
message: "OAR038: Response property
|
|
533
|
+
description: "The 201 response schema of a POST operation must have properties named 'data' or 'error' with at least one sub-property."
|
|
534
|
+
message: "OAR038: Response property must be named 'data' or 'error'. Got '{{property}}'."
|
|
510
535
|
severity: error
|
|
511
|
-
given: "$.paths
|
|
536
|
+
given: "$.paths.*.post.responses.*.content..schema.properties"
|
|
512
537
|
then:
|
|
513
|
-
|
|
514
|
-
function: pattern
|
|
515
|
-
functionOptions:
|
|
516
|
-
match: "^data|error$"
|
|
538
|
+
function: apq-valid-response-schema
|
|
517
539
|
apiq:OAR039:
|
|
518
540
|
description: "Response codes must be defined according to the standard depending on the HTTP verb and resource path."
|
|
519
541
|
message: "{{error}}"
|
|
@@ -810,9 +832,9 @@ rules:
|
|
|
810
832
|
- "$.paths.*.*.parameters[*].schema..properties.*~"
|
|
811
833
|
- "$.paths.*.*.responses[*].schema..properties.*~"
|
|
812
834
|
then:
|
|
813
|
-
function:
|
|
835
|
+
function: pattern
|
|
814
836
|
functionOptions:
|
|
815
|
-
|
|
837
|
+
match: "^([a-z$][a-z0-9_$]*|_[a-z0-9_]+|@[a-zA-Z][a-zA-Z0-9_]*|x-[a-zA-Z][a-zA-Z0-9_-]*)$"
|
|
816
838
|
apiq:OAR067:
|
|
817
839
|
description: "RequestBody and Responses schema property names must be compliant with the camelCase naming convention."
|
|
818
840
|
message: "OAR067: RequestBody and Responses schema property names must be compliant with the camelCase naming convention."
|
|
@@ -871,10 +893,10 @@ rules:
|
|
|
871
893
|
functionOptions:
|
|
872
894
|
notMatch: "stacktrace"
|
|
873
895
|
apiq:OAR073:
|
|
874
|
-
description: "API should include a 429 response to indicate rate limiting, except for health check paths like /status, /health, /ping."
|
|
896
|
+
description: "API should include a 429 response to indicate rate limiting, except for health check paths like /status, /health, /health-check, /ping, /liveness, /readiness."
|
|
875
897
|
message: "OAR073: API should include a 429 response to indicate rate limiting."
|
|
876
898
|
severity: error
|
|
877
|
-
given: "$.paths[?(
|
|
899
|
+
given: "$.paths[?(@property !== '/status' && @property !== '/health' && @property !== '/health-check' && @property !== '/ping' && @property !== '/liveness' && @property !== '/readiness')][*].responses"
|
|
878
900
|
then:
|
|
879
901
|
field: "429"
|
|
880
902
|
function: truthy
|
|
@@ -10,11 +10,14 @@ module.exports = (given, { except }, context) => {
|
|
|
10
10
|
|
|
11
11
|
const parts = paths.substr(1).split('/').filter(p => p.length > 0);
|
|
12
12
|
if (parts.length === 0) return result;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const firstPart = parts.shift();
|
|
14
|
+
let previousIsVar;
|
|
15
|
+
if (except && except.includes(firstPart)) {
|
|
16
|
+
previousIsVar = true;
|
|
17
|
+
} else if (isVariable(firstPart)) {
|
|
18
|
+
return [{ message: context.rule.message }];
|
|
19
|
+
} else {
|
|
20
|
+
previousIsVar = false;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
for (const part of parts) {
|
|
@@ -6,18 +6,25 @@
|
|
|
6
6
|
module.exports = (targetVal, _options, context) => {
|
|
7
7
|
const pattern = /^[a-zA-Z0-9_\-., ]+$/;
|
|
8
8
|
const errors = [];
|
|
9
|
+
const basePath = context.path || [];
|
|
9
10
|
|
|
10
11
|
if (typeof targetVal === 'string') {
|
|
11
12
|
if (!pattern.test(targetVal)) {
|
|
12
|
-
errors.push({
|
|
13
|
+
errors.push({
|
|
14
|
+
message: context.rule.message,
|
|
15
|
+
path: basePath
|
|
16
|
+
});
|
|
13
17
|
}
|
|
14
18
|
} else if (Array.isArray(targetVal)) {
|
|
15
19
|
targetVal.forEach((role, index) => {
|
|
16
20
|
if (typeof role === 'string' && !pattern.test(role)) {
|
|
17
|
-
errors.push({
|
|
21
|
+
errors.push({
|
|
22
|
+
message: context.rule.message,
|
|
23
|
+
path: [...basePath, index]
|
|
24
|
+
});
|
|
18
25
|
}
|
|
19
26
|
});
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
return errors;
|
|
23
|
-
};
|
|
30
|
+
};
|
|
@@ -1,98 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (given === null || typeof given !== "object") {
|
|
14
|
-
return [];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const errors = [];
|
|
18
|
-
const path = context.path || [];
|
|
19
|
-
|
|
20
|
-
if (given.type === "string" && options.formats.string) {
|
|
21
|
-
if (given.format) {
|
|
22
|
-
if (!options.formats.string.includes(given.format)) {
|
|
23
|
-
errors.push({
|
|
24
|
-
message: `Schema with type string has unrecognized format: ${given.format}`,
|
|
25
|
-
path: [...path, "format"]
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
} else if (given.type === "integer" && options.formats.integer) {
|
|
30
|
-
if (given.format) {
|
|
31
|
-
if (!options.formats.integer.includes(given.format)) {
|
|
32
|
-
errors.push({
|
|
33
|
-
message: `Schema with type integer has unrecognized format: ${given.format}`,
|
|
34
|
-
path: [...path, "format"]
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
errors.push({
|
|
39
|
-
message: "Schema with type integer should specify format",
|
|
40
|
-
path
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
} else if (given.type === "number" && options.formats.number) {
|
|
44
|
-
if (given.format) {
|
|
45
|
-
if (!options.formats.number.includes(given.format)) {
|
|
46
|
-
errors.push({
|
|
47
|
-
message: `Schema with type number has unrecognized format: ${given.format}`,
|
|
48
|
-
path: [...path, "format"]
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
} else {
|
|
52
|
-
errors.push({
|
|
53
|
-
message: "Schema with type number should specify format",
|
|
54
|
-
path
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
} else if (given.type === "boolean") {
|
|
58
|
-
if (given.format) {
|
|
59
|
-
errors.push({
|
|
60
|
-
message: "Schema with type boolean should not specify format",
|
|
61
|
-
path: [...path, "format"]
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
} else if (given.properties && typeof given.properties === "object") {
|
|
65
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
66
|
-
for (const [key, value] of Object.entries(given.properties)) {
|
|
67
|
-
errors.push(...checkTypeAndFormat(value, options, {
|
|
68
|
-
path: [...path, "properties", key],
|
|
69
|
-
document: context.document,
|
|
70
|
-
documentInventory: context.documentInventory,
|
|
71
|
-
rule: context.rule
|
|
72
|
-
}));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (given.type === "array") {
|
|
77
|
-
errors.push(...checkTypeAndFormat(given.items, options, {
|
|
78
|
-
path: [...path, "items"],
|
|
79
|
-
document: context.document,
|
|
80
|
-
documentInventory: context.documentInventory,
|
|
81
|
-
rule: context.rule
|
|
82
|
-
}));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (given.allOf && Array.isArray(given.allOf)) {
|
|
86
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
87
|
-
for (const [index, value] of given.allOf.entries()) {
|
|
88
|
-
errors.push(...checkTypeAndFormat(value, options, {
|
|
89
|
-
path: [...path, "allOf", index],
|
|
90
|
-
document: context.document,
|
|
91
|
-
documentInventory: context.documentInventory,
|
|
92
|
-
rule: context.rule
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return errors;
|
|
1
|
+
const VALID_FORMATS = new Set([
|
|
2
|
+
'date', 'date-time', 'password', 'byte', 'binary', 'email',
|
|
3
|
+
'uuid', 'uri', 'hostname', 'ipv4', 'ipv6', 'hex', 'hex(16)',
|
|
4
|
+
'json', 'xml', 'base64'
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
module.exports = (targetVal, _options, context) => {
|
|
8
|
+
const format = targetVal.format;
|
|
9
|
+
if (format === undefined || format === null || !VALID_FORMATS.has(String(format).toLowerCase())) {
|
|
10
|
+
return [{ message: context.rule.message }];
|
|
11
|
+
}
|
|
12
|
+
return [];
|
|
98
13
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} targetVal
|
|
3
|
+
* @param {object} _options
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = (targetVal, _options, context) => {
|
|
7
|
+
const errors = [];
|
|
8
|
+
const VALID_NAMES = ['data', 'error'];
|
|
9
|
+
|
|
10
|
+
if (!targetVal || typeof targetVal !== 'object') {
|
|
11
|
+
return errors;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const path = context.path || [];
|
|
15
|
+
const responsesIdx = path.lastIndexOf('responses');
|
|
16
|
+
if (responsesIdx >= 0 && path[responsesIdx + 1] !== '201') {
|
|
17
|
+
return errors;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const [propName, propValue] of Object.entries(targetVal)) {
|
|
21
|
+
if (!VALID_NAMES.includes(propName)) {
|
|
22
|
+
errors.push({
|
|
23
|
+
message: context.rule.message,
|
|
24
|
+
path: [propName]
|
|
25
|
+
});
|
|
26
|
+
} else {
|
|
27
|
+
const subProps = propValue && propValue.properties;
|
|
28
|
+
const hasSubProps = subProps && Object.keys(subProps).length > 0;
|
|
29
|
+
if (!hasSubProps) {
|
|
30
|
+
errors.push({
|
|
31
|
+
message: `OAR038: Property '${propName}' must have at least one sub-property.`,
|
|
32
|
+
path: [propName]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return errors;
|
|
39
|
+
};
|