api-quality-spectral-ruleset 1.4.1 → 1.5.0-beta.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 +37 -22
- 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-path-pattern.js +14 -0
- package/functions/apq-query-params-optional.js +182 -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,10 @@ functions:
|
|
|
30
30
|
- apq-status-endpoint-check
|
|
31
31
|
- apq-validate-structure
|
|
32
32
|
- apq-numeric-parameter-integrity
|
|
33
|
+
- apq-path-pattern
|
|
34
|
+
- apq-query-params-optional
|
|
35
|
+
- apq-wso2-scopes-valid
|
|
36
|
+
- apq-numeric-path-param
|
|
33
37
|
extends:
|
|
34
38
|
- spectral:asyncapi
|
|
35
39
|
- spectral:oas
|
|
@@ -46,14 +50,12 @@ rules:
|
|
|
46
50
|
match: "^https://"
|
|
47
51
|
apiq:OAR002:
|
|
48
52
|
description: "A wrong scope definition may cause problems to import the API definition into WSO2."
|
|
49
|
-
message: "
|
|
53
|
+
message: "{{error}}"
|
|
50
54
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR002.md"
|
|
51
55
|
severity: "error"
|
|
52
|
-
given: "$.x-wso2-security.apim.x-wso2-scopes
|
|
56
|
+
given: "$.x-wso2-security.apim.x-wso2-scopes"
|
|
53
57
|
then:
|
|
54
|
-
|
|
55
|
-
field: "key"
|
|
56
|
-
message: "Scope must define a 'key' attribute."
|
|
58
|
+
function: apq-wso2-scopes-valid
|
|
57
59
|
apiq:OAR003:
|
|
58
60
|
description: "A description can help other developers to understand the correct use of the scope."
|
|
59
61
|
message: "OAR003: Scope must define a 'description' attribute."
|
|
@@ -194,7 +196,7 @@ rules:
|
|
|
194
196
|
message: "OAR016: Numeric types must use a valid format for their type."
|
|
195
197
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR016.md"
|
|
196
198
|
severity: error
|
|
197
|
-
given: "$..[?(@ && (@.type=='number' || @.type=='integer'))]"
|
|
199
|
+
given: "$..[?(@ && (@.type=='number' || @.type=='integer' || (@.type && @.type.indexOf && (@.type.indexOf('number') > -1 || @.type.indexOf('integer') > -1))))]"
|
|
198
200
|
then:
|
|
199
201
|
function: schema
|
|
200
202
|
functionOptions:
|
|
@@ -485,7 +487,7 @@ rules:
|
|
|
485
487
|
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
488
|
severity: error
|
|
487
489
|
resolved: false
|
|
488
|
-
given: "$..[?(@ && @.type
|
|
490
|
+
given: "$..[?(@ && @.type)]"
|
|
489
491
|
then:
|
|
490
492
|
function: apq-schema-format
|
|
491
493
|
apiq:OAR038:
|
|
@@ -686,7 +688,7 @@ rules:
|
|
|
686
688
|
message: "OAR052: Numeric types requires a format"
|
|
687
689
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR052.md"
|
|
688
690
|
severity: warn
|
|
689
|
-
given: "$..[?(@ && (@.type == 'integer' || @.type == 'number'))]"
|
|
691
|
+
given: "$..[?(@ && (@.type == 'integer' || @.type == 'number' || (@.type && @.type.indexOf && (@.type.indexOf('integer') > -1 || @.type.indexOf('number') > -1))))]"
|
|
690
692
|
then:
|
|
691
693
|
field: format
|
|
692
694
|
function: truthy
|
|
@@ -716,12 +718,18 @@ rules:
|
|
|
716
718
|
match: ^(http(s)?:\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.apiquality.io\b([-a-zA-Z0-9@:%_\+.~#?&\/=]*)$
|
|
717
719
|
apiq:OAR060:
|
|
718
720
|
description: "All query parameters must be defined as optional."
|
|
719
|
-
message: "
|
|
721
|
+
message: "{{error}}"
|
|
720
722
|
severity: error
|
|
721
|
-
given:
|
|
723
|
+
given:
|
|
724
|
+
- "$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[?(@.in == 'query')]"
|
|
725
|
+
- "$.paths[*].parameters[?(@.in == 'query')]"
|
|
726
|
+
- "$.components.parameters[?(@.in == 'query')]"
|
|
727
|
+
- "$.parameters[?(@.in == 'query')]"
|
|
722
728
|
then:
|
|
723
729
|
field: "required"
|
|
724
|
-
function:
|
|
730
|
+
function: apq-query-params-optional
|
|
731
|
+
functionOptions:
|
|
732
|
+
path-exclusions: "/status"
|
|
725
733
|
apiq:OAR061:
|
|
726
734
|
description: "Ensure get have mandatory response codes"
|
|
727
735
|
message: "OAR061: Ensure get have the mandatory response codes"
|
|
@@ -836,10 +844,7 @@ rules:
|
|
|
836
844
|
severity: error
|
|
837
845
|
given: "$.paths[*][get,post,put,patch,delete].parameters[?(@.in == 'path')]"
|
|
838
846
|
then:
|
|
839
|
-
|
|
840
|
-
function: pattern
|
|
841
|
-
functionOptions:
|
|
842
|
-
match: "^(?!integer|number)"
|
|
847
|
+
function: apq-numeric-path-param
|
|
843
848
|
apiq:OAR071:
|
|
844
849
|
description: "Query parameters 'param1', 'param2', and 'param3' must be defined in the operation."
|
|
845
850
|
message: "OAR071: Query parameters 'param1', 'param2', and 'param3' must be defined."
|
|
@@ -876,15 +881,15 @@ rules:
|
|
|
876
881
|
severity: error
|
|
877
882
|
resolved: false
|
|
878
883
|
given:
|
|
879
|
-
- "$..[?(@ && @.in && @.schema && (@.schema.type == 'integer' || @.schema.type == 'number'))].schema"
|
|
880
|
-
- "$..[?(@ && @.in && (@.type == 'integer' || @.type == 'number'))]"
|
|
884
|
+
- "$..[?(@ && @.in && @.schema && (@.schema.type == 'integer' || @.schema.type == 'number' || (@.schema.type && @.schema.type.indexOf && (@.schema.type.indexOf('integer') > -1 || @.schema.type.indexOf('number') > -1))))].schema"
|
|
885
|
+
- "$..[?(@ && @.in && (@.type == 'integer' || @.type == 'number' || (@.type && @.type.indexOf && (@.type.indexOf('integer') > -1 || @.type.indexOf('number') > -1))))]"
|
|
881
886
|
then:
|
|
882
887
|
function: apq-numeric-parameter-integrity
|
|
883
888
|
apiq:OAR075:
|
|
884
889
|
description: "String parameters should have minLength, maxLength, pattern (regular expression), or enum restriction."
|
|
885
890
|
message: "OAR075: String parameters should have minLength, maxLength, pattern, or enum restriction."
|
|
886
891
|
severity: error
|
|
887
|
-
given: "$.paths[*][get,post,put,patch,delete].parameters[?(@ && @.schema && @.schema.type == 'string' && @.in == 'path')]"
|
|
892
|
+
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
893
|
then:
|
|
889
894
|
function: schema
|
|
890
895
|
functionOptions:
|
|
@@ -902,7 +907,7 @@ rules:
|
|
|
902
907
|
message: "OAR076: Schema should use well-defined type and format."
|
|
903
908
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR076.md"
|
|
904
909
|
severity: error
|
|
905
|
-
given: "$..[?(@ && (@.type=='number' || @.type=='integer'))]"
|
|
910
|
+
given: "$..[?(@ && (@.type=='number' || @.type=='integer' || (@.type && @.type.indexOf && (@.type.indexOf('number') > -1 || @.type.indexOf('integer') > -1))))]"
|
|
906
911
|
then:
|
|
907
912
|
function: schema
|
|
908
913
|
functionOptions:
|
|
@@ -1000,7 +1005,7 @@ rules:
|
|
|
1000
1005
|
functionOptions:
|
|
1001
1006
|
notMatch: "^password$"
|
|
1002
1007
|
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."
|
|
1008
|
+
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
1009
|
message: "{{error}}"
|
|
1005
1010
|
severity: warn
|
|
1006
1011
|
given:
|
|
@@ -1009,7 +1014,7 @@ rules:
|
|
|
1009
1014
|
then:
|
|
1010
1015
|
function: apq-valid-openapi-version
|
|
1011
1016
|
functionOptions:
|
|
1012
|
-
valid-versions: "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.2.0"
|
|
1017
|
+
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
1018
|
apiq:OAR086:
|
|
1014
1019
|
description: "Descriptions must begin with a capital letter, end with a period, and not be empty."
|
|
1015
1020
|
message: "OAR086: Descriptions must begin with a capital letter, end with a period, and not be empty."
|
|
@@ -1264,4 +1269,14 @@ rules:
|
|
|
1264
1269
|
- "$.components.schemas[*]"
|
|
1265
1270
|
- "$.definitions[*]"
|
|
1266
1271
|
then:
|
|
1267
|
-
function: apq-required-fields-exist
|
|
1272
|
+
function: apq-required-fields-exist
|
|
1273
|
+
apiq:OAR116:
|
|
1274
|
+
description: "Every API path must match the configured regular expression."
|
|
1275
|
+
message: "{{error}}"
|
|
1276
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR116.md"
|
|
1277
|
+
severity: error
|
|
1278
|
+
given: "$.paths.*~"
|
|
1279
|
+
then:
|
|
1280
|
+
function: apq-path-pattern
|
|
1281
|
+
functionOptions:
|
|
1282
|
+
pattern: "^/"
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = (targetVal, options = {}) => {
|
|
2
|
+
const patternStr = (options && options.pattern) || '^/';
|
|
3
|
+
|
|
4
|
+
if (typeof targetVal !== 'string') {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const regex = new RegExp(patternStr);
|
|
9
|
+
if (!regex.test(targetVal)) {
|
|
10
|
+
return [{ message: `OAR116: Path does not match the required pattern: ${patternStr}` }];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return [];
|
|
14
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const HTTP_VERBS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
|
|
2
|
+
|
|
3
|
+
// Guards against a cycle in a `$ref` chain (A -> B -> A).
|
|
4
|
+
const MAX_REF_DEPTH = 10;
|
|
5
|
+
|
|
6
|
+
// The `$ref` usage map depends only on the source document, so cache it per document instead of
|
|
7
|
+
// rebuilding it on every matched parameter.
|
|
8
|
+
const usageCache = new WeakMap();
|
|
9
|
+
|
|
10
|
+
const issue = () => [{ message: 'OAR060: All query parameter must be optional (required: false).' }];
|
|
11
|
+
|
|
12
|
+
function parseExclusions(value) {
|
|
13
|
+
return new Set(
|
|
14
|
+
String(value === undefined || value === null ? '' : value)
|
|
15
|
+
.split(',')
|
|
16
|
+
.map((p) => p.trim())
|
|
17
|
+
.filter(Boolean),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Split a local JSON pointer (`#/components/parameters/Foo`) into its decoded segments.
|
|
23
|
+
* Returns null for external refs and for anything that is not a document-local pointer.
|
|
24
|
+
*/
|
|
25
|
+
function refToSegments(ref) {
|
|
26
|
+
if (typeof ref !== 'string' || !ref.startsWith('#/')) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return ref.slice(2).split('/').map((raw) => {
|
|
30
|
+
let segment = raw;
|
|
31
|
+
try {
|
|
32
|
+
segment = decodeURIComponent(raw);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
segment = raw;
|
|
35
|
+
}
|
|
36
|
+
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getAt(doc, segments) {
|
|
41
|
+
return segments.reduce(
|
|
42
|
+
(node, segment) => (node !== null && typeof node === 'object' ? node[segment] : undefined),
|
|
43
|
+
doc,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const keyOf = (segments) => JSON.stringify(segments);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The two containers the rule's `given` scans for shared parameter definitions:
|
|
51
|
+
* `components.parameters.<name>` (OpenAPI 3) and `parameters.<name>` (OpenAPI 2).
|
|
52
|
+
*/
|
|
53
|
+
function definitionSegments(path) {
|
|
54
|
+
if (path[0] === 'components' && path[1] === 'parameters' && path.length >= 3) {
|
|
55
|
+
return path.slice(0, 3);
|
|
56
|
+
}
|
|
57
|
+
if (path[0] === 'parameters' && path.length >= 2) {
|
|
58
|
+
return path.slice(0, 2);
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const isSharedDefinitionRef = (segments) => segments !== null
|
|
64
|
+
&& ((segments.length === 3 && segments[0] === 'components' && segments[1] === 'parameters')
|
|
65
|
+
|| (segments.length === 2 && segments[0] === 'parameters'));
|
|
66
|
+
|
|
67
|
+
/** Map of shared-definition pointer -> set of API paths that reference it through a `$ref`. */
|
|
68
|
+
function buildRefUsages(doc) {
|
|
69
|
+
const usages = new Map();
|
|
70
|
+
const paths = doc && typeof doc === 'object' ? doc.paths : undefined;
|
|
71
|
+
if (!paths || typeof paths !== 'object') {
|
|
72
|
+
return usages;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Object.keys(paths).forEach((apiPath) => {
|
|
76
|
+
const pathItem = paths[apiPath];
|
|
77
|
+
if (!pathItem || typeof pathItem !== 'object') {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const parameterLists = [pathItem.parameters];
|
|
82
|
+
HTTP_VERBS.forEach((verb) => {
|
|
83
|
+
const operation = pathItem[verb];
|
|
84
|
+
if (operation && typeof operation === 'object') {
|
|
85
|
+
parameterLists.push(operation.parameters);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
parameterLists.forEach((list) => {
|
|
90
|
+
if (!Array.isArray(list)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
list.forEach((parameter) => {
|
|
94
|
+
let current = parameter;
|
|
95
|
+
for (let depth = 0; depth < MAX_REF_DEPTH; depth += 1) {
|
|
96
|
+
if (!current || typeof current !== 'object' || typeof current.$ref !== 'string') {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
const segments = refToSegments(current.$ref);
|
|
100
|
+
if (segments === null) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const key = keyOf(segments);
|
|
104
|
+
if (!usages.has(key)) {
|
|
105
|
+
usages.set(key, new Set());
|
|
106
|
+
}
|
|
107
|
+
usages.get(key).add(apiPath);
|
|
108
|
+
const next = getAt(doc, segments);
|
|
109
|
+
if (!next || next === current) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
current = next;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return usages;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function refUsagesFor(doc) {
|
|
122
|
+
if (!doc || typeof doc !== 'object') {
|
|
123
|
+
return new Map();
|
|
124
|
+
}
|
|
125
|
+
let cached = usageCache.get(doc);
|
|
126
|
+
if (cached === undefined) {
|
|
127
|
+
cached = buildRefUsages(doc);
|
|
128
|
+
usageCache.set(doc, cached);
|
|
129
|
+
}
|
|
130
|
+
return cached;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* True when the match landed on a `$ref` to a shared definition the rule also scans on its own —
|
|
135
|
+
* either a use site under `paths`, or one shared definition aliasing another.
|
|
136
|
+
*
|
|
137
|
+
* Spectral runs the rule over the resolved document, so it matches such a parameter once here and
|
|
138
|
+
* once at the definition it points to. Reporting is left to the definition-site match, which is
|
|
139
|
+
* also the only place Sonar reports it (its AST visits the definition, never the use sites).
|
|
140
|
+
*/
|
|
141
|
+
function isRefToSharedDefinition(source, path) {
|
|
142
|
+
const parameterPath = path[path.length - 1] === 'required' ? path.slice(0, -1) : path;
|
|
143
|
+
const node = getAt(source, parameterPath);
|
|
144
|
+
if (!node || typeof node !== 'object' || typeof node.$ref !== 'string') {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return isSharedDefinitionRef(refToSegments(node.$ref));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = (targetVal, options = {}, context = {}) => {
|
|
151
|
+
const exclusions = parseExclusions((options || {})['path-exclusions']);
|
|
152
|
+
const path = (context && context.path) || [];
|
|
153
|
+
const source = context && context.document ? context.document.data : undefined;
|
|
154
|
+
|
|
155
|
+
if (isRefToSharedDefinition(source, path)) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (path[0] === 'paths') {
|
|
160
|
+
const apiPath = path.find((p) => typeof p === 'string' && p.startsWith('/'));
|
|
161
|
+
if (apiPath && exclusions.has(apiPath)) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
} else if (exclusions.size > 0) {
|
|
165
|
+
// A shared definition has no path of its own: the AST/JSONPath match lands on
|
|
166
|
+
// `components.parameters.<name>` (or `parameters.<name>` in OpenAPI 2). Exclude it only when
|
|
167
|
+
// every path that references it is excluded — an unreferenced definition stays in scope.
|
|
168
|
+
const definition = definitionSegments(path);
|
|
169
|
+
const usages = definition === null
|
|
170
|
+
? undefined
|
|
171
|
+
: refUsagesFor(source).get(keyOf(definition));
|
|
172
|
+
if (usages !== undefined && usages.size > 0 && [...usages].every((u) => exclusions.has(u))) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (targetVal === true || targetVal === 'true') {
|
|
178
|
+
return issue();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [];
|
|
182
|
+
};
|
|
@@ -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
|
+
};
|