api-quality-spectral-ruleset 1.5.0 → 1.6.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 +62 -60
- package/functions/apq-check-examples-coverage.js +11 -18
- package/functions/apq-compare-insensitive.js +4 -3
- package/functions/apq-password-format.js +5 -2
- package/functions/apq-path-param-query-conflict.js +20 -3
- package/functions/apq-post-201-location-header.js +36 -0
- package/functions/apq-require-response-on-path-params.js +6 -4
- package/functions/apq-response-content-required.js +24 -0
- package/functions/apq-response-media-type.js +22 -19
- package/functions/apq-response-no-content.js +27 -0
- package/functions/apq-string-parameter-integrity.js +10 -0
- package/functions/apq-tags-consistency.js +71 -0
- package/functions/apq-total-param-default-value.js +24 -0
- package/functions/apq-undefined-response-media-type.js +46 -0
- package/functions/apq-valid-response-schema.js +71 -20
- package/functions/apq-validate-structure.js +34 -5
- package/functions/apq-wso2-auth-type-required.js +134 -0
- package/functions/apq-wso2-scope-defined.js +271 -0
- package/package.json +1 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
const MESSAGE = 'WSO2 scope definition does not exists';
|
|
2
|
+
|
|
3
|
+
const VERBS_V2 = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch'];
|
|
4
|
+
const VERBS_V3 = VERBS_V2.concat(['trace']);
|
|
5
|
+
const VERBS_V32 = VERBS_V3.concat(['query']);
|
|
6
|
+
|
|
7
|
+
const KIND_MAP = 2;
|
|
8
|
+
const KIND_SEQ = 3;
|
|
9
|
+
const KIND_ANCHOR_REF = 4;
|
|
10
|
+
|
|
11
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
+
|
|
13
|
+
function documentVersion(root) {
|
|
14
|
+
if (Object.prototype.hasOwnProperty.call(root, 'swagger')) return 'v2';
|
|
15
|
+
const openapi = root.openapi;
|
|
16
|
+
if (typeof openapi === 'string') {
|
|
17
|
+
if (openapi.indexOf('3.2') === 0) return 'v32';
|
|
18
|
+
if (openapi.indexOf('3.1') === 0) return 'v31';
|
|
19
|
+
}
|
|
20
|
+
return 'v3';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function surfaceFor(version) {
|
|
24
|
+
return {
|
|
25
|
+
verbs: version === 'v32' ? VERBS_V32 : (version === 'v2' ? VERBS_V2 : VERBS_V3),
|
|
26
|
+
components: version !== 'v2',
|
|
27
|
+
callbacks: version !== 'v2',
|
|
28
|
+
webhooks: version === 'v31' || version === 'v32',
|
|
29
|
+
pathItems: version === 'v31' || version === 'v32',
|
|
30
|
+
additionalOperations: version === 'v32',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decodePointerSegment(segment) {
|
|
35
|
+
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolvePointer(root, ref) {
|
|
39
|
+
if (typeof ref !== 'string' || ref.charAt(0) !== '#') return undefined;
|
|
40
|
+
const pointer = ref.slice(1);
|
|
41
|
+
if (pointer === '') return root;
|
|
42
|
+
if (pointer.charAt(0) !== '/') return undefined;
|
|
43
|
+
const segments = pointer.slice(1).split('/').map(decodePointerSegment);
|
|
44
|
+
let value = root;
|
|
45
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
46
|
+
const segment = segments[i];
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
const index = Number(segment);
|
|
49
|
+
if (!Number.isInteger(index) || index < 0 || index >= value.length) return undefined;
|
|
50
|
+
value = value[index];
|
|
51
|
+
} else if (isPlainObject(value) && Object.prototype.hasOwnProperty.call(value, segment)) {
|
|
52
|
+
value = value[segment];
|
|
53
|
+
} else {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveRef(root, value) {
|
|
61
|
+
let current = value;
|
|
62
|
+
const seenRefs = new Set();
|
|
63
|
+
while (isPlainObject(current) && typeof current.$ref === 'string') {
|
|
64
|
+
const ref = current.$ref;
|
|
65
|
+
if (seenRefs.has(ref)) return current;
|
|
66
|
+
seenRefs.add(ref);
|
|
67
|
+
const target = resolvePointer(root, ref);
|
|
68
|
+
if (target === undefined) return current;
|
|
69
|
+
current = target;
|
|
70
|
+
}
|
|
71
|
+
return current;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function collectScopeNames(root) {
|
|
75
|
+
const names = new Set();
|
|
76
|
+
let security = root['x-wso2-security'];
|
|
77
|
+
if (security !== undefined) security = resolveRef(root, security);
|
|
78
|
+
if (!isPlainObject(security)) return names;
|
|
79
|
+
const apim = security.apim;
|
|
80
|
+
if (!isPlainObject(apim)) return names;
|
|
81
|
+
const scopesContainer = apim['x-wso2-scopes'];
|
|
82
|
+
let entries = [];
|
|
83
|
+
if (isPlainObject(scopesContainer)) {
|
|
84
|
+
entries = Object.keys(scopesContainer).map((key) => scopesContainer[key]);
|
|
85
|
+
} else if (Array.isArray(scopesContainer)) {
|
|
86
|
+
entries = scopesContainer.slice();
|
|
87
|
+
}
|
|
88
|
+
entries.forEach((entry) => {
|
|
89
|
+
const resolved = resolveRef(root, entry);
|
|
90
|
+
if (!isPlainObject(resolved)) return;
|
|
91
|
+
const name = resolved.name;
|
|
92
|
+
if (name === null || name === undefined) return;
|
|
93
|
+
names.add(String(name));
|
|
94
|
+
});
|
|
95
|
+
return names;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function collectOperations(root, surface) {
|
|
99
|
+
const operations = [];
|
|
100
|
+
|
|
101
|
+
const visitOperation = (op, path) => {
|
|
102
|
+
if (isPlainObject(op)) operations.push({ op, path });
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const visitOperationWithCallbacks = (op, path) => {
|
|
106
|
+
visitOperation(op, path);
|
|
107
|
+
if (!surface.callbacks || !isPlainObject(op) || !isPlainObject(op.callbacks)) return;
|
|
108
|
+
Object.keys(op.callbacks).forEach((expressionKey) => {
|
|
109
|
+
if (expressionKey.charAt(0) === 'x') return;
|
|
110
|
+
const callback = op.callbacks[expressionKey];
|
|
111
|
+
if (!isPlainObject(callback)) return;
|
|
112
|
+
Object.keys(callback).forEach((pathKey) => {
|
|
113
|
+
if (pathKey.charAt(0) === 'x') return;
|
|
114
|
+
// eslint-disable-next-line no-use-before-define
|
|
115
|
+
visitPathItem(callback[pathKey], path.concat(['callbacks', expressionKey, pathKey]));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const visitPathItem = (pathItem, path) => {
|
|
121
|
+
if (!isPlainObject(pathItem)) return;
|
|
122
|
+
surface.verbs.forEach((verb) => {
|
|
123
|
+
if (Object.prototype.hasOwnProperty.call(pathItem, verb)) {
|
|
124
|
+
visitOperationWithCallbacks(pathItem[verb], path.concat([verb]));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
if (surface.additionalOperations && isPlainObject(pathItem.additionalOperations)) {
|
|
128
|
+
Object.keys(pathItem.additionalOperations).forEach((method) => {
|
|
129
|
+
visitOperationWithCallbacks(pathItem.additionalOperations[method], path.concat(['additionalOperations', method]));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (isPlainObject(root.paths)) {
|
|
135
|
+
Object.keys(root.paths).forEach((pathKey) => {
|
|
136
|
+
if (pathKey.charAt(0) !== '/') return;
|
|
137
|
+
visitPathItem(root.paths[pathKey], ['paths', pathKey]);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (surface.webhooks && isPlainObject(root.webhooks)) {
|
|
142
|
+
Object.keys(root.webhooks).forEach((name) => {
|
|
143
|
+
visitPathItem(root.webhooks[name], ['webhooks', name]);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (surface.components && isPlainObject(root.components)) {
|
|
148
|
+
const components = root.components;
|
|
149
|
+
if (surface.callbacks && isPlainObject(components.callbacks)) {
|
|
150
|
+
Object.keys(components.callbacks).forEach((name) => {
|
|
151
|
+
const callback = components.callbacks[name];
|
|
152
|
+
if (!isPlainObject(callback)) return;
|
|
153
|
+
Object.keys(callback).forEach((pathKey) => {
|
|
154
|
+
if (pathKey.charAt(0) === 'x') return;
|
|
155
|
+
visitPathItem(callback[pathKey], ['components', 'callbacks', name, pathKey]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (surface.pathItems && isPlainObject(components.pathItems)) {
|
|
160
|
+
Object.keys(components.pathItems).forEach((name) => {
|
|
161
|
+
visitPathItem(components.pathItems[name], ['components', 'pathItems', name]);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (surface.webhooks && isPlainObject(components.webhooks)) {
|
|
165
|
+
Object.keys(components.webhooks).forEach((name) => {
|
|
166
|
+
visitPathItem(components.webhooks[name], ['components', 'webhooks', name]);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return operations;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function unwrapAnchor(node) {
|
|
175
|
+
let current = node;
|
|
176
|
+
while (current && current.kind === KIND_ANCHOR_REF) {
|
|
177
|
+
current = current.value;
|
|
178
|
+
}
|
|
179
|
+
return current;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function astNodeAt(root, path) {
|
|
183
|
+
let node = unwrapAnchor(root);
|
|
184
|
+
for (let i = 0; i < path.length; i += 1) {
|
|
185
|
+
if (!node) return undefined;
|
|
186
|
+
const segment = path[i];
|
|
187
|
+
if (node.kind === KIND_MAP) {
|
|
188
|
+
const mappings = node.mappings || [];
|
|
189
|
+
let match = null;
|
|
190
|
+
for (let j = mappings.length - 1; j >= 0; j -= 1) {
|
|
191
|
+
const mapping = mappings[j];
|
|
192
|
+
if (mapping.key && mapping.key.value === String(segment)) {
|
|
193
|
+
match = mapping;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!match) return undefined;
|
|
198
|
+
node = unwrapAnchor(match.value);
|
|
199
|
+
} else if (node.kind === KIND_SEQ) {
|
|
200
|
+
const index = Number(segment);
|
|
201
|
+
const items = node.items || [];
|
|
202
|
+
if (!Number.isInteger(index) || index < 0 || index >= items.length) return undefined;
|
|
203
|
+
node = unwrapAnchor(items[index]);
|
|
204
|
+
} else {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return node;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function wasWrittenWithNoValue(context, path) {
|
|
212
|
+
const parserResult = context && context.document && context.document.parserResult;
|
|
213
|
+
const ast = parserResult && parserResult.ast;
|
|
214
|
+
if (!ast || typeof ast.kind !== 'number') return false;
|
|
215
|
+
return astNodeAt(ast, path) === null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function issuePrefix(context) {
|
|
219
|
+
const name = (context && context.rule && context.rule.name) || '';
|
|
220
|
+
const code = name.indexOf(':') === -1 ? name : name.split(':').pop();
|
|
221
|
+
return code ? `${code}: ` : '';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function tokenText(value) {
|
|
225
|
+
if (Array.isArray(value)) return '[';
|
|
226
|
+
if (isPlainObject(value)) return '{';
|
|
227
|
+
return String(value);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function firstChildPath(scopePath, value) {
|
|
231
|
+
if (Array.isArray(value) && value.length > 0) return scopePath.concat([0]);
|
|
232
|
+
if (isPlainObject(value)) {
|
|
233
|
+
const firstKey = Object.keys(value)[0];
|
|
234
|
+
if (firstKey !== undefined) return scopePath.concat([firstKey]);
|
|
235
|
+
}
|
|
236
|
+
return scopePath;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {object} given the whole raw document, since the rule is anchored with `given: "$"`
|
|
241
|
+
* @param {null} _options OAR005 has no rule properties on either engine
|
|
242
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
243
|
+
* @returns {Array<{message: string, path: Array<string|number>}>}
|
|
244
|
+
*/
|
|
245
|
+
module.exports = (given, _options, context) => {
|
|
246
|
+
if (!isPlainObject(given)) return [];
|
|
247
|
+
|
|
248
|
+
const version = documentVersion(given);
|
|
249
|
+
const surface = surfaceFor(version);
|
|
250
|
+
const definedScopes = collectScopeNames(given);
|
|
251
|
+
const prefix = issuePrefix(context);
|
|
252
|
+
const results = [];
|
|
253
|
+
|
|
254
|
+
collectOperations(given, surface).forEach(({ op, path }) => {
|
|
255
|
+
if (!Object.prototype.hasOwnProperty.call(op, 'x-scope')) return;
|
|
256
|
+
const value = op['x-scope'];
|
|
257
|
+
const scopePath = path.concat(['x-scope']);
|
|
258
|
+
|
|
259
|
+
if (value === null) {
|
|
260
|
+
if (wasWrittenWithNoValue(context, scopePath)) return;
|
|
261
|
+
results.push({ message: `${prefix}${MESSAGE}`, path: scopePath });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!definedScopes.has(tokenText(value))) {
|
|
266
|
+
results.push({ message: `${prefix}${MESSAGE}`, path: firstChildPath(scopePath, value) });
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return results;
|
|
271
|
+
};
|