api-quality-spectral-ruleset 1.4.1 → 1.5.0-beta.1

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
@@ -30,6 +30,8 @@ functions:
30
30
  - apq-status-endpoint-check
31
31
  - apq-validate-structure
32
32
  - apq-numeric-parameter-integrity
33
+ - apq-wso2-scopes-valid
34
+ - apq-numeric-path-param
33
35
  extends:
34
36
  - spectral:asyncapi
35
37
  - spectral:oas
@@ -46,14 +48,12 @@ rules:
46
48
  match: "^https://"
47
49
  apiq:OAR002:
48
50
  description: "A wrong scope definition may cause problems to import the API definition into WSO2."
49
- message: "OAR002: Scope definition must include the 'key' attribute along with 'name' and 'roles'."
51
+ message: "{{error}}"
50
52
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR002.md"
51
53
  severity: "error"
52
- given: "$.x-wso2-security.apim.x-wso2-scopes[*]"
54
+ given: "$.x-wso2-security.apim.x-wso2-scopes"
53
55
  then:
54
- - function: "truthy"
55
- field: "key"
56
- message: "Scope must define a 'key' attribute."
56
+ function: apq-wso2-scopes-valid
57
57
  apiq:OAR003:
58
58
  description: "A description can help other developers to understand the correct use of the scope."
59
59
  message: "OAR003: Scope must define a 'description' attribute."
@@ -194,7 +194,7 @@ rules:
194
194
  message: "OAR016: Numeric types must use a valid format for their type."
195
195
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR016.md"
196
196
  severity: error
197
- given: "$..[?(@ && (@.type=='number' || @.type=='integer'))]"
197
+ given: "$..[?(@ && (@.type=='number' || @.type=='integer' || (@.type && @.type.indexOf && (@.type.indexOf('number') > -1 || @.type.indexOf('integer') > -1))))]"
198
198
  then:
199
199
  function: schema
200
200
  functionOptions:
@@ -485,7 +485,7 @@ rules:
485
485
  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."
486
486
  severity: error
487
487
  resolved: false
488
- given: "$..[?(@ && @.type=='string')]"
488
+ given: "$..[?(@ && @.type)]"
489
489
  then:
490
490
  function: apq-schema-format
491
491
  apiq:OAR038:
@@ -686,7 +686,7 @@ rules:
686
686
  message: "OAR052: Numeric types requires a format"
687
687
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR052.md"
688
688
  severity: warn
689
- given: "$..[?(@ && (@.type == 'integer' || @.type == 'number'))]"
689
+ given: "$..[?(@ && (@.type == 'integer' || @.type == 'number' || (@.type && @.type.indexOf && (@.type.indexOf('integer') > -1 || @.type.indexOf('number') > -1))))]"
690
690
  then:
691
691
  field: format
692
692
  function: truthy
@@ -836,10 +836,7 @@ rules:
836
836
  severity: error
837
837
  given: "$.paths[*][get,post,put,patch,delete].parameters[?(@.in == 'path')]"
838
838
  then:
839
- field: "schema.type"
840
- function: pattern
841
- functionOptions:
842
- match: "^(?!integer|number)"
839
+ function: apq-numeric-path-param
843
840
  apiq:OAR071:
844
841
  description: "Query parameters 'param1', 'param2', and 'param3' must be defined in the operation."
845
842
  message: "OAR071: Query parameters 'param1', 'param2', and 'param3' must be defined."
@@ -876,15 +873,15 @@ rules:
876
873
  severity: error
877
874
  resolved: false
878
875
  given:
879
- - "$..[?(@ && @.in && @.schema && (@.schema.type == 'integer' || @.schema.type == 'number'))].schema"
880
- - "$..[?(@ && @.in && (@.type == 'integer' || @.type == 'number'))]"
876
+ - "$..[?(@ && @.in && @.schema && (@.schema.type == 'integer' || @.schema.type == 'number' || (@.schema.type && @.schema.type.indexOf && (@.schema.type.indexOf('integer') > -1 || @.schema.type.indexOf('number') > -1))))].schema"
877
+ - "$..[?(@ && @.in && (@.type == 'integer' || @.type == 'number' || (@.type && @.type.indexOf && (@.type.indexOf('integer') > -1 || @.type.indexOf('number') > -1))))]"
881
878
  then:
882
879
  function: apq-numeric-parameter-integrity
883
880
  apiq:OAR075:
884
881
  description: "String parameters should have minLength, maxLength, pattern (regular expression), or enum restriction."
885
882
  message: "OAR075: String parameters should have minLength, maxLength, pattern, or enum restriction."
886
883
  severity: error
887
- given: "$.paths[*][get,post,put,patch,delete].parameters[?(@ && @.schema && @.schema.type == 'string' && @.in == 'path')]"
884
+ given: "$.paths[*][get,post,put,patch,delete].parameters[?(@ && @.schema && (@.schema.type == 'string' || (@.schema.type && @.schema.type.indexOf && @.schema.type.indexOf('string') > -1)) && @.in == 'path')]"
888
885
  then:
889
886
  function: schema
890
887
  functionOptions:
@@ -902,7 +899,7 @@ rules:
902
899
  message: "OAR076: Schema should use well-defined type and format."
903
900
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR076.md"
904
901
  severity: error
905
- given: "$..[?(@ && (@.type=='number' || @.type=='integer'))]"
902
+ given: "$..[?(@ && (@.type=='number' || @.type=='integer' || (@.type && @.type.indexOf && (@.type.indexOf('number') > -1 || @.type.indexOf('integer') > -1))))]"
906
903
  then:
907
904
  function: schema
908
905
  functionOptions:
@@ -1000,7 +997,7 @@ rules:
1000
997
  functionOptions:
1001
998
  notMatch: "^password$"
1002
999
  apiq:OAR085:
1003
- 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."
1000
+ description: "The OpenAPI version must be one of: 2.0, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.0.4, 3.1.0, 3.1.1, 3.1.2, 3.2.0."
1004
1001
  message: "{{error}}"
1005
1002
  severity: warn
1006
1003
  given:
@@ -1009,7 +1006,7 @@ rules:
1009
1006
  then:
1010
1007
  function: apq-valid-openapi-version
1011
1008
  functionOptions:
1012
- valid-versions: "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.2.0"
1009
+ valid-versions: "2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.0.4,3.1.0,3.1.1,3.1.2,3.2.0"
1013
1010
  apiq:OAR086:
1014
1011
  description: "Descriptions must begin with a capital letter, end with a period, and not be empty."
1015
1012
  message: "OAR086: Descriptions must begin with a capital letter, end with a period, and not be empty."
@@ -5,7 +5,6 @@ module.exports = (schema, options = {}, context) => {
5
5
  return results;
6
6
  }
7
7
 
8
- // Fields that must have byte or binary format
9
8
  const fieldsToCheck = (options['fields-to-apply'] || 'product,line,price')
10
9
  .split(',')
11
10
  .map(f => f.trim().toLowerCase());
@@ -13,7 +12,6 @@ module.exports = (schema, options = {}, context) => {
13
12
  const properties = schema.properties || {};
14
13
 
15
14
  Object.entries(properties).forEach(([propName, propSchema]) => {
16
- // Only check if this property is in our list
17
15
  if (!fieldsToCheck.includes(propName.toLowerCase())) {
18
16
  return;
19
17
  }
@@ -22,13 +20,17 @@ module.exports = (schema, options = {}, context) => {
22
20
  return;
23
21
  }
24
22
 
25
- // Check if it's a string type
26
- if (propSchema.type !== 'string') {
23
+ const type = propSchema.type;
24
+ const isStringType = type === 'string' || (Array.isArray(type) && type.includes('string'));
25
+ if (!isStringType) {
27
26
  return;
28
27
  }
29
28
 
30
29
  const format = propSchema.format || '';
31
- if (format !== 'byte' && format !== 'binary') {
30
+ const hasBinaryFormat = format === 'byte' || format === 'binary';
31
+ const hasContentEncoding = propSchema.contentEncoding !== undefined
32
+ || propSchema.contentMediaType !== undefined;
33
+ if (!hasBinaryFormat && !hasContentEncoding) {
32
34
  results.push({
33
35
  message: `OAR082: Property '${propName}' must define a 'byte' or 'binary' format (currently: ${format || 'missing'}).`,
34
36
  path: [...context.path, 'properties', propName, 'type'],
@@ -9,7 +9,9 @@ module.exports = (node, options, context) => {
9
9
  if (!node || typeof node !== 'object') return results;
10
10
 
11
11
  const { type } = node;
12
- if (type !== 'integer' && type !== 'number') return results;
12
+ const types = Array.isArray(type) ? type : [type];
13
+ const isNumeric = types.indexOf('integer') > -1 || types.indexOf('number') > -1;
14
+ if (!isNumeric) return results;
13
15
 
14
16
  const has = (field) => Object.prototype.hasOwnProperty.call(node, field);
15
17
  const hasMin = has('minimum');
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @param {object} param the parameter object (given selects params with `in: path`)
3
+ * @param {object} options
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+ module.exports = (param, options, context) => {
7
+ const results = [];
8
+ if (!param || typeof param !== 'object' || param.in !== 'path') return results;
9
+
10
+ let typeVal;
11
+ let typePath;
12
+ if (param.schema && param.schema.type !== undefined) {
13
+ typeVal = param.schema.type;
14
+ typePath = [...context.path, 'schema', 'type'];
15
+ } else if (param.type !== undefined) {
16
+ typeVal = param.type;
17
+ typePath = [...context.path, 'type'];
18
+ } else {
19
+ return results;
20
+ }
21
+
22
+ const types = Array.isArray(typeVal) ? typeVal : [typeVal];
23
+ const isNumeric = types.indexOf('integer') > -1
24
+ || types.indexOf('number') > -1
25
+ || types.indexOf('float') > -1;
26
+ if (!isNumeric) return results;
27
+
28
+ const ruleCode = context.rule.name.split(':').pop();
29
+ results.push({
30
+ message: `${ruleCode}: Parameters in path should not be numeric.`,
31
+ path: typePath,
32
+ });
33
+ return results;
34
+ };
@@ -6,7 +6,10 @@
6
6
  module.exports = (given, options, context) => {
7
7
  const results = [];
8
8
 
9
- if (!given || given.type !== 'object') return results;
9
+ if (!given) return results;
10
+ const t = given.type;
11
+ const isObjectType = t === 'object' || (Array.isArray(t) && t.indexOf('object') > -1);
12
+ if (!isObjectType) return results;
10
13
 
11
14
  const { required, properties } = given;
12
15
  if (!Array.isArray(required)) return results;
@@ -16,6 +16,12 @@ function isValidPattern(pattern) {
16
16
  module.exports = (targetVal, _options, context) => {
17
17
  const typePath = [...context.path, 'type'];
18
18
 
19
+ const type = targetVal.type;
20
+ const isStringType = type === 'string' || (Array.isArray(type) && type.includes('string'));
21
+ if (!isStringType) {
22
+ return [];
23
+ }
24
+
19
25
  const format = targetVal.format;
20
26
  if (format !== undefined && format !== null) {
21
27
  if (!VALID_FORMATS.has(String(format).toLowerCase())) {
@@ -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,3.2.0';
1
+ const DEFAULT_VALID_VERSIONS = '2.0,3.0.0,3.0.1,3.0.2,3.0.3,3.0.4,3.1.0,3.1.1,3.1.2,3.2.0';
2
2
 
3
3
  /**
4
4
  * @param {string} targetVal
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @param {*} targetVal the `x-wso2-scopes` container node value
3
+ * @param {object} _options unused
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+ const DEFINITION_WRONG = "OAR002: WSO2 scopes definition is wrong";
7
+ const REQUIRED_PROP = "OAR002: WSO2 scope '{0}' is required";
8
+ const REQUIRED_PROPERTIES = ['name', 'key', 'roles'];
9
+
10
+ const requiredMessage = (prop) => REQUIRED_PROP.replace('{0}', prop);
11
+
12
+ // A scope property counts as "required but missing" when it is absent, null, a
13
+ // blank string, or an empty collection ([] / {}) — mirrors Sonar's OAR002 check.
14
+ const isEmptyValue = (value) =>
15
+ value === null
16
+ || (typeof value === 'string' && value.trim() === '')
17
+ || (Array.isArray(value) && value.length === 0)
18
+ || (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
19
+
20
+ module.exports = (targetVal, _options, context) => {
21
+ const errors = [];
22
+ const basePath = context.path || [];
23
+
24
+ if (targetVal === null || typeof targetVal !== 'object') {
25
+ errors.push({ message: DEFINITION_WRONG, path: basePath });
26
+ return errors;
27
+ }
28
+
29
+ const entries = Array.isArray(targetVal)
30
+ ? targetVal.map((scope, index) => [index, scope])
31
+ : Object.keys(targetVal).map((key) => [key, targetVal[key]]);
32
+
33
+ if (entries.length === 0) {
34
+ errors.push({ message: DEFINITION_WRONG, path: basePath });
35
+ return errors;
36
+ }
37
+
38
+ entries.forEach(([key, scope]) => {
39
+ const scopePath = [...basePath, key];
40
+
41
+ if (scope === null || typeof scope !== 'object' || Array.isArray(scope)) {
42
+ REQUIRED_PROPERTIES.forEach((prop) => {
43
+ errors.push({ message: requiredMessage(prop), path: scopePath });
44
+ });
45
+ return;
46
+ }
47
+
48
+ REQUIRED_PROPERTIES.forEach((prop) => {
49
+ if (!Object.prototype.hasOwnProperty.call(scope, prop)) {
50
+ errors.push({ message: requiredMessage(prop), path: scopePath });
51
+ } else if (isEmptyValue(scope[prop])) {
52
+ errors.push({ message: requiredMessage(prop), path: [...scopePath, prop] });
53
+ }
54
+ });
55
+ });
56
+
57
+ return errors;
58
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-quality-spectral-ruleset",
3
- "version": "1.4.1",
3
+ "version": "1.5.0-beta.1",
4
4
  "description": "Spectral ruleset by API Quality",
5
5
  "main": "apq-spectral.yaml",
6
6
  "files": [