api-quality-spectral-ruleset 1.1.1 → 1.1.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
@@ -10,9 +10,11 @@ functions:
10
10
  - apq-response-headers
11
11
  - apq-require-response-on-path-params
12
12
  - apq-required-fields-exist
13
+ - apq-response-media-type
13
14
  - apq-security-check
14
15
  - apq-custom-field
15
16
  - apq-check-examples-coverage
17
+ - apq-path-depth
16
18
  extends:
17
19
  - spectral:asyncapi
18
20
  - spectral:oas
@@ -110,10 +112,9 @@ rules:
110
112
  description: "Default response media type should be defined for responses."
111
113
  message: "OAR010: Default response media type is mandatory."
112
114
  severity: warn
113
- given: "$.paths[*][get,post,put,patch,delete].responses[*]"
115
+ given: "$.paths[*][get,post,put,patch,delete]"
114
116
  then:
115
- field: "content['application/json']"
116
- function: truthy
117
+ function: apq-response-media-type
117
118
  apiq:OAR011:
118
119
  description: "URLs should follow the specified naming convention: all literal path segments must be in kebab-case."
119
120
  message: "OAR011: The resource names with more than two words must be compliant with the standard naming convention (kebab-case)."
@@ -151,13 +152,16 @@ rules:
151
152
  function: truthy
152
153
  apiq:OAR014:
153
154
  description: "Resources depth level should be below the non-suggested range."
154
- message: "OAR014: The number of parts of the path must be less than 4."
155
- severity: warn
156
- given: "$.paths.*~"
155
+ message: "OAR014: The number of parts of the path must be less than 4."
156
+ documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR014.md"
157
+ severity: warn
158
+ given: "$.paths.*~"
157
159
  then:
158
- function: pattern
160
+ function: apq-path-depth
159
161
  functionOptions:
160
- notMatch: "^(?:\\/[^\\/]+){4,}"
162
+ maxDepth: 3
163
+ ignoreSegments:
164
+ - me
161
165
  apiq:OAR015:
162
166
  description: "Resources depth level should be smaller than 5."
163
167
  message: "OAR015: Path exceeds the maximum allowed depth level of 5."
@@ -270,11 +274,30 @@ rules:
270
274
  apiq:OAR022:
271
275
  description: "$orderby must be defined as a query parameter in all operations."
272
276
  message: "OAR022: $orderby must be defined as a query parameter in this operation."
277
+ documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR022.md"
273
278
  severity: warn
274
- given: "$.paths[*].get.parameters"
279
+ given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/))].get"
275
280
  then:
276
- field: "$[?(@.name == '$orderby' && @.in == 'query')]"
277
- function: truthy
281
+ function: schema
282
+ functionOptions:
283
+ schema:
284
+ if:
285
+ properties:
286
+ responses:
287
+ required: ["206"]
288
+ then:
289
+ required: ["parameters"]
290
+ properties:
291
+ parameters:
292
+ type: array
293
+ contains:
294
+ type: object
295
+ properties:
296
+ name:
297
+ const: "$orderby"
298
+ in:
299
+ const: query
300
+ required: [name, in]
278
301
  apiq:OAR023:
279
302
  description: "$total must be defined as a query parameter in all operations."
280
303
  message: "OAR023: $total must be defined as a query parameter in this operation."
@@ -294,11 +317,30 @@ rules:
294
317
  apiq:OAR025:
295
318
  description: "$limit must be defined as a query parameter in all operations."
296
319
  message: "OAR025: $limit must be defined as a query parameter in this operation."
320
+ documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR025.md"
297
321
  severity: error
298
- given: "$.paths[*].get.parameters"
322
+ given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/))].get"
299
323
  then:
300
- field: "$[?(@.name == '$limit' && @.in == 'query')]"
301
- function: truthy
324
+ function: schema
325
+ functionOptions:
326
+ schema:
327
+ if:
328
+ properties:
329
+ responses:
330
+ required: ["206"]
331
+ then:
332
+ required: ["parameters"]
333
+ properties:
334
+ parameters:
335
+ type: array
336
+ contains:
337
+ type: object
338
+ properties:
339
+ name:
340
+ const: "$limit"
341
+ in:
342
+ const: query
343
+ required: [name, in]
302
344
  apiq:OAR026:
303
345
  description: "The $total parameter default value should be false."
304
346
  message: "OAR026: The $total parameter default value should be false."
@@ -1062,11 +1104,13 @@ rules:
1062
1104
  description: "Last path part must be the API version, indicated with the prefix 'v' and the version number as integer."
1063
1105
  message: "OAR100: Last path part must be the API version, indicated with the prefix 'v' and the version number as integer."
1064
1106
  severity: "error"
1065
- given: "$.servers[*].url"
1107
+ given:
1108
+ - "$.servers[*].url"
1109
+ - "$.basePath"
1066
1110
  then:
1067
1111
  function: pattern
1068
1112
  functionOptions:
1069
- match: "^https?://[^/]+/.*/v[0-9]+$"
1113
+ match: "^(https?://[^/]+)?(?:/[^/]+)*/v[0-9]+$"
1070
1114
  apiq:OAR101:
1071
1115
  description: "The first part of the path should be one of the allowed paths (e.g., '/hello')."
1072
1116
  message: "OAR101: The first part of the path should be one of the allowed paths."
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @param {string} targetVal
3
+ * @param {object} options
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+ module.exports = (targetVal, options, context) => {
7
+ const maxDepth = options.maxDepth ?? 3
8
+ const ignore = options.ignoreSegments ?? []
9
+
10
+ const segments = targetVal
11
+ .split('/')
12
+ .filter(Boolean)
13
+ .filter(segment => !ignore.includes(segment))
14
+
15
+ if (segments.length > maxDepth) {
16
+ return [
17
+ { message: context.rule.message }
18
+ ]
19
+ }
20
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @param {object} given
3
+ * @param {object} options
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+
7
+ const NO_BODY_CODES = new Set(['204']);
8
+
9
+ module.exports = (given, options, context) => {
10
+ const errors = [];
11
+ if (!given) return errors;
12
+
13
+ const root = context.document.parserResult.data;
14
+
15
+ if (root.swagger) {
16
+ const produces = given.produces ?? root.produces;
17
+
18
+ if (!produces || !Array.isArray(produces) || !produces.includes('application/json')) {
19
+ errors.push({
20
+ message: context.rule.message,
21
+ path: [...context.path]
22
+ });
23
+ }
24
+
25
+ return errors;
26
+ }
27
+
28
+ const responses = given.responses;
29
+ if (!responses) return errors;
30
+
31
+ for (const [statusCode, response] of Object.entries(responses)) {
32
+ if (NO_BODY_CODES.has(statusCode)) continue;
33
+
34
+ if (!response?.content?.['application/json']) {
35
+ errors.push({
36
+ message: context.rule.message,
37
+ path: [...context.path, 'responses', statusCode]
38
+ });
39
+ }
40
+ }
41
+
42
+ return errors;
43
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-quality-spectral-ruleset",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Spectral ruleset by API Quality",
5
5
  "main": "apq-spectral.yaml",
6
6
  "files": [
@@ -1,23 +0,0 @@
1
- /**
2
- * @param {object} given
3
- * @param {object} options
4
- * @param {string} options.type
5
- * @param {string} options.match
6
- * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
7
- */
8
- module.exports = (given, options, context) => {
9
- const errors = [];
10
- if (!given) return errors;
11
-
12
- if (!given.type || given.type.toString() !== options.type.toString()) {
13
- errors.push({
14
- message: context.rule.message,
15
- });
16
- } else if (!given.default || given.default.toString() !== options.match.toString()) {
17
- errors.push({
18
- message: context.rule.message,
19
- });
20
- }
21
-
22
- return errors;
23
- }
@@ -1,21 +0,0 @@
1
- const { pattern } = require("@stoplight/spectral-functions");
2
-
3
- const patterns = {
4
- kebabCase: /^(\/|[a-z0-9-.]+|{[a-zA-Z0-9_]+})+$/,
5
- camelCase: /^[a-z]+([A-Z][a-z0-9]+)*$/,
6
- snakeCase: /^[a-z]+(_[a-z0-9]+)*$/,
7
- pascalCase: /^[A-Z]+([A-Z][a-z0-9]+)*$/
8
- };
9
-
10
- /**
11
- * @param {object} given
12
- * @param {object} options
13
- * @param {string} options.pattern
14
- * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
15
- */
16
- module.exports = (given, options, context) => {
17
- const errors = [];
18
- if (!given) return errors;
19
-
20
- return pattern(given, { match: patterns[options.pattern] }, context);
21
- }
@@ -1,29 +0,0 @@
1
- // Check that format is valid for a schema type.
2
-
3
- /**
4
- * @param {object} given
5
- * @param {object} options
6
- * @param {Array<string>} options.formats
7
- * @param {Array<string>} options.properties
8
- * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
9
- */
10
- module.exports = function checkTypeAndFormat(given, options, context) {
11
- if (given === null || typeof given !== "object") {
12
- return [];
13
- }
14
-
15
- const errors = [];
16
- const path = context.path || [];
17
- const name = path.slice(-1)[0].toString();
18
-
19
- if (options.properties.includes(name)) {
20
- if (!given.format || !options.formats.includes(given.format)) {
21
- errors.push({
22
- message: context.rule.message,
23
- path: [...path, name]
24
- });
25
- }
26
- }
27
-
28
- return errors;
29
- };