api-quality-spectral-ruleset 1.4.1 → 1.5.0-beta.1
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 +15 -18
- package/functions/apq-binary-format-check.js +7 -5
- package/functions/apq-numeric-parameter-integrity.js +3 -1
- package/functions/apq-numeric-path-param.js +34 -0
- package/functions/apq-required-fields-exist.js +4 -1
- package/functions/apq-schema-format.js +6 -0
- package/functions/apq-valid-openapi-version.js +1 -1
- package/functions/apq-wso2-scopes-valid.js +58 -0
- package/package.json +1 -1
package/apq-spectral.yaml
CHANGED
|
@@ -30,6 +30,8 @@ functions:
|
|
|
30
30
|
- apq-status-endpoint-check
|
|
31
31
|
- apq-validate-structure
|
|
32
32
|
- apq-numeric-parameter-integrity
|
|
33
|
+
- apq-wso2-scopes-valid
|
|
34
|
+
- apq-numeric-path-param
|
|
33
35
|
extends:
|
|
34
36
|
- spectral:asyncapi
|
|
35
37
|
- spectral:oas
|
|
@@ -46,14 +48,12 @@ rules:
|
|
|
46
48
|
match: "^https://"
|
|
47
49
|
apiq:OAR002:
|
|
48
50
|
description: "A wrong scope definition may cause problems to import the API definition into WSO2."
|
|
49
|
-
message: "
|
|
51
|
+
message: "{{error}}"
|
|
50
52
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR002.md"
|
|
51
53
|
severity: "error"
|
|
52
|
-
given: "$.x-wso2-security.apim.x-wso2-scopes
|
|
54
|
+
given: "$.x-wso2-security.apim.x-wso2-scopes"
|
|
53
55
|
then:
|
|
54
|
-
|
|
55
|
-
field: "key"
|
|
56
|
-
message: "Scope must define a 'key' attribute."
|
|
56
|
+
function: apq-wso2-scopes-valid
|
|
57
57
|
apiq:OAR003:
|
|
58
58
|
description: "A description can help other developers to understand the correct use of the scope."
|
|
59
59
|
message: "OAR003: Scope must define a 'description' attribute."
|
|
@@ -194,7 +194,7 @@ rules:
|
|
|
194
194
|
message: "OAR016: Numeric types must use a valid format for their type."
|
|
195
195
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR016.md"
|
|
196
196
|
severity: error
|
|
197
|
-
given: "$..[?(@ && (@.type=='number' || @.type=='integer'))]"
|
|
197
|
+
given: "$..[?(@ && (@.type=='number' || @.type=='integer' || (@.type && @.type.indexOf && (@.type.indexOf('number') > -1 || @.type.indexOf('integer') > -1))))]"
|
|
198
198
|
then:
|
|
199
199
|
function: schema
|
|
200
200
|
functionOptions:
|
|
@@ -485,7 +485,7 @@ rules:
|
|
|
485
485
|
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."
|
|
486
486
|
severity: error
|
|
487
487
|
resolved: false
|
|
488
|
-
given: "$..[?(@ && @.type
|
|
488
|
+
given: "$..[?(@ && @.type)]"
|
|
489
489
|
then:
|
|
490
490
|
function: apq-schema-format
|
|
491
491
|
apiq:OAR038:
|
|
@@ -686,7 +686,7 @@ rules:
|
|
|
686
686
|
message: "OAR052: Numeric types requires a format"
|
|
687
687
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR052.md"
|
|
688
688
|
severity: warn
|
|
689
|
-
given: "$..[?(@ && (@.type == 'integer' || @.type == 'number'))]"
|
|
689
|
+
given: "$..[?(@ && (@.type == 'integer' || @.type == 'number' || (@.type && @.type.indexOf && (@.type.indexOf('integer') > -1 || @.type.indexOf('number') > -1))))]"
|
|
690
690
|
then:
|
|
691
691
|
field: format
|
|
692
692
|
function: truthy
|
|
@@ -836,10 +836,7 @@ rules:
|
|
|
836
836
|
severity: error
|
|
837
837
|
given: "$.paths[*][get,post,put,patch,delete].parameters[?(@.in == 'path')]"
|
|
838
838
|
then:
|
|
839
|
-
|
|
840
|
-
function: pattern
|
|
841
|
-
functionOptions:
|
|
842
|
-
match: "^(?!integer|number)"
|
|
839
|
+
function: apq-numeric-path-param
|
|
843
840
|
apiq:OAR071:
|
|
844
841
|
description: "Query parameters 'param1', 'param2', and 'param3' must be defined in the operation."
|
|
845
842
|
message: "OAR071: Query parameters 'param1', 'param2', and 'param3' must be defined."
|
|
@@ -876,15 +873,15 @@ rules:
|
|
|
876
873
|
severity: error
|
|
877
874
|
resolved: false
|
|
878
875
|
given:
|
|
879
|
-
- "$..[?(@ && @.in && @.schema && (@.schema.type == 'integer' || @.schema.type == 'number'))].schema"
|
|
880
|
-
- "$..[?(@ && @.in && (@.type == 'integer' || @.type == 'number'))]"
|
|
876
|
+
- "$..[?(@ && @.in && @.schema && (@.schema.type == 'integer' || @.schema.type == 'number' || (@.schema.type && @.schema.type.indexOf && (@.schema.type.indexOf('integer') > -1 || @.schema.type.indexOf('number') > -1))))].schema"
|
|
877
|
+
- "$..[?(@ && @.in && (@.type == 'integer' || @.type == 'number' || (@.type && @.type.indexOf && (@.type.indexOf('integer') > -1 || @.type.indexOf('number') > -1))))]"
|
|
881
878
|
then:
|
|
882
879
|
function: apq-numeric-parameter-integrity
|
|
883
880
|
apiq:OAR075:
|
|
884
881
|
description: "String parameters should have minLength, maxLength, pattern (regular expression), or enum restriction."
|
|
885
882
|
message: "OAR075: String parameters should have minLength, maxLength, pattern, or enum restriction."
|
|
886
883
|
severity: error
|
|
887
|
-
given: "$.paths[*][get,post,put,patch,delete].parameters[?(@ && @.schema && @.schema.type == 'string' && @.in == 'path')]"
|
|
884
|
+
given: "$.paths[*][get,post,put,patch,delete].parameters[?(@ && @.schema && (@.schema.type == 'string' || (@.schema.type && @.schema.type.indexOf && @.schema.type.indexOf('string') > -1)) && @.in == 'path')]"
|
|
888
885
|
then:
|
|
889
886
|
function: schema
|
|
890
887
|
functionOptions:
|
|
@@ -902,7 +899,7 @@ rules:
|
|
|
902
899
|
message: "OAR076: Schema should use well-defined type and format."
|
|
903
900
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR076.md"
|
|
904
901
|
severity: error
|
|
905
|
-
given: "$..[?(@ && (@.type=='number' || @.type=='integer'))]"
|
|
902
|
+
given: "$..[?(@ && (@.type=='number' || @.type=='integer' || (@.type && @.type.indexOf && (@.type.indexOf('number') > -1 || @.type.indexOf('integer') > -1))))]"
|
|
906
903
|
then:
|
|
907
904
|
function: schema
|
|
908
905
|
functionOptions:
|
|
@@ -1000,7 +997,7 @@ rules:
|
|
|
1000
997
|
functionOptions:
|
|
1001
998
|
notMatch: "^password$"
|
|
1002
999
|
apiq:OAR085:
|
|
1003
|
-
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, 3.2.0."
|
|
1000
|
+
description: "The OpenAPI version must be one of: 2.0, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.0.4, 3.1.0, 3.1.1, 3.1.2, 3.2.0."
|
|
1004
1001
|
message: "{{error}}"
|
|
1005
1002
|
severity: warn
|
|
1006
1003
|
given:
|
|
@@ -1009,7 +1006,7 @@ rules:
|
|
|
1009
1006
|
then:
|
|
1010
1007
|
function: apq-valid-openapi-version
|
|
1011
1008
|
functionOptions:
|
|
1012
|
-
valid-versions: "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.2.0"
|
|
1009
|
+
valid-versions: "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.0.4,3.1.0,3.1.1,3.1.2,3.2.0"
|
|
1013
1010
|
apiq:OAR086:
|
|
1014
1011
|
description: "Descriptions must begin with a capital letter, end with a period, and not be empty."
|
|
1015
1012
|
message: "OAR086: Descriptions must begin with a capital letter, end with a period, and not be empty."
|
|
@@ -5,7 +5,6 @@ module.exports = (schema, options = {}, context) => {
|
|
|
5
5
|
return results;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
// Fields that must have byte or binary format
|
|
9
8
|
const fieldsToCheck = (options['fields-to-apply'] || 'product,line,price')
|
|
10
9
|
.split(',')
|
|
11
10
|
.map(f => f.trim().toLowerCase());
|
|
@@ -13,7 +12,6 @@ module.exports = (schema, options = {}, context) => {
|
|
|
13
12
|
const properties = schema.properties || {};
|
|
14
13
|
|
|
15
14
|
Object.entries(properties).forEach(([propName, propSchema]) => {
|
|
16
|
-
// Only check if this property is in our list
|
|
17
15
|
if (!fieldsToCheck.includes(propName.toLowerCase())) {
|
|
18
16
|
return;
|
|
19
17
|
}
|
|
@@ -22,13 +20,17 @@ module.exports = (schema, options = {}, context) => {
|
|
|
22
20
|
return;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const type = propSchema.type;
|
|
24
|
+
const isStringType = type === 'string' || (Array.isArray(type) && type.includes('string'));
|
|
25
|
+
if (!isStringType) {
|
|
27
26
|
return;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
const format = propSchema.format || '';
|
|
31
|
-
|
|
30
|
+
const hasBinaryFormat = format === 'byte' || format === 'binary';
|
|
31
|
+
const hasContentEncoding = propSchema.contentEncoding !== undefined
|
|
32
|
+
|| propSchema.contentMediaType !== undefined;
|
|
33
|
+
if (!hasBinaryFormat && !hasContentEncoding) {
|
|
32
34
|
results.push({
|
|
33
35
|
message: `OAR082: Property '${propName}' must define a 'byte' or 'binary' format (currently: ${format || 'missing'}).`,
|
|
34
36
|
path: [...context.path, 'properties', propName, 'type'],
|
|
@@ -9,7 +9,9 @@ module.exports = (node, options, context) => {
|
|
|
9
9
|
if (!node || typeof node !== 'object') return results;
|
|
10
10
|
|
|
11
11
|
const { type } = node;
|
|
12
|
-
|
|
12
|
+
const types = Array.isArray(type) ? type : [type];
|
|
13
|
+
const isNumeric = types.indexOf('integer') > -1 || types.indexOf('number') > -1;
|
|
14
|
+
if (!isNumeric) return results;
|
|
13
15
|
|
|
14
16
|
const has = (field) => Object.prototype.hasOwnProperty.call(node, field);
|
|
15
17
|
const hasMin = has('minimum');
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} param the parameter object (given selects params with `in: path`)
|
|
3
|
+
* @param {object} options
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = (param, options, context) => {
|
|
7
|
+
const results = [];
|
|
8
|
+
if (!param || typeof param !== 'object' || param.in !== 'path') return results;
|
|
9
|
+
|
|
10
|
+
let typeVal;
|
|
11
|
+
let typePath;
|
|
12
|
+
if (param.schema && param.schema.type !== undefined) {
|
|
13
|
+
typeVal = param.schema.type;
|
|
14
|
+
typePath = [...context.path, 'schema', 'type'];
|
|
15
|
+
} else if (param.type !== undefined) {
|
|
16
|
+
typeVal = param.type;
|
|
17
|
+
typePath = [...context.path, 'type'];
|
|
18
|
+
} else {
|
|
19
|
+
return results;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const types = Array.isArray(typeVal) ? typeVal : [typeVal];
|
|
23
|
+
const isNumeric = types.indexOf('integer') > -1
|
|
24
|
+
|| types.indexOf('number') > -1
|
|
25
|
+
|| types.indexOf('float') > -1;
|
|
26
|
+
if (!isNumeric) return results;
|
|
27
|
+
|
|
28
|
+
const ruleCode = context.rule.name.split(':').pop();
|
|
29
|
+
results.push({
|
|
30
|
+
message: `${ruleCode}: Parameters in path should not be numeric.`,
|
|
31
|
+
path: typePath,
|
|
32
|
+
});
|
|
33
|
+
return results;
|
|
34
|
+
};
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
module.exports = (given, options, context) => {
|
|
7
7
|
const results = [];
|
|
8
8
|
|
|
9
|
-
if (!given
|
|
9
|
+
if (!given) return results;
|
|
10
|
+
const t = given.type;
|
|
11
|
+
const isObjectType = t === 'object' || (Array.isArray(t) && t.indexOf('object') > -1);
|
|
12
|
+
if (!isObjectType) return results;
|
|
10
13
|
|
|
11
14
|
const { required, properties } = given;
|
|
12
15
|
if (!Array.isArray(required)) return results;
|
|
@@ -16,6 +16,12 @@ function isValidPattern(pattern) {
|
|
|
16
16
|
module.exports = (targetVal, _options, context) => {
|
|
17
17
|
const typePath = [...context.path, 'type'];
|
|
18
18
|
|
|
19
|
+
const type = targetVal.type;
|
|
20
|
+
const isStringType = type === 'string' || (Array.isArray(type) && type.includes('string'));
|
|
21
|
+
if (!isStringType) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
const format = targetVal.format;
|
|
20
26
|
if (format !== undefined && format !== null) {
|
|
21
27
|
if (!VALID_FORMATS.has(String(format).toLowerCase())) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {*} targetVal the `x-wso2-scopes` container node value
|
|
3
|
+
* @param {object} _options unused
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
const DEFINITION_WRONG = "OAR002: WSO2 scopes definition is wrong";
|
|
7
|
+
const REQUIRED_PROP = "OAR002: WSO2 scope '{0}' is required";
|
|
8
|
+
const REQUIRED_PROPERTIES = ['name', 'key', 'roles'];
|
|
9
|
+
|
|
10
|
+
const requiredMessage = (prop) => REQUIRED_PROP.replace('{0}', prop);
|
|
11
|
+
|
|
12
|
+
// A scope property counts as "required but missing" when it is absent, null, a
|
|
13
|
+
// blank string, or an empty collection ([] / {}) — mirrors Sonar's OAR002 check.
|
|
14
|
+
const isEmptyValue = (value) =>
|
|
15
|
+
value === null
|
|
16
|
+
|| (typeof value === 'string' && value.trim() === '')
|
|
17
|
+
|| (Array.isArray(value) && value.length === 0)
|
|
18
|
+
|| (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
|
|
19
|
+
|
|
20
|
+
module.exports = (targetVal, _options, context) => {
|
|
21
|
+
const errors = [];
|
|
22
|
+
const basePath = context.path || [];
|
|
23
|
+
|
|
24
|
+
if (targetVal === null || typeof targetVal !== 'object') {
|
|
25
|
+
errors.push({ message: DEFINITION_WRONG, path: basePath });
|
|
26
|
+
return errors;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const entries = Array.isArray(targetVal)
|
|
30
|
+
? targetVal.map((scope, index) => [index, scope])
|
|
31
|
+
: Object.keys(targetVal).map((key) => [key, targetVal[key]]);
|
|
32
|
+
|
|
33
|
+
if (entries.length === 0) {
|
|
34
|
+
errors.push({ message: DEFINITION_WRONG, path: basePath });
|
|
35
|
+
return errors;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
entries.forEach(([key, scope]) => {
|
|
39
|
+
const scopePath = [...basePath, key];
|
|
40
|
+
|
|
41
|
+
if (scope === null || typeof scope !== 'object' || Array.isArray(scope)) {
|
|
42
|
+
REQUIRED_PROPERTIES.forEach((prop) => {
|
|
43
|
+
errors.push({ message: requiredMessage(prop), path: scopePath });
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
REQUIRED_PROPERTIES.forEach((prop) => {
|
|
49
|
+
if (!Object.prototype.hasOwnProperty.call(scope, prop)) {
|
|
50
|
+
errors.push({ message: requiredMessage(prop), path: scopePath });
|
|
51
|
+
} else if (isEmptyValue(scope[prop])) {
|
|
52
|
+
errors.push({ message: requiredMessage(prop), path: [...scopePath, prop] });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return errors;
|
|
58
|
+
};
|