api-quality-spectral-ruleset 1.3.0-beta.4 → 1.3.0-beta.5

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
@@ -17,7 +17,9 @@ functions:
17
17
  - apq-check-examples-coverage
18
18
  - apq-path-depth
19
19
  - apq-forbidden-characters
20
+ - apq-valid-response-schema
20
21
  - apq-path-param-query-conflict
22
+ - apq-schema-format
21
23
  - apq-security-required-response
22
24
  - apq-check-ambiguous-path
23
25
  - apq-password-format
@@ -218,7 +220,7 @@ rules:
218
220
  then:
219
221
  function: apq-alternate-paths
220
222
  functionOptions:
221
- except: ["me"]
223
+ except: ["me", "get", "search"]
222
224
  apiq:OAR018:
223
225
  description: Operation not recommended for resource path depending on HTTP verb
224
226
  message: "OAR018: Operation not recommended for resource path: {{path}} ({{verb}})"
@@ -282,18 +284,44 @@ rules:
282
284
  description: "$expand must be defined as a query parameter in collection operations (excluding detail endpoints and health checks)."
283
285
  message: "OAR020: $expand must be defined as a query parameter in this operation."
284
286
  severity: warn
285
- given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/) && !@property.match(/status|health|ping/))].get.parameters"
287
+ given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/) && !@property.match(/status|health|ping/))].get"
286
288
  then:
287
- field: "$[?(@.name == '$expand' && @.in == 'query')]"
288
- function: truthy
289
+ function: schema
290
+ functionOptions:
291
+ schema:
292
+ required: ["parameters"]
293
+ properties:
294
+ parameters:
295
+ type: array
296
+ contains:
297
+ type: object
298
+ properties:
299
+ name:
300
+ const: "$expand"
301
+ in:
302
+ const: query
303
+ required: [name, in]
289
304
  apiq:OAR021:
290
305
  description: "$exclude must be defined as a query parameter in collection operations (excluding detail endpoints and health checks)."
291
306
  message: "OAR021: $exclude must be defined as a query parameter in this operation."
292
307
  severity: warn
293
- given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/) && !@property.match(/status|health|ping/))].get.parameters"
308
+ given: "$.paths[?(!@property.match(/\\/me(\\/|$)/) && !@property.match(/\\/\\{[^}]+\\}$/) && !@property.match(/status|health|ping/))].get"
294
309
  then:
295
- field: "$[?(@.name == '$exclude' && @.in == 'query')]"
296
- function: truthy
310
+ function: schema
311
+ functionOptions:
312
+ schema:
313
+ required: ["parameters"]
314
+ properties:
315
+ parameters:
316
+ type: array
317
+ contains:
318
+ type: object
319
+ properties:
320
+ name:
321
+ const: "$exclude"
322
+ in:
323
+ const: query
324
+ required: [name, in]
297
325
  apiq:OAR022:
298
326
  description: "$orderby must be defined as a query parameter in all operations."
299
327
  message: "OAR022: $orderby must be defined as a query parameter in this operation."
@@ -496,24 +524,18 @@ rules:
496
524
  notMatch: "^(Cookie|Set-Cookie)$"
497
525
  apiq:OAR037:
498
526
  description: "Schema should use well-defined type and format."
499
- message: "OAR037: String schemas must specify a valid format (date, date-time, password, byte, binary, email, uuid, uri, hostname, ipv4, ipv6, HEX, json, xml, or base64)."
527
+ 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)."
500
528
  severity: error
501
- given: "$..schema..[?(@.type=='string' && !@.enum)]"
529
+ given: "$..[?(@.type=='string')]"
502
530
  then:
503
- field: format
504
- function: pattern
505
- functionOptions:
506
- match: "^(date|date-time|password|byte|binary|email|uuid|uri|hostname|ipv4|ipv6|HEX|json|xml|base64)$"
531
+ function: apq-schema-format
507
532
  apiq:OAR038:
508
- description: "Response schema must be compliant with the standard."
509
- message: "OAR038: Response property is required for status code '{{property}}'."
533
+ description: "The 201 response schema of a POST operation must have properties named 'data' or 'error' with at least one sub-property."
534
+ message: "OAR038: Response property must be named 'data' or 'error'. Got '{{property}}'."
510
535
  severity: error
511
- given: "$.paths.*.*.responses..content..schema.properties"
536
+ given: "$.paths.*.post.responses.*.content..schema.properties"
512
537
  then:
513
- field: "@key"
514
- function: pattern
515
- functionOptions:
516
- match: "^data|error$"
538
+ function: apq-valid-response-schema
517
539
  apiq:OAR039:
518
540
  description: "Response codes must be defined according to the standard depending on the HTTP verb and resource path."
519
541
  message: "{{error}}"
@@ -810,9 +832,9 @@ rules:
810
832
  - "$.paths.*.*.parameters[*].schema..properties.*~"
811
833
  - "$.paths.*.*.responses[*].schema..properties.*~"
812
834
  then:
813
- function: casing
835
+ function: pattern
814
836
  functionOptions:
815
- type: snake
837
+ match: "^([a-z$][a-z0-9_$]*|_[a-z0-9_]+|@[a-zA-Z][a-zA-Z0-9_]*|x-[a-zA-Z][a-zA-Z0-9_-]*)$"
816
838
  apiq:OAR067:
817
839
  description: "RequestBody and Responses schema property names must be compliant with the camelCase naming convention."
818
840
  message: "OAR067: RequestBody and Responses schema property names must be compliant with the camelCase naming convention."
@@ -871,10 +893,10 @@ rules:
871
893
  functionOptions:
872
894
  notMatch: "stacktrace"
873
895
  apiq:OAR073:
874
- description: "API should include a 429 response to indicate rate limiting, except for health check paths like /status, /health, /ping."
896
+ description: "API should include a 429 response to indicate rate limiting, except for health check paths like /status, /health, /health-check, /ping, /liveness, /readiness."
875
897
  message: "OAR073: API should include a 429 response to indicate rate limiting."
876
898
  severity: error
877
- given: "$.paths[?(!(@property.match(/\\/status|health|ping|liveness|readiness/)))][*].responses"
899
+ given: "$.paths[?(@property !== '/status' && @property !== '/health' && @property !== '/health-check' && @property !== '/ping' && @property !== '/liveness' && @property !== '/readiness')][*].responses"
878
900
  then:
879
901
  field: "429"
880
902
  function: truthy
@@ -10,11 +10,14 @@ module.exports = (given, { except }, context) => {
10
10
 
11
11
  const parts = paths.substr(1).split('/').filter(p => p.length > 0);
12
12
  if (parts.length === 0) return result;
13
- let previousIsVar = isVariable(parts.shift());
14
- if (previousIsVar) {
15
- return [{
16
- message: context.rule.message,
17
- }];
13
+ const firstPart = parts.shift();
14
+ let previousIsVar;
15
+ if (except && except.includes(firstPart)) {
16
+ previousIsVar = true;
17
+ } else if (isVariable(firstPart)) {
18
+ return [{ message: context.rule.message }];
19
+ } else {
20
+ previousIsVar = false;
18
21
  }
19
22
 
20
23
  for (const part of parts) {
@@ -6,18 +6,25 @@
6
6
  module.exports = (targetVal, _options, context) => {
7
7
  const pattern = /^[a-zA-Z0-9_\-., ]+$/;
8
8
  const errors = [];
9
+ const basePath = context.path || [];
9
10
 
10
11
  if (typeof targetVal === 'string') {
11
12
  if (!pattern.test(targetVal)) {
12
- errors.push({ message: context.rule.message });
13
+ errors.push({
14
+ message: context.rule.message,
15
+ path: basePath
16
+ });
13
17
  }
14
18
  } else if (Array.isArray(targetVal)) {
15
19
  targetVal.forEach((role, index) => {
16
20
  if (typeof role === 'string' && !pattern.test(role)) {
17
- errors.push({ message: context.rule.message, path: [index] });
21
+ errors.push({
22
+ message: context.rule.message,
23
+ path: [...basePath, index]
24
+ });
18
25
  }
19
26
  });
20
27
  }
21
28
 
22
29
  return errors;
23
- };
30
+ };
@@ -1,98 +1,13 @@
1
- // Check that format is valid for a schema type.
2
-
3
- /**
4
- * @param {object} given
5
- * @param {object} options
6
- * @param {object} options.formats
7
- * @param {Array<string>} options.formats.string
8
- * @param {Array<string>} options.formats.number
9
- * @param {Array<string>} options.formats.integer
10
- * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
11
- */
12
- module.exports = function checkTypeAndFormat(given, options, context) {
13
- if (given === null || typeof given !== "object") {
14
- return [];
15
- }
16
-
17
- const errors = [];
18
- const path = context.path || [];
19
-
20
- if (given.type === "string" && options.formats.string) {
21
- if (given.format) {
22
- if (!options.formats.string.includes(given.format)) {
23
- errors.push({
24
- message: `Schema with type string has unrecognized format: ${given.format}`,
25
- path: [...path, "format"]
26
- });
27
- }
28
- }
29
- } else if (given.type === "integer" && options.formats.integer) {
30
- if (given.format) {
31
- if (!options.formats.integer.includes(given.format)) {
32
- errors.push({
33
- message: `Schema with type integer has unrecognized format: ${given.format}`,
34
- path: [...path, "format"]
35
- });
36
- }
37
- } else {
38
- errors.push({
39
- message: "Schema with type integer should specify format",
40
- path
41
- });
42
- }
43
- } else if (given.type === "number" && options.formats.number) {
44
- if (given.format) {
45
- if (!options.formats.number.includes(given.format)) {
46
- errors.push({
47
- message: `Schema with type number has unrecognized format: ${given.format}`,
48
- path: [...path, "format"]
49
- });
50
- }
51
- } else {
52
- errors.push({
53
- message: "Schema with type number should specify format",
54
- path
55
- });
56
- }
57
- } else if (given.type === "boolean") {
58
- if (given.format) {
59
- errors.push({
60
- message: "Schema with type boolean should not specify format",
61
- path: [...path, "format"]
62
- });
63
- }
64
- } else if (given.properties && typeof given.properties === "object") {
65
- // eslint-disable-next-line no-restricted-syntax
66
- for (const [key, value] of Object.entries(given.properties)) {
67
- errors.push(...checkTypeAndFormat(value, options, {
68
- path: [...path, "properties", key],
69
- document: context.document,
70
- documentInventory: context.documentInventory,
71
- rule: context.rule
72
- }));
73
- }
74
- }
75
-
76
- if (given.type === "array") {
77
- errors.push(...checkTypeAndFormat(given.items, options, {
78
- path: [...path, "items"],
79
- document: context.document,
80
- documentInventory: context.documentInventory,
81
- rule: context.rule
82
- }));
83
- }
84
-
85
- if (given.allOf && Array.isArray(given.allOf)) {
86
- // eslint-disable-next-line no-restricted-syntax
87
- for (const [index, value] of given.allOf.entries()) {
88
- errors.push(...checkTypeAndFormat(value, options, {
89
- path: [...path, "allOf", index],
90
- document: context.document,
91
- documentInventory: context.documentInventory,
92
- rule: context.rule
93
- }));
94
- }
95
- }
96
-
97
- return errors;
1
+ const VALID_FORMATS = new Set([
2
+ 'date', 'date-time', 'password', 'byte', 'binary', 'email',
3
+ 'uuid', 'uri', 'hostname', 'ipv4', 'ipv6', 'hex', 'hex(16)',
4
+ 'json', 'xml', 'base64'
5
+ ]);
6
+
7
+ module.exports = (targetVal, _options, context) => {
8
+ const format = targetVal.format;
9
+ if (format === undefined || format === null || !VALID_FORMATS.has(String(format).toLowerCase())) {
10
+ return [{ message: context.rule.message }];
11
+ }
12
+ return [];
98
13
  };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @param {object} targetVal
3
+ * @param {object} _options
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+ module.exports = (targetVal, _options, context) => {
7
+ const errors = [];
8
+ const VALID_NAMES = ['data', 'error'];
9
+
10
+ if (!targetVal || typeof targetVal !== 'object') {
11
+ return errors;
12
+ }
13
+
14
+ const path = context.path || [];
15
+ const responsesIdx = path.lastIndexOf('responses');
16
+ if (responsesIdx >= 0 && path[responsesIdx + 1] !== '201') {
17
+ return errors;
18
+ }
19
+
20
+ for (const [propName, propValue] of Object.entries(targetVal)) {
21
+ if (!VALID_NAMES.includes(propName)) {
22
+ errors.push({
23
+ message: context.rule.message,
24
+ path: [propName]
25
+ });
26
+ } else {
27
+ const subProps = propValue && propValue.properties;
28
+ const hasSubProps = subProps && Object.keys(subProps).length > 0;
29
+ if (!hasSubProps) {
30
+ errors.push({
31
+ message: `OAR038: Property '${propName}' must have at least one sub-property.`,
32
+ path: [propName]
33
+ });
34
+ }
35
+ }
36
+ }
37
+
38
+ return errors;
39
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-quality-spectral-ruleset",
3
- "version": "1.3.0-beta.4",
3
+ "version": "1.3.0-beta.5",
4
4
  "description": "Spectral ruleset by API Quality",
5
5
  "main": "apq-spectral.yaml",
6
6
  "files": [