api-quality-spectral-ruleset 1.3.0 → 1.4.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
CHANGED
|
@@ -220,7 +220,7 @@ rules:
|
|
|
220
220
|
then:
|
|
221
221
|
function: apq-alternate-paths
|
|
222
222
|
functionOptions:
|
|
223
|
-
except: ["me", "get", "search"]
|
|
223
|
+
except: ["me", "get", "search", "delete"]
|
|
224
224
|
apiq:OAR018:
|
|
225
225
|
description: Operation not recommended for resource path depending on HTTP verb
|
|
226
226
|
message: "OAR018: Operation not recommended for resource path: {{path}} ({{verb}})"
|
|
@@ -475,6 +475,11 @@ rules:
|
|
|
475
475
|
- $.paths.*.*.responses[?(@property !== "204")]
|
|
476
476
|
then:
|
|
477
477
|
function: apq-check-examples-coverage
|
|
478
|
+
functionOptions:
|
|
479
|
+
validateResponse: true
|
|
480
|
+
validateRequestBody: true
|
|
481
|
+
validateParameter: true
|
|
482
|
+
validateProperty: true
|
|
478
483
|
apiq:OAR032:
|
|
479
484
|
description: "Ambiguous path parts not encouraged."
|
|
480
485
|
message: "OAR032: When reading a URL, it must be possible to interpret which resources it is referring to in a concrete way. Ambiguous names in resources must be avoided."
|
|
@@ -523,7 +528,7 @@ rules:
|
|
|
523
528
|
functionOptions:
|
|
524
529
|
notMatch: "^(Cookie|Set-Cookie)$"
|
|
525
530
|
apiq:OAR037:
|
|
526
|
-
description: "
|
|
531
|
+
description: "If a string schema declares a format, it must be a valid format value."
|
|
527
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)."
|
|
528
533
|
severity: error
|
|
529
534
|
given: "$..[?(@.type=='string')]"
|
|
@@ -615,11 +620,14 @@ rules:
|
|
|
615
620
|
description: "Declared media type should conform to RFC6838 and RFC7231."
|
|
616
621
|
message: "OAR044: Declared media type range should conform to RFC7231."
|
|
617
622
|
severity: "error"
|
|
618
|
-
given:
|
|
623
|
+
given:
|
|
624
|
+
- "$.paths.*.*.responses.*.content.*~"
|
|
625
|
+
- "$.paths.*.*.requestBody.content.*~"
|
|
626
|
+
- "$.components.requestBodies.*.content.*~"
|
|
619
627
|
then:
|
|
620
628
|
function: pattern
|
|
621
629
|
functionOptions:
|
|
622
|
-
match: "^
|
|
630
|
+
match: "^(\\*|[a-zA-Z0-9][a-zA-Z0-9!#$&\\-^_.+]*)/(\\*|[a-zA-Z0-9][a-zA-Z0-9!#$&\\-^_.+]*)(;\\s*[a-zA-Z0-9]+=[\\w\\-]+)*$"
|
|
623
631
|
apiq:OAR045:
|
|
624
632
|
description: "Response schema is required for responses with status codes 201 and others that return content."
|
|
625
633
|
message: "OAR045: Response schema is required for status code '{{property}}'."
|
|
@@ -1,47 +1,92 @@
|
|
|
1
1
|
const COMBINERS = ['allOf', 'oneOf', 'anyOf'];
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
// A level is validated unless explicitly turned off with `false`.
|
|
4
|
+
const isOn = (value) => value !== false;
|
|
5
|
+
|
|
6
|
+
// Whole-body (root) example declared directly on a schema node.
|
|
7
|
+
const schemaHasRootExample = (schema) =>
|
|
8
|
+
!!schema && typeof schema === 'object'
|
|
9
|
+
&& (schema.example !== undefined || schema.examples !== undefined);
|
|
10
|
+
|
|
11
|
+
// Parameter-level example: param.example/examples, the param schema's root example,
|
|
12
|
+
// or a media-type example under param.content (OAS3). OAS2 params use param.example.
|
|
13
|
+
const hasParameterExample = (param) => {
|
|
14
|
+
if (!param || typeof param !== 'object') return false;
|
|
15
|
+
if (param.example !== undefined || param.examples !== undefined) return true;
|
|
16
|
+
if (schemaHasRootExample(param.schema)) return true;
|
|
17
|
+
if (param.content && typeof param.content === 'object') {
|
|
18
|
+
return Object.values(param.content).some(
|
|
19
|
+
(mt) => mt && typeof mt === 'object'
|
|
20
|
+
&& (mt.example !== undefined || mt.examples !== undefined || schemaHasRootExample(mt.schema)),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
9
24
|
};
|
|
10
25
|
|
|
11
|
-
|
|
26
|
+
// Body-level (whole response / requestBody) example: a media-type example/examples or a
|
|
27
|
+
// root schema example. OAS3 -> content.<mt>.{example|examples|schema.example}.
|
|
28
|
+
// OAS2 -> node.examples map or node.schema root example.
|
|
29
|
+
const hasBodyLevelExample = (node) => {
|
|
12
30
|
if (!node || typeof node !== 'object') return false;
|
|
13
|
-
if (node.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
31
|
+
if (node.content && typeof node.content === 'object') {
|
|
32
|
+
return Object.values(node.content).some(
|
|
33
|
+
(mt) => mt && typeof mt === 'object'
|
|
34
|
+
&& (mt.example !== undefined || mt.examples !== undefined || schemaHasRootExample(mt.schema)),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (node.examples !== undefined) return true; // OAS2 response-level examples map
|
|
38
|
+
if (schemaHasRootExample(node.schema)) return true; // OAS2 root schema example
|
|
39
|
+
return false;
|
|
17
40
|
};
|
|
18
41
|
|
|
19
|
-
|
|
42
|
+
// Per-leaf-property example check (recurses objects, arrays and combiners).
|
|
43
|
+
const collectPropertyIssues = (schema, issues, basePath) => {
|
|
20
44
|
if (!schema || typeof schema !== 'object' || schema.$ref) return;
|
|
21
45
|
|
|
22
46
|
if (schema.type === 'array') {
|
|
23
|
-
schema.items
|
|
47
|
+
if (schema.items) collectPropertyIssues(schema.items, issues, [...basePath, 'items']);
|
|
24
48
|
return;
|
|
25
49
|
}
|
|
26
50
|
|
|
27
51
|
if (schema.properties) {
|
|
28
52
|
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
29
53
|
if (!propSchema || typeof propSchema !== 'object' || propSchema.$ref) continue;
|
|
54
|
+
const propPath = [...basePath, 'properties', propName];
|
|
30
55
|
const propType = propSchema.type;
|
|
31
56
|
if (propType === 'object' || propType === 'array' || (!propType && propSchema.properties)) {
|
|
32
|
-
collectPropertyIssues(propSchema, issues);
|
|
57
|
+
collectPropertyIssues(propSchema, issues, propPath);
|
|
33
58
|
} else if (propType !== undefined && propSchema.example === undefined && propSchema.examples === undefined) {
|
|
34
|
-
issues.push({
|
|
59
|
+
issues.push({
|
|
60
|
+
message: `OAR031: Property '${propName}' is missing an example.`,
|
|
61
|
+
path: propPath,
|
|
62
|
+
});
|
|
35
63
|
}
|
|
36
64
|
}
|
|
37
65
|
}
|
|
38
66
|
|
|
39
|
-
COMBINERS.forEach(c => {
|
|
40
|
-
if (Array.isArray(schema[c]))
|
|
67
|
+
COMBINERS.forEach((c) => {
|
|
68
|
+
if (Array.isArray(schema[c])) {
|
|
69
|
+
schema[c].forEach((sub, i) => collectPropertyIssues(sub, issues, [...basePath, c, i]));
|
|
70
|
+
}
|
|
41
71
|
});
|
|
42
72
|
};
|
|
43
73
|
|
|
74
|
+
// Property-level coverage for a response / requestBody node (OAS3 content.* or OAS2 schema).
|
|
75
|
+
const collectBodyProperties = (node, issues, basePath) => {
|
|
76
|
+
if (node.content && typeof node.content === 'object') {
|
|
77
|
+
Object.entries(node.content).forEach(([mediaType, mt]) => {
|
|
78
|
+
if (mt?.schema) collectPropertyIssues(mt.schema, issues, [...basePath, 'content', mediaType, 'schema']);
|
|
79
|
+
});
|
|
80
|
+
} else if (node.schema) {
|
|
81
|
+
collectPropertyIssues(node.schema, issues, [...basePath, 'schema']);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
44
85
|
/**
|
|
86
|
+
* OAR031 — examples coverage, validated independently per level. Each level can be
|
|
87
|
+
* switched off via functionOptions (all default on):
|
|
88
|
+
* validateResponse, validateRequestBody, validateParameter, validateProperty
|
|
89
|
+
*
|
|
45
90
|
* @param {object} given
|
|
46
91
|
* @param {object} options
|
|
47
92
|
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
@@ -49,23 +94,42 @@ const collectPropertyIssues = (schema, issues) => {
|
|
|
49
94
|
module.exports = function oar031ExamplesCoverage(given, options, context) {
|
|
50
95
|
if (!given || typeof given !== 'object') return [];
|
|
51
96
|
|
|
97
|
+
const opts = options || {};
|
|
98
|
+
const validateResponse = isOn(opts.validateResponse);
|
|
99
|
+
const validateRequestBody = isOn(opts.validateRequestBody);
|
|
100
|
+
const validateParameter = isOn(opts.validateParameter);
|
|
101
|
+
const validateProperty = isOn(opts.validateProperty);
|
|
102
|
+
|
|
52
103
|
const issues = [];
|
|
53
104
|
const nodePath = context.path || [];
|
|
54
105
|
const isParameter = nodePath.length >= 2 && nodePath.at(-2) === 'parameters';
|
|
55
|
-
const
|
|
106
|
+
const isRequestBody = nodePath.at(-1) === 'requestBody';
|
|
56
107
|
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (given.content) {
|
|
63
|
-
Object.values(given.content).forEach(mt => {
|
|
64
|
-
if (mt?.schema) collectPropertyIssues(mt.schema, issues);
|
|
108
|
+
if (isParameter) {
|
|
109
|
+
if (validateParameter && !hasParameterExample(given)) {
|
|
110
|
+
issues.push({
|
|
111
|
+
message: `OAR031: Parameter '${given.name || ''}' must have an example defined`,
|
|
112
|
+
path: nodePath,
|
|
65
113
|
});
|
|
66
|
-
} else if (given.schema) {
|
|
67
|
-
collectPropertyIssues(given.schema, issues);
|
|
68
114
|
}
|
|
115
|
+
// OAS2 body parameters carry a schema, so property-level coverage applies to them too.
|
|
116
|
+
if (given.in === 'body' && validateProperty && given.schema) {
|
|
117
|
+
collectPropertyIssues(given.schema, issues, [...nodePath, 'schema']);
|
|
118
|
+
}
|
|
119
|
+
return issues;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const levelOn = isRequestBody ? validateRequestBody : validateResponse;
|
|
123
|
+
const levelMessage = isRequestBody
|
|
124
|
+
? 'OAR031: Request body must have an example defined'
|
|
125
|
+
: 'OAR031: Response must have an example defined';
|
|
126
|
+
|
|
127
|
+
if (levelOn && !hasBodyLevelExample(given)) {
|
|
128
|
+
issues.push({ message: levelMessage, path: nodePath });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (validateProperty) {
|
|
132
|
+
collectBodyProperties(given, issues, nodePath);
|
|
69
133
|
}
|
|
70
134
|
|
|
71
135
|
return issues;
|
|
@@ -6,7 +6,7 @@ const VALID_FORMATS = new Set([
|
|
|
6
6
|
|
|
7
7
|
module.exports = (targetVal, _options, context) => {
|
|
8
8
|
const format = targetVal.format;
|
|
9
|
-
if (format
|
|
9
|
+
if (format !== undefined && format !== null && !VALID_FORMATS.has(String(format).toLowerCase())) {
|
|
10
10
|
return [{ message: context.rule.message }];
|
|
11
11
|
}
|
|
12
12
|
return [];
|