api-quality-spectral-ruleset 1.3.0 → 1.4.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
|
@@ -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."
|
|
@@ -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;
|