api-quality-spectral-ruleset 1.4.0-beta.5 → 1.4.0
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 +34 -46
- 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-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."
|
|
@@ -537,11 +519,15 @@ rules:
|
|
|
537
519
|
function: apq-schema-format
|
|
538
520
|
apiq:OAR038:
|
|
539
521
|
description: "The 201 response schema of a POST operation must have properties named 'data' or 'error' with at least one sub-property."
|
|
540
|
-
message: "
|
|
522
|
+
message: "{{error}}"
|
|
541
523
|
severity: error
|
|
542
|
-
given:
|
|
524
|
+
given:
|
|
525
|
+
- "$.paths.*.post.responses.*.content..schema.properties"
|
|
526
|
+
- "$.paths.*.post.responses.*.schema.properties"
|
|
543
527
|
then:
|
|
544
528
|
function: apq-valid-response-schema
|
|
529
|
+
functionOptions:
|
|
530
|
+
data-property: data
|
|
545
531
|
apiq:OAR039:
|
|
546
532
|
description: "Response codes must be defined according to the standard depending on the HTTP verb and resource path."
|
|
547
533
|
message: "{{error}}"
|
|
@@ -580,13 +566,13 @@ rules:
|
|
|
580
566
|
;patch:^/[^/{}]+/(\{[^/{}]+\}|\bme\b)/[^/{}]+/(\{[^/{}]+\}|\bme\b)/[^/{}]+/(\{[^/{}]+\}|\bme\b)$:200,404,400,415,500,503
|
|
581
567
|
apiq:OAR040:
|
|
582
568
|
description: "A scope name non-compliant with the standard may cause problems at application level."
|
|
583
|
-
message: "OAR040: Scope name
|
|
569
|
+
message: "OAR040: Scope name value is non compliant with the standard."
|
|
584
570
|
severity: "error"
|
|
585
571
|
given: "$.x-wso2-security.apim.x-wso2-scopes[*].name"
|
|
586
572
|
then:
|
|
587
573
|
function: pattern
|
|
588
574
|
functionOptions:
|
|
589
|
-
match: "^
|
|
575
|
+
match: "^[a-zA-Z]{4,}_(SC|sc)_[a-zA-Z0-9]{1,}$"
|
|
590
576
|
apiq:OAR041:
|
|
591
577
|
description: Available scopes must be defined at the end of the document, at the root level, inside `x-wso2-security`.
|
|
592
578
|
message: "OAR041:Scopes must be defined in `x-wso2-security` at the end of the document, before the paths."
|
|
@@ -1021,7 +1007,7 @@ rules:
|
|
|
1021
1007
|
function: apq-password-format
|
|
1022
1008
|
apiq:OAR082:
|
|
1023
1009
|
description: "The string properties 'product', 'line', and 'price' must define a byte or binary format."
|
|
1024
|
-
message: "
|
|
1010
|
+
message: "{{error}}"
|
|
1025
1011
|
severity: error
|
|
1026
1012
|
given: "$..[?(@.properties)]"
|
|
1027
1013
|
then:
|
|
@@ -1050,13 +1036,15 @@ rules:
|
|
|
1050
1036
|
notMatch: "^password$"
|
|
1051
1037
|
apiq:OAR085:
|
|
1052
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."
|
|
1053
|
-
message: "
|
|
1039
|
+
message: "{{error}}"
|
|
1054
1040
|
severity: warn
|
|
1055
|
-
given:
|
|
1041
|
+
given:
|
|
1042
|
+
- "$.openapi"
|
|
1043
|
+
- "$.swagger"
|
|
1056
1044
|
then:
|
|
1057
|
-
function:
|
|
1045
|
+
function: apq-valid-openapi-version
|
|
1058
1046
|
functionOptions:
|
|
1059
|
-
|
|
1047
|
+
valid-versions: "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0"
|
|
1060
1048
|
apiq:OAR086:
|
|
1061
1049
|
description: "Descriptions must begin with a capital letter, end with a period, and not be empty."
|
|
1062
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
|
]
|
|
@@ -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
|
}
|