api-quality-spectral-ruleset 1.4.0 → 1.4.1-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 CHANGED
@@ -29,6 +29,7 @@ functions:
29
29
  - apq-paged-response-check
30
30
  - apq-status-endpoint-check
31
31
  - apq-validate-structure
32
+ - apq-numeric-parameter-integrity
32
33
  extends:
33
34
  - spectral:asyncapi
34
35
  - spectral:oas
@@ -65,7 +66,7 @@ rules:
65
66
  message: "Scope must define a 'description' attribute."
66
67
  apiq:OAR004:
67
68
  description: "A role with forbidden characters may cause problems in some applications."
68
- message: "OAR004: Role should not contain forbidden characters (e.g. special characters like €, *, etc.)."
69
+ message: "{{error}}"
69
70
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR004.md"
70
71
  severity: "error"
71
72
  given: "$.x-wso2-security.apim.x-wso2-scopes[*].roles"
@@ -166,7 +167,7 @@ rules:
166
167
  function: truthy
167
168
  apiq:OAR014:
168
169
  description: "Resources depth level should be below the non-suggested range."
169
- message: "OAR014: The number of parts of the path must be less than 4."
170
+ message: "{{error}}"
170
171
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR014.md"
171
172
  severity: warn
172
173
  given: "$.paths.*~"
@@ -179,13 +180,13 @@ rules:
179
180
  - me
180
181
  apiq:OAR015:
181
182
  description: "Resources depth level should be smaller than 5."
182
- message: "OAR015: Path exceeds the maximum allowed depth level of 5."
183
+ message: "{{error}}"
183
184
  severity: error
184
185
  given: "$.paths.*~"
185
186
  then:
186
187
  function: apq-path-depth
187
188
  functionOptions:
188
- maxDepth: 5
189
+ max-level-allowed: 5
189
190
  ignoreSegments:
190
191
  - me
191
192
  apiq:OAR016:
@@ -193,7 +194,7 @@ rules:
193
194
  message: "OAR016: Numeric types must use a valid format for their type."
194
195
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR016.md"
195
196
  severity: error
196
- given: "$..[?(@.type=='number' || @.type=='integer')]"
197
+ given: "$..[?(@ && (@.type=='number' || @.type=='integer'))]"
197
198
  then:
198
199
  function: schema
199
200
  functionOptions:
@@ -492,11 +493,13 @@ rules:
492
493
  paging-schema: '{"type":"object","properties":{"numPages":{"type":"integer"},"total":{"type":"integer"},"start":{"type":"integer"},"limit":{"type":"integer"},"links":{"type":"object","properties":{"next":{"type":"object","properties":{"href":{"type":"string"}}},"previous":{"type":"object","properties":{"href":{"type":"string"}}},"last":{"type":"object","properties":{"href":{"type":"string"}}},"self":{"type":"object","properties":{"href":{"type":"string"}}},"first":{"type":"object","properties":{"href":{"type":"string"}}}},"required":["self","previous","next"]}},"required":["start","limit","links"],"pagingPropertyName":"paging"}'
493
494
  apiq:OAR035:
494
495
  description: "Response code 401 must be defined for operations with security schemes defined."
495
- message: "OAR035: Response code 401 must be defined for operations with security schemes defined."
496
+ message: "{{error}}"
496
497
  severity: "error"
497
498
  given: "$.paths[*][*]"
498
499
  then:
499
500
  function: apq-security-required-response
501
+ functionOptions:
502
+ expected-codes: "401"
500
503
  apiq:OAR036:
501
504
  description: "Cookie use is forbidden as a session mechanism."
502
505
  message: "OAR036: Cookie use is forbidden as a session mechanism."
@@ -514,7 +517,7 @@ rules:
514
517
  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), or a valid pattern when no format is defined."
515
518
  severity: error
516
519
  resolved: false
517
- given: "$..[?(@.type=='string')]"
520
+ given: "$..[?(@ && @.type=='string')]"
518
521
  then:
519
522
  function: apq-schema-format
520
523
  apiq:OAR038:
@@ -566,13 +569,13 @@ rules:
566
569
  ;patch:^/[^/{}]+/(\{[^/{}]+\}|\bme\b)/[^/{}]+/(\{[^/{}]+\}|\bme\b)/[^/{}]+/(\{[^/{}]+\}|\bme\b)$:200,404,400,415,500,503
567
570
  apiq:OAR040:
568
571
  description: "A scope name non-compliant with the standard may cause problems at application level."
569
- message: "OAR040: Scope name value is non compliant with the standard."
572
+ message: "{{error}}"
570
573
  severity: "error"
571
574
  given: "$.x-wso2-security.apim.x-wso2-scopes[*].name"
572
575
  then:
573
- function: pattern
576
+ function: apq-forbidden-characters
574
577
  functionOptions:
575
- match: "^[a-zA-Z]{4,}_(SC|sc)_[a-zA-Z0-9]{1,}$"
578
+ pattern: "^[a-zA-Z]{4,}_(SC|sc)_[a-zA-Z0-9]{1,}$"
576
579
  apiq:OAR041:
577
580
  description: Available scopes must be defined at the end of the document, at the root level, inside `x-wso2-security`.
578
581
  message: "OAR041:Scopes must be defined in `x-wso2-security` at the end of the document, before the paths."
@@ -618,7 +621,7 @@ rules:
618
621
  then:
619
622
  function: pattern
620
623
  functionOptions:
621
- match: "^(\\*|[a-zA-Z0-9.][a-zA-Z0-9.!#$&_^+\\-]+)/(\\*|[a-zA-Z0-9.][a-zA-Z0-9.!#$&_^+\\-]+(; charset=[a-zA-Z0-9_\\-]+)?)$"
624
+ match: '^(\*|[a-zA-Z0-9][a-zA-Z0-9.!#$&^_+\-]*)/(\*|[a-zA-Z0-9][a-zA-Z0-9.!#$&^_+\-]*)(?:[ \t]*;[ \t]*[a-zA-Z0-9!#$%&''*+\-.^_`|~]+=(?:[a-zA-Z0-9!#$%&''*+\-.^_`|~]+|"(?:[^"\\]|\\.)*"))*$'
622
625
  apiq:OAR045:
623
626
  description: "Response schema is required for responses with status codes 201 and others that return content."
624
627
  message: "OAR045: Response schema is required for status code '{{property}}'."
@@ -715,7 +718,7 @@ rules:
715
718
  message: "OAR052: Numeric types requires a format"
716
719
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR052.md"
717
720
  severity: warn
718
- given: "$..[?(@.type == 'integer' || @.type == 'number')]"
721
+ given: "$..[?(@ && (@.type == 'integer' || @.type == 'number'))]"
719
722
  then:
720
723
  field: format
721
724
  function: truthy
@@ -853,8 +856,8 @@ rules:
853
856
  functionOptions:
854
857
  type: pascal
855
858
  apiq:OAR069:
856
- description: "Path parameters should not appear as query parameters."
857
- message: "OAR069: Path parameter should not appear as both path and query parameter."
859
+ description: "Any param in PATH or QUERY should have a Bad Request (400) response."
860
+ message: "OAR069: Any param in PATH or QUERY should have a Bad Request (400) response."
858
861
  severity: "error"
859
862
  given: "$.paths"
860
863
  then:
@@ -900,26 +903,20 @@ rules:
900
903
  field: "429"
901
904
  function: truthy
902
905
  apiq:OAR074:
903
- description: "Numeric parameters should have minimum, maximum, or format restriction."
904
- message: "OAR074: Numeric parameter '{{property}}' should have a minimum, maximum, or format restriction."
906
+ description: "Numeric parameters should define minimum and maximum, or a format restriction."
907
+ message: "OAR074: Numeric parameter should define both 'minimum' and 'maximum', or a 'format' restriction."
905
908
  severity: error
906
- given: "$.paths[*][get,post,put,patch,delete].parameters[?(@.schema.type == 'integer' || @.schema.type == 'number')]"
909
+ resolved: false
910
+ given:
911
+ - "$..[?(@ && @.in && @.schema && (@.schema.type == 'integer' || @.schema.type == 'number'))].schema"
912
+ - "$..[?(@ && @.in && (@.type == 'integer' || @.type == 'number'))]"
907
913
  then:
908
- function: schema
909
- functionOptions:
910
- schema:
911
- type: object
912
- properties:
913
- schema:
914
- anyOf:
915
- - required: ["minimum"]
916
- - required: ["maximum"]
917
- - required: ["format"]
914
+ function: apq-numeric-parameter-integrity
918
915
  apiq:OAR075:
919
916
  description: "String parameters should have minLength, maxLength, pattern (regular expression), or enum restriction."
920
917
  message: "OAR075: String parameters should have minLength, maxLength, pattern, or enum restriction."
921
918
  severity: error
922
- given: "$.paths[*][get,post,put,patch,delete].parameters[?(@.schema.type == 'string' && @.in == 'path')]"
919
+ given: "$.paths[*][get,post,put,patch,delete].parameters[?(@ && @.schema && @.schema.type == 'string' && @.in == 'path')]"
923
920
  then:
924
921
  function: schema
925
922
  functionOptions:
@@ -937,7 +934,7 @@ rules:
937
934
  message: "OAR076: Schema should use well-defined type and format."
938
935
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR076.md"
939
936
  severity: error
940
- given: "$..[?(@.type=='number' || @.type=='integer')]"
937
+ given: "$..[?(@ && (@.type=='number' || @.type=='integer'))]"
941
938
  then:
942
939
  function: schema
943
940
  functionOptions:
@@ -1009,7 +1006,7 @@ rules:
1009
1006
  description: "The string properties 'product', 'line', and 'price' must define a byte or binary format."
1010
1007
  message: "{{error}}"
1011
1008
  severity: error
1012
- given: "$..[?(@.properties)]"
1009
+ given: "$..[?(@ && @.properties)]"
1013
1010
  then:
1014
1011
  function: apq-binary-format-check
1015
1012
  functionOptions:
@@ -1035,7 +1032,7 @@ rules:
1035
1032
  functionOptions:
1036
1033
  notMatch: "^password$"
1037
1034
  apiq:OAR085:
1038
- description: "The OpenAPI version must be one of: 2.0, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.1.0."
1035
+ description: "The OpenAPI version must be one of: 2.0, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.1.0, 3.2.0."
1039
1036
  message: "{{error}}"
1040
1037
  severity: warn
1041
1038
  given:
@@ -1044,7 +1041,7 @@ rules:
1044
1041
  then:
1045
1042
  function: apq-valid-openapi-version
1046
1043
  functionOptions:
1047
- valid-versions: "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0"
1044
+ valid-versions: "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.2.0"
1048
1045
  apiq:OAR086:
1049
1046
  description: "Descriptions must begin with a capital letter, end with a period, and not be empty."
1050
1047
  message: "OAR086: Descriptions must begin with a capital letter, end with a period, and not be empty."
@@ -1124,13 +1121,13 @@ rules:
1124
1121
  function: falsy
1125
1122
  apiq:OAR096:
1126
1123
  description: "Response code 403 must be defined for operations with security schemes defined."
1127
- message: "OAR096: Response code 403 must be defined for operations with security schemes defined."
1124
+ message: "{{error}}"
1128
1125
  severity: "error"
1129
- given: "$.paths[*][*].responses"
1126
+ given: "$.paths[*][*]"
1130
1127
  then:
1131
- function: truthy
1132
- field: "403"
1133
- message: "Operation must define a 403 Unauthorized response for security schemes."
1128
+ function: apq-security-required-response
1129
+ functionOptions:
1130
+ expected-codes: "403"
1134
1131
  apiq:OAR097:
1135
1132
  description: "The base path must contain at least two parts."
1136
1133
  message: "OAR097: Path has too few parts."
@@ -18,6 +18,10 @@ module.exports = (schema, options = {}, context) => {
18
18
  return;
19
19
  }
20
20
 
21
+ if (!propSchema || typeof propSchema !== 'object') {
22
+ return;
23
+ }
24
+
21
25
  // Check if it's a string type
22
26
  if (propSchema.type !== 'string') {
23
27
  return;
@@ -4,14 +4,16 @@
4
4
  * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
5
  */
6
6
  module.exports = (targetVal, options, context) => {
7
- const pattern = new RegExp((options && options.pattern) || '^[a-zA-Z0-9_\\-., ]+$');
7
+ const patternStr = (options && options.pattern) || '^[a-zA-Z0-9_\\-., ]+$';
8
+ const pattern = new RegExp(patternStr);
8
9
  const errors = [];
9
10
  const basePath = context.path || [];
11
+ const buildMessage = (value) => `Value '${value}' does not match the required pattern: ${patternStr}.`;
10
12
 
11
13
  if (typeof targetVal === 'string') {
12
14
  if (!pattern.test(targetVal)) {
13
15
  errors.push({
14
- message: context.rule.message,
16
+ message: buildMessage(targetVal),
15
17
  path: basePath
16
18
  });
17
19
  }
@@ -19,7 +21,7 @@ module.exports = (targetVal, options, context) => {
19
21
  targetVal.forEach((role, index) => {
20
22
  if (typeof role === 'string' && !pattern.test(role)) {
21
23
  errors.push({
22
- message: context.rule.message,
24
+ message: buildMessage(role),
23
25
  path: [...basePath, index]
24
26
  });
25
27
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @param {object} node
3
+ * @param {object} options
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+ module.exports = (node, options, context) => {
7
+ const results = [];
8
+
9
+ if (!node || typeof node !== 'object') return results;
10
+
11
+ const { type } = node;
12
+ if (type !== 'integer' && type !== 'number') return results;
13
+
14
+ const has = (field) => Object.prototype.hasOwnProperty.call(node, field);
15
+ const hasMin = has('minimum');
16
+ const hasMax = has('maximum');
17
+ const hasFormat = has('format');
18
+
19
+ const hasBothBounds = hasMin && hasMax;
20
+ const formatAlone = hasFormat && !hasMin && !hasMax;
21
+
22
+ if (hasBothBounds || formatAlone) return results;
23
+
24
+ const ruleCode = context.rule.name.split(':').pop();
25
+ results.push({
26
+ message: `${ruleCode}: Numeric parameter should define both 'minimum' and 'maximum', or a 'format' restriction.`,
27
+ path: [...context.path, 'type'],
28
+ });
29
+
30
+ return results;
31
+ };
@@ -5,7 +5,7 @@
5
5
  */
6
6
  module.exports = (targetVal, options, context) => {
7
7
  const minDepth = options['min-level'] ?? options.minDepth ?? null
8
- const maxDepth = options['max-level'] ?? options.maxDepth ?? 3
8
+ const maxDepth = options['max-level'] ?? options['max-level-allowed'] ?? options.maxDepth ?? 3
9
9
  const ignore = options.ignoreSegments ?? []
10
10
  const segments = targetVal
11
11
  .split('/')
@@ -20,8 +20,11 @@ module.exports = (targetVal, options, context) => {
20
20
  : depth > maxDepth
21
21
 
22
22
  if (isOutOfRange) {
23
+ const message = minDepth != null
24
+ ? `OAR014: Path depth (${depth}) must not fall within the non-suggested range ${minDepth}-${maxDepth}.`
25
+ : `OAR015: Path depth (${depth}) exceeds the maximum allowed level of ${maxDepth}.`
23
26
  return [
24
- { message: context.rule.message }
27
+ { message }
25
28
  ]
26
29
  }
27
30
  }
@@ -1,9 +1,7 @@
1
1
  /**
2
- * Validates that path parameters don't appear as query parameters
3
- * This prevents ambiguity and design issues
4
2
  *
5
- * @param {object} given - The paths object
6
- * @param {object} options - Function options
3
+ * @param {object} given - The paths object ($.paths)
4
+ * @param {object} options - Function options (unused)
7
5
  * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
8
6
  */
9
7
  module.exports = (given, options, context) => {
@@ -26,27 +24,25 @@ module.exports = (given, options, context) => {
26
24
  continue;
27
25
  }
28
26
 
29
- const parameters = operation.parameters || [];
30
-
27
+ const parameters = operation.parameters;
31
28
  if (!Array.isArray(parameters)) {
32
29
  continue;
33
30
  }
34
31
 
35
- const hasPathOrQueryParams = parameters.some(
36
- param => param && (param.in === 'path' || param.in === 'query')
37
- );
38
-
39
- if (!hasPathOrQueryParams) {
32
+ const responses = operation.responses || {};
33
+ const has400 = Boolean(responses['400']);
34
+ if (has400) {
40
35
  continue;
41
36
  }
42
37
 
43
- const responses = operation.responses || {};
44
- if (!responses['400']) {
45
- errors.push({
46
- message: `OAR069: Any param in PATH or QUERY, should have bad request (400) response.`,
47
- path: [...context.path, pathKey, operationKey, 'responses']
48
- });
49
- }
38
+ parameters.forEach((param, index) => {
39
+ if (param && (param.in === 'path' || param.in === 'query')) {
40
+ errors.push({
41
+ message: context.rule.message,
42
+ path: [...context.path, pathKey, operationKey, 'parameters', index]
43
+ });
44
+ }
45
+ });
50
46
  }
51
47
  }
52
48
 
@@ -15,6 +15,7 @@ function isValidPattern(pattern) {
15
15
 
16
16
  module.exports = (targetVal, _options, context) => {
17
17
  const typePath = [...context.path, 'type'];
18
+
18
19
  const format = targetVal.format;
19
20
  if (format !== undefined && format !== null) {
20
21
  if (!VALID_FORMATS.has(String(format).toLowerCase())) {
@@ -23,6 +24,11 @@ module.exports = (targetVal, _options, context) => {
23
24
  return [];
24
25
  }
25
26
 
27
+ const enumValues = targetVal.enum;
28
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
29
+ return [];
30
+ }
31
+
26
32
  const pattern = targetVal.pattern;
27
33
  if (pattern !== undefined && pattern !== null && String(pattern).length > 0 && isValidPattern(String(pattern))) {
28
34
  return [];
@@ -1,7 +1,6 @@
1
1
  /**
2
- * Validates that operations with security defined include a 401 Unauthorized response
3
- * @param {object} given - The operation node
4
- * @param {object} options - Function options
2
+ * @param {object} given - The operation node ($.paths[*][*])
3
+ * @param {object} options - Function options ({ 'expected-codes': '401' | '403' | '401,403' | ... })
5
4
  * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
6
5
  */
7
6
  module.exports = (given, options, context) => {
@@ -16,6 +15,11 @@ module.exports = (given, options, context) => {
16
15
  return results;
17
16
  }
18
17
 
18
+ const raw = (options && options['expected-codes']) || '401';
19
+ const expectedCodes = (Array.isArray(raw) ? raw : String(raw).split(','))
20
+ .map((code) => String(code).trim())
21
+ .filter(Boolean);
22
+
19
23
  // Check if this operation has security defined
20
24
  const operationHasSecurity = given.security &&
21
25
  Array.isArray(given.security) &&
@@ -32,15 +36,22 @@ module.exports = (given, options, context) => {
32
36
  // Ignore errors accessing root security
33
37
  }
34
38
 
35
- // Only require 401 if security is defined (operation-level or global)
36
- if (operationHasSecurity || globalHasSecurity) {
37
- if (!responses['401']) {
39
+ if (!(operationHasSecurity || globalHasSecurity)) {
40
+ return results;
41
+ }
42
+
43
+ const ruleName = (context.rule && context.rule.name) || '';
44
+ const oarId = ruleName.includes(':') ? ruleName.split(':').pop() : ruleName;
45
+ const prefix = oarId ? `${oarId}: ` : '';
46
+
47
+ expectedCodes.forEach((code) => {
48
+ if (!responses[code]) {
38
49
  results.push({
39
- message: context.rule.message || 'OAR035: Response code 401 must be defined for operations with security schemes defined.',
40
- path: [...context.path, 'responses']
50
+ message: `${prefix}Response code ${code} must be defined for operations with security schemes defined.`,
51
+ path: [...context.path, 'responses'],
41
52
  });
42
53
  }
43
- }
54
+ });
44
55
 
45
56
  return results;
46
57
  };
@@ -1,4 +1,4 @@
1
- const DEFAULT_VALID_VERSIONS = '2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0';
1
+ const DEFAULT_VALID_VERSIONS = '2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.2.0';
2
2
 
3
3
  /**
4
4
  * @param {string} targetVal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-quality-spectral-ruleset",
3
- "version": "1.4.0",
3
+ "version": "1.4.1-beta.2",
4
4
  "description": "Spectral ruleset by API Quality",
5
5
  "main": "apq-spectral.yaml",
6
6
  "files": [
@@ -25,10 +25,10 @@
25
25
  "url": "https://github.com/apiaddicts/apquality-spectral"
26
26
  },
27
27
  "devDependencies": {
28
- "@stoplight/spectral-core": "^1.19.5",
29
- "@stoplight/spectral-functions": "^1.9.0",
30
- "@stoplight/spectral-ruleset-migrator": "^1.10.0",
31
- "@stoplight/spectral-rulesets": "^1.20.2",
28
+ "@stoplight/spectral-core": "1.23.0",
29
+ "@stoplight/spectral-functions": "^1.10.5",
30
+ "@stoplight/spectral-ruleset-migrator": "^1.12.1",
31
+ "@stoplight/spectral-rulesets": "1.22.2",
32
32
  "@types/jest": "^29.5.14",
33
33
  "coverage-badges-cli": "^2.1.0",
34
34
  "jest": "^29.7.0"