api-quality-spectral-ruleset 1.4.0-beta.4 → 1.4.0-beta.6
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 +37 -48
- package/functions/apq-binary-format-check.js +3 -6
- package/functions/apq-collection-query-param-required.js +69 -0
- package/functions/apq-forbidden-characters.js +2 -2
- package/functions/apq-path-depth.js +7 -2
- package/functions/apq-schema-format.js +22 -3
- package/functions/apq-valid-openapi-version.js +20 -0
- package/functions/apq-valid-response-schema.js +7 -6
- package/package.json +1 -1
package/apq-spectral.yaml
CHANGED
|
@@ -18,6 +18,8 @@ functions:
|
|
|
18
18
|
- apq-path-depth
|
|
19
19
|
- apq-forbidden-characters
|
|
20
20
|
- apq-valid-response-schema
|
|
21
|
+
- apq-valid-openapi-version
|
|
22
|
+
- apq-collection-query-param-required
|
|
21
23
|
- apq-path-param-query-conflict
|
|
22
24
|
- apq-schema-format
|
|
23
25
|
- apq-security-required-response
|
|
@@ -69,6 +71,8 @@ rules:
|
|
|
69
71
|
given: "$.x-wso2-security.apim.x-wso2-scopes[*].roles"
|
|
70
72
|
then:
|
|
71
73
|
function: apq-forbidden-characters
|
|
74
|
+
functionOptions:
|
|
75
|
+
pattern: "^[a-zA-Z0-9_\\-., ]+$"
|
|
72
76
|
apiq:OAR005:
|
|
73
77
|
description: "A wrong scope may cause problems to import the API definition into WSO2 or allow all users to call the endpoint."
|
|
74
78
|
message: "OAR005: Scope in the operation must be defined correctly and match an existing scope."
|
|
@@ -169,7 +173,8 @@ rules:
|
|
|
169
173
|
then:
|
|
170
174
|
function: apq-path-depth
|
|
171
175
|
functionOptions:
|
|
172
|
-
|
|
176
|
+
min-level: 4
|
|
177
|
+
max-level: 5
|
|
173
178
|
ignoreSegments:
|
|
174
179
|
- me
|
|
175
180
|
apiq:OAR015:
|
|
@@ -274,54 +279,31 @@ rules:
|
|
|
274
279
|
;options:.*
|
|
275
280
|
apiq:OAR019:
|
|
276
281
|
description: "$select must be defined as a query parameter in collection operations (excluding detail endpoints and health checks)."
|
|
277
|
-
message: "
|
|
282
|
+
message: "{{error}}"
|
|
278
283
|
severity: warn
|
|
279
|
-
given: "$.paths
|
|
284
|
+
given: "$.paths"
|
|
280
285
|
then:
|
|
281
|
-
|
|
282
|
-
|
|
286
|
+
function: apq-collection-query-param-required
|
|
287
|
+
functionOptions:
|
|
288
|
+
parameter-name: "$select"
|
|
283
289
|
apiq:OAR020:
|
|
284
290
|
description: "$expand must be defined as a query parameter in collection operations (excluding detail endpoints and health checks)."
|
|
285
|
-
message: "
|
|
291
|
+
message: "{{error}}"
|
|
286
292
|
severity: warn
|
|
287
|
-
given: "$.paths
|
|
293
|
+
given: "$.paths"
|
|
288
294
|
then:
|
|
289
|
-
function:
|
|
295
|
+
function: apq-collection-query-param-required
|
|
290
296
|
functionOptions:
|
|
291
|
-
|
|
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]
|
|
297
|
+
parameter-name: "$expand"
|
|
304
298
|
apiq:OAR021:
|
|
305
299
|
description: "$exclude must be defined as a query parameter in collection operations (excluding detail endpoints and health checks)."
|
|
306
|
-
message: "
|
|
300
|
+
message: "{{error}}"
|
|
307
301
|
severity: warn
|
|
308
|
-
given: "$.paths
|
|
302
|
+
given: "$.paths"
|
|
309
303
|
then:
|
|
310
|
-
function:
|
|
304
|
+
function: apq-collection-query-param-required
|
|
311
305
|
functionOptions:
|
|
312
|
-
|
|
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]
|
|
306
|
+
parameter-name: "$exclude"
|
|
325
307
|
apiq:OAR022:
|
|
326
308
|
description: "$orderby must be defined as a query parameter in all operations."
|
|
327
309
|
message: "OAR022: $orderby must be defined as a query parameter in this operation."
|
|
@@ -528,19 +510,24 @@ rules:
|
|
|
528
510
|
functionOptions:
|
|
529
511
|
notMatch: "^(Cookie|Set-Cookie)$"
|
|
530
512
|
apiq:OAR037:
|
|
531
|
-
description: "
|
|
532
|
-
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)."
|
|
513
|
+
description: "String schemas must specify a valid format, or a valid pattern when no format is defined."
|
|
514
|
+
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), or a valid pattern when no format is defined."
|
|
533
515
|
severity: error
|
|
516
|
+
resolved: false
|
|
534
517
|
given: "$..[?(@.type=='string')]"
|
|
535
518
|
then:
|
|
536
519
|
function: apq-schema-format
|
|
537
520
|
apiq:OAR038:
|
|
538
521
|
description: "The 201 response schema of a POST operation must have properties named 'data' or 'error' with at least one sub-property."
|
|
539
|
-
message: "
|
|
522
|
+
message: "{{error}}"
|
|
540
523
|
severity: error
|
|
541
|
-
given:
|
|
524
|
+
given:
|
|
525
|
+
- "$.paths.*.post.responses.*.content..schema.properties"
|
|
526
|
+
- "$.paths.*.post.responses.*.schema.properties"
|
|
542
527
|
then:
|
|
543
528
|
function: apq-valid-response-schema
|
|
529
|
+
functionOptions:
|
|
530
|
+
data-property: data
|
|
544
531
|
apiq:OAR039:
|
|
545
532
|
description: "Response codes must be defined according to the standard depending on the HTTP verb and resource path."
|
|
546
533
|
message: "{{error}}"
|
|
@@ -579,13 +566,13 @@ rules:
|
|
|
579
566
|
;patch:^/[^/{}]+/(\{[^/{}]+\}|\bme\b)/[^/{}]+/(\{[^/{}]+\}|\bme\b)/[^/{}]+/(\{[^/{}]+\}|\bme\b)$:200,404,400,415,500,503
|
|
580
567
|
apiq:OAR040:
|
|
581
568
|
description: "A scope name non-compliant with the standard may cause problems at application level."
|
|
582
|
-
message: "OAR040: Scope name
|
|
569
|
+
message: "OAR040: Scope name value is non compliant with the standard."
|
|
583
570
|
severity: "error"
|
|
584
571
|
given: "$.x-wso2-security.apim.x-wso2-scopes[*].name"
|
|
585
572
|
then:
|
|
586
573
|
function: pattern
|
|
587
574
|
functionOptions:
|
|
588
|
-
match: "^
|
|
575
|
+
match: "^[a-zA-Z]{4,}_(SC|sc)_[a-zA-Z0-9]{1,}$"
|
|
589
576
|
apiq:OAR041:
|
|
590
577
|
description: Available scopes must be defined at the end of the document, at the root level, inside `x-wso2-security`.
|
|
591
578
|
message: "OAR041:Scopes must be defined in `x-wso2-security` at the end of the document, before the paths."
|
|
@@ -1020,7 +1007,7 @@ rules:
|
|
|
1020
1007
|
function: apq-password-format
|
|
1021
1008
|
apiq:OAR082:
|
|
1022
1009
|
description: "The string properties 'product', 'line', and 'price' must define a byte or binary format."
|
|
1023
|
-
message: "
|
|
1010
|
+
message: "{{error}}"
|
|
1024
1011
|
severity: error
|
|
1025
1012
|
given: "$..[?(@.properties)]"
|
|
1026
1013
|
then:
|
|
@@ -1049,13 +1036,15 @@ rules:
|
|
|
1049
1036
|
notMatch: "^password$"
|
|
1050
1037
|
apiq:OAR085:
|
|
1051
1038
|
description: "The OpenAPI version must be one of: 2.0, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.1.0."
|
|
1052
|
-
message: "
|
|
1039
|
+
message: "{{error}}"
|
|
1053
1040
|
severity: warn
|
|
1054
|
-
given:
|
|
1041
|
+
given:
|
|
1042
|
+
- "$.openapi"
|
|
1043
|
+
- "$.swagger"
|
|
1055
1044
|
then:
|
|
1056
|
-
function:
|
|
1045
|
+
function: apq-valid-openapi-version
|
|
1057
1046
|
functionOptions:
|
|
1058
|
-
|
|
1047
|
+
valid-versions: "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0"
|
|
1059
1048
|
apiq:OAR086:
|
|
1060
1049
|
description: "Descriptions must begin with a capital letter, end with a period, and not be empty."
|
|
1061
1050
|
message: "OAR086: Descriptions must begin with a capital letter, end with a period, and not be empty."
|
|
@@ -23,14 +23,11 @@ module.exports = (schema, options = {}, context) => {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// Check if format is byte or binary
|
|
27
26
|
const format = propSchema.format || '';
|
|
28
|
-
if (format
|
|
27
|
+
if (format !== 'byte' && format !== 'binary') {
|
|
29
28
|
results.push({
|
|
30
|
-
message:
|
|
31
|
-
|
|
32
|
-
.replace('{{format}}', format || 'undefined'),
|
|
33
|
-
path: [...context.path, 'properties', propName],
|
|
29
|
+
message: `OAR082: Property '${propName}' must define a 'byte' or 'binary' format (currently: ${format || 'missing'}).`,
|
|
30
|
+
path: [...context.path, 'properties', propName, 'type'],
|
|
34
31
|
});
|
|
35
32
|
}
|
|
36
33
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
|
|
2
|
+
const DEFAULT_PATHS = '/me;/health;/ping;/status';
|
|
3
|
+
const PATH_PARAM_SUFFIX_REGEX = /\/\{[^}]+\}$/;
|
|
4
|
+
const STRATEGY_INCLUDE = 'Include';
|
|
5
|
+
const STRATEGY_EXCLUDE = 'Exclude';
|
|
6
|
+
|
|
7
|
+
const escapeRegExp = (segment) => segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const parsePathPatterns = (paths) => paths
|
|
11
|
+
.split(/[\n;]/)
|
|
12
|
+
.map((p) => p.trim())
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map((segment) => new RegExp(`${escapeRegExp(segment)}(/|$)`));
|
|
15
|
+
|
|
16
|
+
const shouldIncludePath = (path, patterns, strategy) => {
|
|
17
|
+
const matchesList = patterns.some((regex) => regex.test(path));
|
|
18
|
+
return strategy === STRATEGY_INCLUDE ? matchesList : !matchesList;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const hasQueryParameter = (parameters, parameterName) => Array.isArray(parameters)
|
|
22
|
+
&& parameters.some((p) => p && !p.$ref && p.name === parameterName && p.in === 'query');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} given
|
|
26
|
+
* @param {object} options
|
|
27
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
28
|
+
*/
|
|
29
|
+
module.exports = (given, options = {}, context) => {
|
|
30
|
+
if (!given || typeof given !== 'object') return [];
|
|
31
|
+
|
|
32
|
+
const ruleCode = context && context.rule && context.rule.name ? context.rule.name.split(':').pop() : 'apq-collection-query-param-required';
|
|
33
|
+
|
|
34
|
+
const parameterName = options['parameter-name'];
|
|
35
|
+
if (!parameterName) {
|
|
36
|
+
return [{
|
|
37
|
+
message: `${ruleCode}: "parameter-name" functionOption is required.`,
|
|
38
|
+
path: context.path,
|
|
39
|
+
}];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const strategy = options.pathValidationStrategy === STRATEGY_INCLUDE
|
|
43
|
+
? STRATEGY_INCLUDE
|
|
44
|
+
: STRATEGY_EXCLUDE;
|
|
45
|
+
|
|
46
|
+
const rawPaths = typeof options.paths === 'string' ? options.paths : DEFAULT_PATHS;
|
|
47
|
+
const patterns = parsePathPatterns(rawPaths);
|
|
48
|
+
|
|
49
|
+
const results = [];
|
|
50
|
+
|
|
51
|
+
Object.entries(given).forEach(([path, pathItem]) => {
|
|
52
|
+
if (!pathItem || typeof pathItem !== 'object') return;
|
|
53
|
+
|
|
54
|
+
const getOperation = pathItem.get;
|
|
55
|
+
if (!getOperation || typeof getOperation !== 'object') return;
|
|
56
|
+
|
|
57
|
+
if (PATH_PARAM_SUFFIX_REGEX.test(path)) return;
|
|
58
|
+
if (!shouldIncludePath(path, patterns, strategy)) return;
|
|
59
|
+
|
|
60
|
+
if (!hasQueryParameter(getOperation.parameters, parameterName)) {
|
|
61
|
+
results.push({
|
|
62
|
+
message: `${ruleCode}: ${parameterName} must be defined as a query parameter in this operation.`,
|
|
63
|
+
path: [...context.path, path, 'get'],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return results;
|
|
69
|
+
};
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* @param {object} options
|
|
4
4
|
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
5
|
*/
|
|
6
|
-
module.exports = (targetVal,
|
|
7
|
-
const pattern =
|
|
6
|
+
module.exports = (targetVal, options, context) => {
|
|
7
|
+
const pattern = new RegExp((options && options.pattern) || '^[a-zA-Z0-9_\\-., ]+$');
|
|
8
8
|
const errors = [];
|
|
9
9
|
const basePath = context.path || [];
|
|
10
10
|
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
5
|
*/
|
|
6
6
|
module.exports = (targetVal, options, context) => {
|
|
7
|
-
const
|
|
7
|
+
const minDepth = options['min-level'] ?? options.minDepth ?? null
|
|
8
|
+
const maxDepth = options['max-level'] ?? options.maxDepth ?? 3
|
|
8
9
|
const ignore = options.ignoreSegments ?? []
|
|
9
10
|
const segments = targetVal
|
|
10
11
|
.split('/')
|
|
@@ -14,7 +15,11 @@ module.exports = (targetVal, options, context) => {
|
|
|
14
15
|
|
|
15
16
|
const depth = segments.length;
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
const isOutOfRange = minDepth != null
|
|
19
|
+
? (depth >= minDepth && depth <= maxDepth)
|
|
20
|
+
: depth > maxDepth
|
|
21
|
+
|
|
22
|
+
if (isOutOfRange) {
|
|
18
23
|
return [
|
|
19
24
|
{ message: context.rule.message }
|
|
20
25
|
]
|
|
@@ -4,10 +4,29 @@ const VALID_FORMATS = new Set([
|
|
|
4
4
|
'json', 'xml', 'base64'
|
|
5
5
|
]);
|
|
6
6
|
|
|
7
|
+
function isValidPattern(pattern) {
|
|
8
|
+
try {
|
|
9
|
+
new RegExp(pattern);
|
|
10
|
+
return true;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
module.exports = (targetVal, _options, context) => {
|
|
17
|
+
const typePath = [...context.path, 'type'];
|
|
8
18
|
const format = targetVal.format;
|
|
9
|
-
if (format !== undefined && format !== null
|
|
10
|
-
|
|
19
|
+
if (format !== undefined && format !== null) {
|
|
20
|
+
if (!VALID_FORMATS.has(String(format).toLowerCase())) {
|
|
21
|
+
return [{ message: context.rule.message, path: typePath }];
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
11
24
|
}
|
|
12
|
-
|
|
25
|
+
|
|
26
|
+
const pattern = targetVal.pattern;
|
|
27
|
+
if (pattern !== undefined && pattern !== null && String(pattern).length > 0 && isValidPattern(String(pattern))) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [{ message: context.rule.message, path: typePath }];
|
|
13
32
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const DEFAULT_VALID_VERSIONS = '2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} targetVal
|
|
5
|
+
* @param {object} options
|
|
6
|
+
*/
|
|
7
|
+
module.exports = (targetVal, options) => {
|
|
8
|
+
const raw = (options && options['valid-versions']) || DEFAULT_VALID_VERSIONS;
|
|
9
|
+
const validVersions = (Array.isArray(raw) ? raw : String(raw).split(','))
|
|
10
|
+
.map(version => String(version).trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
|
|
13
|
+
if (!validVersions.includes(String(targetVal))) {
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
message: `OAR085: The OpenAPI version '${targetVal}' is not allowed. Allowed versions: ${validVersions.join(', ')}.`
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @param {object} targetVal
|
|
3
|
-
* @param {object}
|
|
3
|
+
* @param {object} options
|
|
4
4
|
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
5
|
*/
|
|
6
|
-
module.exports = (targetVal,
|
|
6
|
+
module.exports = (targetVal, options, context) => {
|
|
7
7
|
const errors = [];
|
|
8
|
-
const
|
|
8
|
+
const dataProperty = (options && options['data-property']) || 'data';
|
|
9
|
+
const VALID_NAMES = [dataProperty, 'error'];
|
|
9
10
|
|
|
10
11
|
if (!targetVal || typeof targetVal !== 'object') {
|
|
11
12
|
return errors;
|
|
@@ -20,8 +21,8 @@ module.exports = (targetVal, _options, context) => {
|
|
|
20
21
|
for (const [propName, propValue] of Object.entries(targetVal)) {
|
|
21
22
|
if (!VALID_NAMES.includes(propName)) {
|
|
22
23
|
errors.push({
|
|
23
|
-
message:
|
|
24
|
-
path: [propName]
|
|
24
|
+
message: `OAR038: Response property must be named '${dataProperty}' or 'error'. Got '${propName}'.`,
|
|
25
|
+
path: [...path, propName]
|
|
25
26
|
});
|
|
26
27
|
} else {
|
|
27
28
|
const subProps = propValue && propValue.properties;
|
|
@@ -29,7 +30,7 @@ module.exports = (targetVal, _options, context) => {
|
|
|
29
30
|
if (!hasSubProps) {
|
|
30
31
|
errors.push({
|
|
31
32
|
message: `OAR038: Property '${propName}' must have at least one sub-property.`,
|
|
32
|
-
path: [propName]
|
|
33
|
+
path: [...path, propName]
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
}
|