api-quality-spectral-ruleset 1.2.0 → 1.3.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
CHANGED
|
@@ -23,6 +23,7 @@ functions:
|
|
|
23
23
|
- apq-binary-format-check
|
|
24
24
|
- apq-paged-response-check
|
|
25
25
|
- apq-status-endpoint-check
|
|
26
|
+
- apq-validate-structure
|
|
26
27
|
extends:
|
|
27
28
|
- spectral:asyncapi
|
|
28
29
|
- spectral:oas
|
|
@@ -436,8 +437,8 @@ rules:
|
|
|
436
437
|
status-endpoint: "/status"
|
|
437
438
|
method: "get"
|
|
438
439
|
apiq:OAR031:
|
|
439
|
-
description: The examples can help developers to understand the response data structure and representation."
|
|
440
|
-
message: "
|
|
440
|
+
description: "The examples can help developers to understand the response data structure and representation."
|
|
441
|
+
message: "{{error}}"
|
|
441
442
|
severity: error
|
|
442
443
|
given:
|
|
443
444
|
- $.paths.*.*.parameters.*
|
|
@@ -576,6 +577,17 @@ rules:
|
|
|
576
577
|
function: pattern
|
|
577
578
|
functionOptions:
|
|
578
579
|
match: "^(/api-[^/]+/v[0-9]+|https?:\\/\\/[^/]+\\/api-[^/]+\\/v[0-9]+)$"
|
|
580
|
+
apiq:OAR043:
|
|
581
|
+
description: "OpenAPI definition contains structural errors that would be detected by a strict parser or validator."
|
|
582
|
+
message: "{{error}}"
|
|
583
|
+
severity: error
|
|
584
|
+
given:
|
|
585
|
+
- $.paths.*.*.parameters.*.in
|
|
586
|
+
- $.components.parameters.*.in
|
|
587
|
+
- $.paths.*.*.parameters.*.schema.type
|
|
588
|
+
- $.components.parameters.*.schema.type
|
|
589
|
+
then:
|
|
590
|
+
function: apq-validate-structure
|
|
579
591
|
apiq:OAR044:
|
|
580
592
|
description: "Declared media type should conform to RFC6838 and RFC7231."
|
|
581
593
|
message: "OAR044: Declared media type range should conform to RFC7231."
|
|
@@ -6,61 +6,87 @@
|
|
|
6
6
|
module.exports = (given, options, context) => {
|
|
7
7
|
if (!given || typeof given !== 'object') return [];
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (node.example !== undefined || node.examples !== undefined) return true;
|
|
13
|
-
|
|
14
|
-
if (node.content && typeof node.content === 'object') {
|
|
15
|
-
return Object.values(node.content).some(mediaType => hasExample(mediaType));
|
|
16
|
-
}
|
|
9
|
+
const issues = [];
|
|
10
|
+
const nodePath = context.path || [];
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
return checkSchema(node.schema);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (node.properties || node.items || node.type) {
|
|
23
|
-
return checkSchema(node);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return false;
|
|
27
|
-
};
|
|
12
|
+
const isParameter = nodePath.length >= 2 && nodePath[nodePath.length - 2] === 'parameters';
|
|
28
13
|
|
|
29
14
|
const checkSchema = (schema) => {
|
|
30
15
|
if (!schema || typeof schema !== 'object') return false;
|
|
31
|
-
|
|
32
16
|
if (schema.example !== undefined || schema.examples !== undefined) return true;
|
|
33
|
-
|
|
34
17
|
if (schema.properties && typeof schema.properties === 'object') {
|
|
35
18
|
return Object.values(schema.properties).some(prop => checkSchema(prop));
|
|
36
19
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
43
|
-
return schema.allOf.some(subSchema => checkSchema(subSchema));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (schema.oneOf && Array.isArray(schema.oneOf)) {
|
|
47
|
-
return schema.oneOf.some(subSchema => checkSchema(subSchema));
|
|
20
|
+
if (schema.items) return checkSchema(schema.items);
|
|
21
|
+
for (const combiner of ['allOf', 'oneOf', 'anyOf']) {
|
|
22
|
+
if (schema[combiner] && Array.isArray(schema[combiner])) {
|
|
23
|
+
if (schema[combiner].some(sub => checkSchema(sub))) return true;
|
|
24
|
+
}
|
|
48
25
|
}
|
|
26
|
+
return false;
|
|
27
|
+
};
|
|
49
28
|
|
|
50
|
-
|
|
51
|
-
|
|
29
|
+
const hasExample = (node) => {
|
|
30
|
+
if (!node || typeof node !== 'object') return false;
|
|
31
|
+
if (node.example !== undefined || node.examples !== undefined) return true;
|
|
32
|
+
if (node.content && typeof node.content === 'object') {
|
|
33
|
+
return Object.values(node.content).some(mediaType => hasExample(mediaType));
|
|
52
34
|
}
|
|
53
|
-
|
|
35
|
+
if (node.schema) return checkSchema(node.schema);
|
|
36
|
+
if (node.properties || node.items || node.type) return checkSchema(node);
|
|
54
37
|
return false;
|
|
55
38
|
};
|
|
56
39
|
|
|
57
40
|
if (!hasExample(given)) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
41
|
+
issues.push({ message: context.rule.message });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!isParameter) {
|
|
45
|
+
const collectPropertyIssues = (schema, path) => {
|
|
46
|
+
if (!schema || typeof schema !== 'object' || schema.$ref) return;
|
|
47
|
+
|
|
48
|
+
if (schema.type === 'array' && schema.items) {
|
|
49
|
+
collectPropertyIssues(schema.items, [...path, 'items']);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (schema.properties && typeof schema.properties === 'object') {
|
|
54
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
55
|
+
if (!propSchema || typeof propSchema !== 'object' || propSchema.$ref) continue;
|
|
56
|
+
|
|
57
|
+
const propType = propSchema.type;
|
|
58
|
+
|
|
59
|
+
if (propType === 'object' || (!propType && propSchema.properties)) {
|
|
60
|
+
collectPropertyIssues(propSchema, [...path, 'properties', propName]);
|
|
61
|
+
} else if (propType === 'array') {
|
|
62
|
+
collectPropertyIssues(propSchema, [...path, 'properties', propName]);
|
|
63
|
+
} else {
|
|
64
|
+
if (propSchema.example === undefined && propSchema.examples === undefined) {
|
|
65
|
+
issues.push({ message: `OAR031: Property '${propName}' is missing an example.` });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const combiner of ['allOf', 'oneOf', 'anyOf']) {
|
|
72
|
+
if (schema[combiner] && Array.isArray(schema[combiner])) {
|
|
73
|
+
schema[combiner].forEach((sub, i) => {
|
|
74
|
+
collectPropertyIssues(sub, [...path, combiner, i]);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (given.content && typeof given.content === 'object') {
|
|
81
|
+
for (const [mediaType, mediaTypeNode] of Object.entries(given.content)) {
|
|
82
|
+
if (mediaTypeNode && mediaTypeNode.schema) {
|
|
83
|
+
collectPropertyIssues(mediaTypeNode.schema, ['content', mediaType, 'schema']);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} else if (given.schema) {
|
|
87
|
+
collectPropertyIssues(given.schema, ['schema']);
|
|
88
|
+
}
|
|
63
89
|
}
|
|
64
90
|
|
|
65
|
-
return
|
|
66
|
-
};
|
|
91
|
+
return issues;
|
|
92
|
+
};
|
|
@@ -10,6 +10,7 @@ module.exports = (targetVal, options, context) => {
|
|
|
10
10
|
const segments = targetVal
|
|
11
11
|
.split('/')
|
|
12
12
|
.filter(Boolean)
|
|
13
|
+
.filter(segment => !segment.startsWith('{') && !segment.endsWith('}'))
|
|
13
14
|
.filter(segment => !ignore.includes(segment))
|
|
14
15
|
|
|
15
16
|
if (segments.length > maxDepth) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} given - The field value
|
|
3
|
+
* @param {object} options
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = (given, options, context) => {
|
|
7
|
+
if (given === null || given === undefined) return [];
|
|
8
|
+
|
|
9
|
+
const VALID_IN_VALUES = ['query', 'header', 'path', 'cookie', 'body', 'formData'];
|
|
10
|
+
const VALID_SCHEMA_TYPES = ['string', 'number', 'integer', 'boolean', 'array', 'object', 'null'];
|
|
11
|
+
|
|
12
|
+
const nodePath = context.path || [];
|
|
13
|
+
const fieldName = nodePath[nodePath.length - 1];
|
|
14
|
+
|
|
15
|
+
if (fieldName === 'in') {
|
|
16
|
+
if (!VALID_IN_VALUES.includes(String(given))) {
|
|
17
|
+
return [{
|
|
18
|
+
message: `in: Expected one of [${VALID_IN_VALUES.map(v => `"${v}"`).join(', ')}]`,
|
|
19
|
+
}];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (fieldName === 'type') {
|
|
24
|
+
if (!VALID_SCHEMA_TYPES.includes(String(given))) {
|
|
25
|
+
return [{
|
|
26
|
+
message: `type: Expected one of [${VALID_SCHEMA_TYPES.map(v => `"${v}"`).join(', ')}]`,
|
|
27
|
+
}];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [];
|
|
32
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-quality-spectral-ruleset",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0-beta.1",
|
|
4
4
|
"description": "Spectral ruleset by API Quality",
|
|
5
5
|
"main": "apq-spectral.yaml",
|
|
6
6
|
"files": [
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
"test": "jest --coverage --detectOpenHandles --no-cache --forceExit",
|
|
12
12
|
"make-badges": "coverage-badges"
|
|
13
13
|
},
|
|
14
|
-
"keywords": [
|
|
14
|
+
"keywords": [
|
|
15
|
+
"spectral",
|
|
16
|
+
"linter",
|
|
17
|
+
"openapi"
|
|
18
|
+
],
|
|
15
19
|
"author": {
|
|
16
20
|
"name": "APIQuality"
|
|
17
21
|
},
|