api-quality-spectral-ruleset 1.1.0 → 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,8 +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
16
+ - apq-check-examples-coverage
17
+ - apq-path-depth
15
18
  extends:
16
19
  - spectral:asyncapi
17
20
  - spectral:oas
@@ -109,19 +112,21 @@ rules:
109
112
  description: "Default response media type should be defined for responses."
110
113
  message: "OAR010: Default response media type is mandatory."
111
114
  severity: warn
112
- given: "$.paths[*][get,post,put,patch,delete].responses[*]"
115
+ given: "$.paths[*][get,post,put,patch,delete]"
113
116
  then:
114
- field: "content['application/json']"
115
- function: truthy
117
+ function: apq-response-media-type
116
118
  apiq:OAR011:
117
119
  description: "URLs should follow the specified naming convention: all literal path segments must be in kebab-case."
118
120
  message: "OAR011: The resource names with more than two words must be compliant with the standard naming convention (kebab-case)."
121
+ documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR011.md"
119
122
  severity: error
120
- given: "$.paths[*]~"
123
+ given:
124
+ - "$.paths[*]~"
125
+ - "$.servers[*].url"
121
126
  then:
122
127
  function: pattern
123
128
  functionOptions:
124
- match: "^(\/|[a-z0-9-]+|{[a-zA-Z0-9_|-]+})+$"
129
+ match: "^(https?:\\/\\/[^\\/]+)?(\\/(?:[a-z0-9]+(?:-[a-z0-9]+)*|\\{[^}]+\\}))*\\/?$"
125
130
  apiq:OAR012:
126
131
  description: "Path params, query params, object names and property names should follow the configured naming convention. You can configure snake_case (default), kebab-case, camelCase or UpperCamelCase"
127
132
  message: "OAR012: Path params, query params, object names and property names must follow the configured naming convention."
@@ -147,13 +152,16 @@ rules:
147
152
  function: truthy
148
153
  apiq:OAR014:
149
154
  description: "Resources depth level should be below the non-suggested range."
150
- message: "OAR014: The number of parts of the path must be less than 4."
151
- severity: warn
152
- 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.*~"
153
159
  then:
154
- function: pattern
160
+ function: apq-path-depth
155
161
  functionOptions:
156
- notMatch: "^(?:\\/[^\\/]+){4,}"
162
+ maxDepth: 3
163
+ ignoreSegments:
164
+ - me
157
165
  apiq:OAR015:
158
166
  description: "Resources depth level should be smaller than 5."
159
167
  message: "OAR015: Path exceeds the maximum allowed depth level of 5."
@@ -168,24 +176,29 @@ rules:
168
176
  message: "OAR016: Numeric types must use a valid format for their type."
169
177
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR016.md"
170
178
  severity: error
171
- given:
172
- - "$..[?(@.type == 'integer' && @.format)]"
173
- - "$..[?(@.type == 'number' && @.format)]"
179
+ given: "$..[?(@.type=='number' || @.type=='integer')]"
174
180
  then:
175
181
  function: schema
176
182
  functionOptions:
177
183
  schema:
178
- oneOf:
179
- - properties:
180
- type:
181
- const: integer
182
- format:
183
- enum: [int32, int64]
184
- - properties:
185
- type:
186
- const: number
187
- format:
188
- enum: [float, double]
184
+ required: [format]
185
+ allOf:
186
+ - if:
187
+ properties:
188
+ type:
189
+ const: integer
190
+ then:
191
+ properties:
192
+ format:
193
+ enum: [int32, int64]
194
+ - if:
195
+ properties:
196
+ type:
197
+ const: number
198
+ then:
199
+ properties:
200
+ format:
201
+ enum: [float, double]
189
202
  apiq:OAR017:
190
203
  description: "Resource path should alternate static and parametrized parts."
191
204
  message: "OAR017: Resource path should alternate static and parametrized parts."
@@ -261,11 +274,30 @@ rules:
261
274
  apiq:OAR022:
262
275
  description: "$orderby must be defined as a query parameter in all operations."
263
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"
264
278
  severity: warn
265
- given: "$.paths[*].get.parameters"
279
+ given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/))].get"
266
280
  then:
267
- field: "$[?(@.name == '$orderby' && @.in == 'query')]"
268
- 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]
269
301
  apiq:OAR023:
270
302
  description: "$total must be defined as a query parameter in all operations."
271
303
  message: "OAR023: $total must be defined as a query parameter in this operation."
@@ -285,11 +317,30 @@ rules:
285
317
  apiq:OAR025:
286
318
  description: "$limit must be defined as a query parameter in all operations."
287
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"
288
321
  severity: error
289
- given: "$.paths[*].get.parameters"
322
+ given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/))].get"
290
323
  then:
291
- field: "$[?(@.name == '$limit' && @.in == 'query')]"
292
- 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]
293
344
  apiq:OAR026:
294
345
  description: "The $total parameter default value should be false."
295
346
  message: "OAR026: The $total parameter default value should be false."
@@ -358,14 +409,14 @@ rules:
358
409
  function: truthy
359
410
  apiq:OAR031:
360
411
  description: The examples can help developers to understand the response data structure and representation."
361
- message: "OAR031: Examples - Parameters, Request Body and Responses must have an examples defined."
412
+ message: "OAR031: Examples - Parameters, Request Body and Responses must have an examples defined."
362
413
  severity: error
363
414
  given:
364
415
  - $.paths.*.*.parameters.*
365
- - $.paths.*.*[responses,requestBody]..content.*
416
+ - $.paths.*.*.requestBody
417
+ - $.paths.*.*.responses[?(@property !== "204")]
366
418
  then:
367
- field: examples
368
- function: truthy
419
+ function: apq-check-examples-coverage
369
420
  apiq:OAR032:
370
421
  description: "Ambiguous path parts not encouraged."
371
422
  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."
@@ -820,16 +871,32 @@ rules:
820
871
  - required: ["enum"]
821
872
  apiq:OAR076:
822
873
  description: "Schema should use well-defined type and format."
823
- message: "OAR076: Invalid combination: type '{{value.type}}' with format '{{value.format}}'"
824
- severity: "error"
825
- given: "$..[?(@.type=='number')]"
874
+ message: "OAR076: Schema should use well-defined type and format."
875
+ documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR076.md"
876
+ severity: error
877
+ given: "$..[?(@.type=='number' || @.type=='integer')]"
826
878
  then:
827
- field: format
828
- function: enumeration
879
+ function: schema
829
880
  functionOptions:
830
- values:
831
- - float
832
- - double
881
+ schema:
882
+ required: [format]
883
+ allOf:
884
+ - if:
885
+ properties:
886
+ type:
887
+ const: integer
888
+ then:
889
+ properties:
890
+ format:
891
+ enum: [int32, int64]
892
+ - if:
893
+ properties:
894
+ type:
895
+ const: number
896
+ then:
897
+ properties:
898
+ format:
899
+ enum: [float, double]
833
900
  apiq:OAR077:
834
901
  description: "All parameters in query must be snake_case."
835
902
  message: "OAR077: All parameters in query must be snake_case."
@@ -966,9 +1033,12 @@ rules:
966
1033
  apiq:OAR091:
967
1034
  description: "Parameters must contain only $ref references."
968
1035
  message: "OAR091: Parameters must contain only $ref references."
1036
+ documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR091.md"
969
1037
  severity: error
970
- given: "$.paths[*][get,post,put,patch,delete].parameters[*].$ref"
1038
+ resolved: false
1039
+ given: "$.paths[*][get,post,put,patch,delete].parameters[*]"
971
1040
  then:
1041
+ field: $ref
972
1042
  function: truthy
973
1043
  apiq:OAR092:
974
1044
  description: "RequestBody must contain a $ref."
@@ -1034,11 +1104,13 @@ rules:
1034
1104
  description: "Last path part must be the API version, indicated with the prefix 'v' and the version number as integer."
1035
1105
  message: "OAR100: Last path part must be the API version, indicated with the prefix 'v' and the version number as integer."
1036
1106
  severity: "error"
1037
- given: "$.servers[*].url"
1107
+ given:
1108
+ - "$.servers[*].url"
1109
+ - "$.basePath"
1038
1110
  then:
1039
1111
  function: pattern
1040
1112
  functionOptions:
1041
- match: "^https?://[^/]+/.*/v[0-9]+$"
1113
+ match: "^(https?://[^/]+)?(?:/[^/]+)*/v[0-9]+$"
1042
1114
  apiq:OAR101:
1043
1115
  description: "The first part of the path should be one of the allowed paths (e.g., '/hello')."
1044
1116
  message: "OAR101: The first part of the path should be one of the allowed paths."
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @param {object} given
3
+ * @param {object} options
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+ module.exports = (given, options, context) => {
7
+ if (!given || typeof given !== 'object') return [];
8
+
9
+ const hasExample = (node) => {
10
+ if (!node || typeof node !== 'object') return false;
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
+ }
17
+
18
+ if (node.schema) {
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
+ };
28
+
29
+ const checkSchema = (schema) => {
30
+ if (!schema || typeof schema !== 'object') return false;
31
+
32
+ if (schema.example !== undefined || schema.examples !== undefined) return true;
33
+
34
+ if (schema.properties && typeof schema.properties === 'object') {
35
+ return Object.values(schema.properties).some(prop => checkSchema(prop));
36
+ }
37
+
38
+ if (schema.items) {
39
+ return checkSchema(schema.items);
40
+ }
41
+
42
+ return false;
43
+ };
44
+
45
+ if (!hasExample(given)) {
46
+ return [
47
+ {
48
+ message: context.rule.message
49
+ },
50
+ ];
51
+ }
52
+
53
+ return [];
54
+ };
@@ -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.0",
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
- };