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

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,8 +30,16 @@ functions:
30
30
  - apq-status-endpoint-check
31
31
  - apq-validate-structure
32
32
  - apq-numeric-parameter-integrity
33
+ - apq-path-pattern
34
+ - apq-query-params-optional
33
35
  - apq-wso2-scopes-valid
34
36
  - apq-numeric-path-param
37
+ - apq-numeric-invalid-format
38
+ - apq-numeric-missing-format
39
+ - apq-numeric-well-defined-format
40
+ - apq-string-parameter-integrity
41
+ - apq-example-schema-types
42
+ - apq-standard-response-schema
35
43
  extends:
36
44
  - spectral:asyncapi
37
45
  - spectral:oas
@@ -194,29 +202,10 @@ rules:
194
202
  message: "OAR016: Numeric types must use a valid format for their type."
195
203
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR016.md"
196
204
  severity: error
197
- given: "$..[?(@ && (@.type=='number' || @.type=='integer' || (@.type && @.type.indexOf && (@.type.indexOf('number') > -1 || @.type.indexOf('integer') > -1))))]"
205
+ resolved: false
206
+ given: "$..[?(@ && @.type)]"
198
207
  then:
199
- function: schema
200
- functionOptions:
201
- schema:
202
- required: [format]
203
- allOf:
204
- - if:
205
- properties:
206
- type:
207
- const: integer
208
- then:
209
- properties:
210
- format:
211
- enum: [int32, int64, uint32, uint64]
212
- - if:
213
- properties:
214
- type:
215
- const: number
216
- then:
217
- properties:
218
- format:
219
- enum: [float, double, decimal]
208
+ function: apq-numeric-invalid-format
220
209
  apiq:OAR017:
221
210
  description: "Resource path should alternate static and parametrized parts."
222
211
  message: "OAR017: Resource path should alternate static and parametrized parts"
@@ -374,38 +363,14 @@ rules:
374
363
  function: apq-has-filter-query-param
375
364
  apiq:OAR029:
376
365
  description: "A response not compliant with the standard may cause application issues."
377
- message: "OAR029: A response not compliant with the standard may cause application issues."
366
+ message: "{{error}}"
378
367
  severity: error
379
- given: "$.components.schemas.errorResponse"
368
+ given: "$.paths"
380
369
  then:
381
- function: schema
370
+ function: apq-standard-response-schema
382
371
  functionOptions:
383
- schema:
384
- type: object
385
- properties:
386
- properties:
387
- type: object
388
- properties:
389
- error:
390
- type: object
391
- properties:
392
- properties:
393
- type: object
394
- properties:
395
- code:
396
- type: string
397
- message:
398
- type: string
399
- details:
400
- type: array
401
- httpStatus:
402
- type: integer
403
- required:
404
- - code
405
- - message
406
- - httpStatus
407
- required:
408
- - properties
372
+ response-schema: '{"type":"object","properties":{"status":{"type":"object","properties":{"code":{"type":"integer"},"description":{"type":"string"},"internal_code":{"type":"string"},"errors":{"type":"array","nullable":true,"items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}}}}},"required":["code"]},"payload":{"type":"any"}},"required":["status","payload"]}'
373
+ path-exclusions: "/status"
409
374
  apiq:OAR030:
410
375
  description: "The configured status endpoint must be declared with the configured HTTP method."
411
376
  message: "OAR030: The required status endpoint is not declared or does not have the required method."
@@ -686,10 +651,10 @@ rules:
686
651
  message: "OAR052: Numeric types requires a format"
687
652
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR052.md"
688
653
  severity: warn
689
- given: "$..[?(@ && (@.type == 'integer' || @.type == 'number' || (@.type && @.type.indexOf && (@.type.indexOf('integer') > -1 || @.type.indexOf('number') > -1))))]"
654
+ resolved: false
655
+ given: "$..[?(@ && @.type)]"
690
656
  then:
691
- field: format
692
- function: truthy
657
+ function: apq-numeric-missing-format
693
658
  apiq:OAR053:
694
659
  description: "Response headers for API observability and tracing must be defined (excluding 204 responses and health endpoints)."
695
660
  message: "OAR053: Response must include mandatory headers and exclude forbidden headers."
@@ -716,12 +681,18 @@ rules:
716
681
  match: ^(http(s)?:\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.apiquality.io\b([-a-zA-Z0-9@:%_\+.~#?&\/=]*)$
717
682
  apiq:OAR060:
718
683
  description: "All query parameters must be defined as optional."
719
- message: "OAR060: All query parameter must be optional (required: false)."
684
+ message: "{{error}}"
720
685
  severity: error
721
- given: "$.paths[*][get,post,put,patch,delete].parameters[?(@.in == 'query')]"
686
+ given:
687
+ - "$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[?(@.in == 'query')]"
688
+ - "$.paths[*].parameters[?(@.in == 'query')]"
689
+ - "$.components.parameters[?(@.in == 'query')]"
690
+ - "$.parameters[?(@.in == 'query')]"
722
691
  then:
723
692
  field: "required"
724
- function: falsy
693
+ function: apq-query-params-optional
694
+ functionOptions:
695
+ path-exclusions: "/status"
725
696
  apiq:OAR061:
726
697
  description: "Ensure get have mandatory response codes"
727
698
  message: "OAR061: Ensure get have the mandatory response codes"
@@ -881,47 +852,25 @@ rules:
881
852
  description: "String parameters should have minLength, maxLength, pattern (regular expression), or enum restriction."
882
853
  message: "OAR075: String parameters should have minLength, maxLength, pattern, or enum restriction."
883
854
  severity: error
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')]"
855
+ resolved: false
856
+ given:
857
+ - "$.paths[*][*].parameters[*]"
858
+ - "$.paths[*].parameters[*]"
859
+ - "$.components.parameters[*]"
860
+ - "$.parameters[*]"
885
861
  then:
886
- function: schema
862
+ function: apq-string-parameter-integrity
887
863
  functionOptions:
888
- schema:
889
- type: object
890
- properties:
891
- schema:
892
- anyOf:
893
- - required: ["minLength"]
894
- - required: ["maxLength"]
895
- - required: ["pattern"]
896
- - required: ["enum"]
864
+ parameter_integrity: "minLength,maxLength,pattern,enum"
897
865
  apiq:OAR076:
898
866
  description: "Schema should use well-defined type and format."
899
867
  message: "OAR076: Schema should use well-defined type and format."
900
868
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR076.md"
901
869
  severity: error
902
- given: "$..[?(@ && (@.type=='number' || @.type=='integer' || (@.type && @.type.indexOf && (@.type.indexOf('number') > -1 || @.type.indexOf('integer') > -1))))]"
870
+ resolved: false
871
+ given: "$..[?(@ && @.type)]"
903
872
  then:
904
- function: schema
905
- functionOptions:
906
- schema:
907
- required: [format]
908
- allOf:
909
- - if:
910
- properties:
911
- type:
912
- const: integer
913
- then:
914
- properties:
915
- format:
916
- enum: [int32, int64]
917
- - if:
918
- properties:
919
- type:
920
- const: number
921
- then:
922
- properties:
923
- format:
924
- enum: [float, double]
873
+ function: apq-numeric-well-defined-format
925
874
  apiq:OAR077:
926
875
  description: "All parameters in query must be snake_case."
927
876
  message: "OAR077: All parameters in query must be snake_case."
@@ -1191,19 +1140,10 @@ rules:
1191
1140
  description: "The schemas should match the provided examples."
1192
1141
  message: "OAR108: Schema does not match the provided example."
1193
1142
  severity: "error"
1194
- given:
1195
- - "$.paths['/item'].get.responses['200'].content['application/json'].example"
1196
- - "$.paths['/item'].get.responses['200'].examples['application/json']"
1143
+ resolved: false
1144
+ given: "$.paths[*][*].responses[*]"
1197
1145
  then:
1198
- - function: "schema"
1199
- functionOptions:
1200
- schema:
1201
- type: "object"
1202
- properties:
1203
- id:
1204
- type: "integer"
1205
- nombre:
1206
- type: "string"
1146
+ function: apq-example-schema-types
1207
1147
  apiq:OAR109:
1208
1148
  description: "Use default response instead of directly specifying 5XX codes."
1209
1149
  message: "OAR109: Use default response instead of specifying 5XX codes directly."
@@ -1257,8 +1197,17 @@ rules:
1257
1197
  message: "OAR115: All fields in the required array must be defined in schema properties."
1258
1198
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR115.md"
1259
1199
  severity: warn
1260
- given:
1261
- - "$.components.schemas[*]"
1262
- - "$.definitions[*]"
1200
+ resolved: false
1201
+ given: "$..[?(@ && @.required)]"
1263
1202
  then:
1264
- function: apq-required-fields-exist
1203
+ function: apq-required-fields-exist
1204
+ apiq:OAR116:
1205
+ description: "Every API path must match the configured regular expression."
1206
+ message: "{{error}}"
1207
+ documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR116.md"
1208
+ severity: error
1209
+ given: "$.paths.*~"
1210
+ then:
1211
+ function: apq-path-pattern
1212
+ functionOptions:
1213
+ pattern: "^/"
@@ -0,0 +1,116 @@
1
+ function getPrimaryType(type) {
2
+ if (type === undefined || type === null) return null;
3
+ if (Array.isArray(type)) {
4
+ for (const element of type) {
5
+ if (element === null || element === undefined) continue;
6
+ const value = String(element);
7
+ if (value !== 'null') return value;
8
+ }
9
+ return null;
10
+ }
11
+ if (typeof type === 'object') return null;
12
+ return String(type);
13
+ }
14
+
15
+ function determineExampleType(value) {
16
+ if (value === null || value === undefined) return 'null';
17
+ if (Array.isArray(value)) return 'array';
18
+ if (typeof value === 'object') return 'object';
19
+ if (typeof value === 'boolean') return 'boolean';
20
+ if (typeof value === 'number') return Number.isInteger(value) ? 'integer' : 'number';
21
+ return 'string';
22
+ }
23
+
24
+ function schemaTypes(schema) {
25
+ const types = new Map();
26
+ if (!schema || typeof schema !== 'object') return types;
27
+ const { properties } = schema;
28
+ if (!properties || typeof properties !== 'object') return types;
29
+ Object.keys(properties).forEach((name) => {
30
+ const property = properties[name];
31
+ const type = property && typeof property === 'object' ? property.type : undefined;
32
+ types.set(name, getPrimaryType(type));
33
+ });
34
+ return types;
35
+ }
36
+
37
+ function exampleTypes(example) {
38
+ const types = new Map();
39
+ if (!example || typeof example !== 'object' || Array.isArray(example)) return types;
40
+ Object.keys(example).forEach((name) => {
41
+ types.set(name, determineExampleType(example[name]));
42
+ });
43
+ return types;
44
+ }
45
+
46
+ function swaggerExampleTypes(examples) {
47
+ const types = new Map();
48
+ if (!examples || typeof examples !== 'object' || Array.isArray(examples)) return types;
49
+ Object.keys(examples).forEach((mediaType) => {
50
+ const payload = examples[mediaType];
51
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return;
52
+ Object.keys(payload).forEach((name) => {
53
+ types.set(name, determineExampleType(payload[name]));
54
+ });
55
+ });
56
+ return types;
57
+ }
58
+
59
+ function isCompatible(expectedType, actualType) {
60
+ if (expectedType === null) return true;
61
+ if (actualType === 'null') return true;
62
+ if (expectedType === actualType) return true;
63
+ return expectedType === 'number' && actualType === 'integer';
64
+ }
65
+
66
+ function mismatches(schema, example, isSwagger) {
67
+ const expected = schemaTypes(schema);
68
+ if (expected.size === 0) return false;
69
+ const actual = isSwagger ? swaggerExampleTypes(example) : exampleTypes(example);
70
+
71
+ for (const [name, expectedType] of expected) {
72
+ const actualType = actual.has(name) ? actual.get(name) : 'unknown';
73
+ if (!isCompatible(expectedType, actualType)) return true;
74
+ }
75
+ return false;
76
+ }
77
+
78
+ function isPresent(value) {
79
+ return value !== undefined && value !== null;
80
+ }
81
+
82
+ /**
83
+ * @param {object} targetVal a Response Object
84
+ * @param {object} options
85
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
86
+ */
87
+ module.exports = (targetVal, options, context) => {
88
+ if (!targetVal || typeof targetVal !== 'object') return [];
89
+
90
+ const results = [];
91
+ const { content } = targetVal;
92
+
93
+ if (isPresent(content) && typeof content === 'object' && !Array.isArray(content)) {
94
+ Object.keys(content).forEach((mediaType) => {
95
+ const mediaTypeNode = content[mediaType];
96
+ if (!mediaTypeNode || typeof mediaTypeNode !== 'object') return;
97
+ const { schema, example } = mediaTypeNode;
98
+ if (!isPresent(schema) || !isPresent(example)) return;
99
+ if (mismatches(schema, example, false)) {
100
+ results.push({
101
+ message: context.rule.message,
102
+ path: [...context.path, 'content', mediaType, 'example'],
103
+ });
104
+ }
105
+ });
106
+ return results;
107
+ }
108
+
109
+ const { schema, examples } = targetVal;
110
+ if (!isPresent(schema) || !isPresent(examples)) return results;
111
+ if (mismatches(schema, examples, true)) {
112
+ results.push({ message: context.rule.message, path: [...context.path, 'examples'] });
113
+ }
114
+
115
+ return results;
116
+ };
@@ -0,0 +1,64 @@
1
+ const VALID_INTEGER_FORMATS = new Set(['int32', 'int64']);
2
+ const VALID_NUMBER_FORMATS = new Set(['float', 'double']);
3
+
4
+ function getPrimaryType(type) {
5
+ if (type === undefined || type === null) return null;
6
+ if (Array.isArray(type)) {
7
+ for (const element of type) {
8
+ if (element === null || element === undefined) continue;
9
+ const value = String(element);
10
+ if (value !== 'null') return value;
11
+ }
12
+ return null;
13
+ }
14
+ if (typeof type === 'object') return null;
15
+ return String(type);
16
+ }
17
+
18
+ function readFormat(node) {
19
+ if (!Object.prototype.hasOwnProperty.call(node, 'format')) return { present: false, blank: false };
20
+ const raw = node.format;
21
+ if (raw === null || raw === undefined) return { present: false, blank: false };
22
+ if (typeof raw === 'object') return { present: true, blank: true };
23
+ const value = String(raw).trim();
24
+ if (value === '') return { present: true, blank: true };
25
+ return { present: true, blank: false, value };
26
+ }
27
+
28
+ const NON_SCHEMA_CONTAINERS = new Set(['example', 'examples', 'default', 'enum']);
29
+
30
+ function isInsideNonSchemaValue(path) {
31
+ for (let i = 0; i < path.length; i += 1) {
32
+ const segment = path[i];
33
+ if (typeof segment !== 'string' || !NON_SCHEMA_CONTAINERS.has(segment)) continue;
34
+ if (i > 0 && path[i - 1] === 'properties') continue;
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+
40
+ function isSwaggerResponseHeader(path) {
41
+ const i = path.length - 2;
42
+ return i >= 0 && path[i] === 'headers' && (i === 0 || path[i - 1] !== 'properties');
43
+ }
44
+
45
+ /**
46
+ * @param {object} targetVal
47
+ * @param {object} options
48
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
49
+ */
50
+ module.exports = (targetVal, options, context) => {
51
+ if (!targetVal || typeof targetVal !== 'object') return [];
52
+ if (isInsideNonSchemaValue(context.path) || isSwaggerResponseHeader(context.path)) return [];
53
+
54
+ const type = getPrimaryType(targetVal.type);
55
+ if (type !== 'integer' && type !== 'number') return [];
56
+
57
+ const format = readFormat(targetVal);
58
+ if (!format.present || format.blank) return [];
59
+
60
+ const valid = type === 'integer' ? VALID_INTEGER_FORMATS : VALID_NUMBER_FORMATS;
61
+ if (valid.has(format.value)) return [];
62
+
63
+ return [{ message: context.rule.message, path: [...context.path, 'type'] }];
64
+ };
@@ -0,0 +1,58 @@
1
+ function getPrimaryType(type) {
2
+ if (type === undefined || type === null) return null;
3
+ if (Array.isArray(type)) {
4
+ for (const element of type) {
5
+ if (element === null || element === undefined) continue;
6
+ const value = String(element);
7
+ if (value !== 'null') return value;
8
+ }
9
+ return null;
10
+ }
11
+ if (typeof type === 'object') return null;
12
+ return String(type);
13
+ }
14
+
15
+ function readFormat(node) {
16
+ if (!Object.prototype.hasOwnProperty.call(node, 'format')) return { present: false, blank: false };
17
+ const raw = node.format;
18
+ if (raw === null || raw === undefined) return { present: false, blank: false };
19
+ if (typeof raw === 'object') return { present: true, blank: true };
20
+ const value = String(raw).trim();
21
+ if (value === '') return { present: true, blank: true };
22
+ return { present: true, blank: false, value };
23
+ }
24
+
25
+ const NON_SCHEMA_CONTAINERS = new Set(['example', 'examples', 'default', 'enum']);
26
+
27
+ function isInsideNonSchemaValue(path) {
28
+ for (let i = 0; i < path.length; i += 1) {
29
+ const segment = path[i];
30
+ if (typeof segment !== 'string' || !NON_SCHEMA_CONTAINERS.has(segment)) continue;
31
+ if (i > 0 && path[i - 1] === 'properties') continue;
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ function isSwaggerResponseHeader(path) {
38
+ const i = path.length - 2;
39
+ return i >= 0 && path[i] === 'headers' && (i === 0 || path[i - 1] !== 'properties');
40
+ }
41
+
42
+ /**
43
+ * @param {object} targetVal
44
+ * @param {object} options
45
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
46
+ */
47
+ module.exports = (targetVal, options, context) => {
48
+ if (!targetVal || typeof targetVal !== 'object') return [];
49
+ if (isInsideNonSchemaValue(context.path) || isSwaggerResponseHeader(context.path)) return [];
50
+
51
+ const type = getPrimaryType(targetVal.type);
52
+ if (type !== 'integer' && type !== 'number') return [];
53
+
54
+ const format = readFormat(targetVal);
55
+ if (format.present) return [];
56
+
57
+ return [{ message: context.rule.message, path: [...context.path, 'type'] }];
58
+ };
@@ -0,0 +1,64 @@
1
+ const VALID_INTEGER_FORMATS = new Set(['int32', 'int64']);
2
+ const VALID_NUMBER_FORMATS = new Set(['float', 'double']);
3
+
4
+ function getPrimaryType(type) {
5
+ if (type === undefined || type === null) return null;
6
+ if (Array.isArray(type)) {
7
+ for (const element of type) {
8
+ if (element === null || element === undefined) continue;
9
+ const value = String(element);
10
+ if (value !== 'null') return value;
11
+ }
12
+ return null;
13
+ }
14
+ if (typeof type === 'object') return null;
15
+ return String(type);
16
+ }
17
+
18
+ function readFormat(node) {
19
+ if (!Object.prototype.hasOwnProperty.call(node, 'format')) return { present: false, blank: false };
20
+ const raw = node.format;
21
+ if (raw === null || raw === undefined) return { present: false, blank: false };
22
+ if (typeof raw === 'object') return { present: true, blank: true };
23
+ const value = String(raw).trim();
24
+ if (value === '') return { present: true, blank: true };
25
+ return { present: true, blank: false, value };
26
+ }
27
+
28
+ const NON_SCHEMA_CONTAINERS = new Set(['example', 'examples', 'default', 'enum']);
29
+
30
+ function isInsideNonSchemaValue(path) {
31
+ for (let i = 0; i < path.length; i += 1) {
32
+ const segment = path[i];
33
+ if (typeof segment !== 'string' || !NON_SCHEMA_CONTAINERS.has(segment)) continue;
34
+ if (i > 0 && path[i - 1] === 'properties') continue;
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+
40
+ function isSwaggerResponseHeader(path) {
41
+ const i = path.length - 2;
42
+ return i >= 0 && path[i] === 'headers' && (i === 0 || path[i - 1] !== 'properties');
43
+ }
44
+
45
+ /**
46
+ * @param {object} targetVal
47
+ * @param {object} options
48
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
49
+ */
50
+ module.exports = (targetVal, options, context) => {
51
+ if (!targetVal || typeof targetVal !== 'object') return [];
52
+ if (isInsideNonSchemaValue(context.path) || isSwaggerResponseHeader(context.path)) return [];
53
+
54
+ const type = getPrimaryType(targetVal.type);
55
+ if (type !== 'integer' && type !== 'number') return [];
56
+
57
+ const format = readFormat(targetVal);
58
+ if (format.blank) return [];
59
+
60
+ const valid = type === 'integer' ? VALID_INTEGER_FORMATS : VALID_NUMBER_FORMATS;
61
+ if (format.present && valid.has(format.value)) return [];
62
+
63
+ return [{ message: context.rule.message, path: [...context.path, 'type'] }];
64
+ };
@@ -0,0 +1,14 @@
1
+ module.exports = (targetVal, options = {}) => {
2
+ const patternStr = (options && options.pattern) || '^/';
3
+
4
+ if (typeof targetVal !== 'string') {
5
+ return [];
6
+ }
7
+
8
+ const regex = new RegExp(patternStr);
9
+ if (!regex.test(targetVal)) {
10
+ return [{ message: `OAR116: Path does not match the required pattern: ${patternStr}` }];
11
+ }
12
+
13
+ return [];
14
+ };
@@ -0,0 +1,182 @@
1
+ const HTTP_VERBS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
2
+
3
+ // Guards against a cycle in a `$ref` chain (A -> B -> A).
4
+ const MAX_REF_DEPTH = 10;
5
+
6
+ // The `$ref` usage map depends only on the source document, so cache it per document instead of
7
+ // rebuilding it on every matched parameter.
8
+ const usageCache = new WeakMap();
9
+
10
+ const issue = () => [{ message: 'OAR060: All query parameter must be optional (required: false).' }];
11
+
12
+ function parseExclusions(value) {
13
+ return new Set(
14
+ String(value === undefined || value === null ? '' : value)
15
+ .split(',')
16
+ .map((p) => p.trim())
17
+ .filter(Boolean),
18
+ );
19
+ }
20
+
21
+ /**
22
+ * Split a local JSON pointer (`#/components/parameters/Foo`) into its decoded segments.
23
+ * Returns null for external refs and for anything that is not a document-local pointer.
24
+ */
25
+ function refToSegments(ref) {
26
+ if (typeof ref !== 'string' || !ref.startsWith('#/')) {
27
+ return null;
28
+ }
29
+ return ref.slice(2).split('/').map((raw) => {
30
+ let segment = raw;
31
+ try {
32
+ segment = decodeURIComponent(raw);
33
+ } catch (e) {
34
+ segment = raw;
35
+ }
36
+ return segment.replace(/~1/g, '/').replace(/~0/g, '~');
37
+ });
38
+ }
39
+
40
+ function getAt(doc, segments) {
41
+ return segments.reduce(
42
+ (node, segment) => (node !== null && typeof node === 'object' ? node[segment] : undefined),
43
+ doc,
44
+ );
45
+ }
46
+
47
+ const keyOf = (segments) => JSON.stringify(segments);
48
+
49
+ /**
50
+ * The two containers the rule's `given` scans for shared parameter definitions:
51
+ * `components.parameters.<name>` (OpenAPI 3) and `parameters.<name>` (OpenAPI 2).
52
+ */
53
+ function definitionSegments(path) {
54
+ if (path[0] === 'components' && path[1] === 'parameters' && path.length >= 3) {
55
+ return path.slice(0, 3);
56
+ }
57
+ if (path[0] === 'parameters' && path.length >= 2) {
58
+ return path.slice(0, 2);
59
+ }
60
+ return null;
61
+ }
62
+
63
+ const isSharedDefinitionRef = (segments) => segments !== null
64
+ && ((segments.length === 3 && segments[0] === 'components' && segments[1] === 'parameters')
65
+ || (segments.length === 2 && segments[0] === 'parameters'));
66
+
67
+ /** Map of shared-definition pointer -> set of API paths that reference it through a `$ref`. */
68
+ function buildRefUsages(doc) {
69
+ const usages = new Map();
70
+ const paths = doc && typeof doc === 'object' ? doc.paths : undefined;
71
+ if (!paths || typeof paths !== 'object') {
72
+ return usages;
73
+ }
74
+
75
+ Object.keys(paths).forEach((apiPath) => {
76
+ const pathItem = paths[apiPath];
77
+ if (!pathItem || typeof pathItem !== 'object') {
78
+ return;
79
+ }
80
+
81
+ const parameterLists = [pathItem.parameters];
82
+ HTTP_VERBS.forEach((verb) => {
83
+ const operation = pathItem[verb];
84
+ if (operation && typeof operation === 'object') {
85
+ parameterLists.push(operation.parameters);
86
+ }
87
+ });
88
+
89
+ parameterLists.forEach((list) => {
90
+ if (!Array.isArray(list)) {
91
+ return;
92
+ }
93
+ list.forEach((parameter) => {
94
+ let current = parameter;
95
+ for (let depth = 0; depth < MAX_REF_DEPTH; depth += 1) {
96
+ if (!current || typeof current !== 'object' || typeof current.$ref !== 'string') {
97
+ break;
98
+ }
99
+ const segments = refToSegments(current.$ref);
100
+ if (segments === null) {
101
+ break;
102
+ }
103
+ const key = keyOf(segments);
104
+ if (!usages.has(key)) {
105
+ usages.set(key, new Set());
106
+ }
107
+ usages.get(key).add(apiPath);
108
+ const next = getAt(doc, segments);
109
+ if (!next || next === current) {
110
+ break;
111
+ }
112
+ current = next;
113
+ }
114
+ });
115
+ });
116
+ });
117
+
118
+ return usages;
119
+ }
120
+
121
+ function refUsagesFor(doc) {
122
+ if (!doc || typeof doc !== 'object') {
123
+ return new Map();
124
+ }
125
+ let cached = usageCache.get(doc);
126
+ if (cached === undefined) {
127
+ cached = buildRefUsages(doc);
128
+ usageCache.set(doc, cached);
129
+ }
130
+ return cached;
131
+ }
132
+
133
+ /**
134
+ * True when the match landed on a `$ref` to a shared definition the rule also scans on its own —
135
+ * either a use site under `paths`, or one shared definition aliasing another.
136
+ *
137
+ * Spectral runs the rule over the resolved document, so it matches such a parameter once here and
138
+ * once at the definition it points to. Reporting is left to the definition-site match, which is
139
+ * also the only place Sonar reports it (its AST visits the definition, never the use sites).
140
+ */
141
+ function isRefToSharedDefinition(source, path) {
142
+ const parameterPath = path[path.length - 1] === 'required' ? path.slice(0, -1) : path;
143
+ const node = getAt(source, parameterPath);
144
+ if (!node || typeof node !== 'object' || typeof node.$ref !== 'string') {
145
+ return false;
146
+ }
147
+ return isSharedDefinitionRef(refToSegments(node.$ref));
148
+ }
149
+
150
+ module.exports = (targetVal, options = {}, context = {}) => {
151
+ const exclusions = parseExclusions((options || {})['path-exclusions']);
152
+ const path = (context && context.path) || [];
153
+ const source = context && context.document ? context.document.data : undefined;
154
+
155
+ if (isRefToSharedDefinition(source, path)) {
156
+ return [];
157
+ }
158
+
159
+ if (path[0] === 'paths') {
160
+ const apiPath = path.find((p) => typeof p === 'string' && p.startsWith('/'));
161
+ if (apiPath && exclusions.has(apiPath)) {
162
+ return [];
163
+ }
164
+ } else if (exclusions.size > 0) {
165
+ // A shared definition has no path of its own: the AST/JSONPath match lands on
166
+ // `components.parameters.<name>` (or `parameters.<name>` in OpenAPI 2). Exclude it only when
167
+ // every path that references it is excluded — an unreferenced definition stays in scope.
168
+ const definition = definitionSegments(path);
169
+ const usages = definition === null
170
+ ? undefined
171
+ : refUsagesFor(source).get(keyOf(definition));
172
+ if (usages !== undefined && usages.size > 0 && [...usages].every((u) => exclusions.has(u))) {
173
+ return [];
174
+ }
175
+ }
176
+
177
+ if (targetVal === true || targetVal === 'true') {
178
+ return issue();
179
+ }
180
+
181
+ return [];
182
+ };
@@ -1,3 +1,28 @@
1
+ const NON_SCHEMA_CONTAINERS = new Set(['example', 'examples', 'default', 'enum']);
2
+
3
+ function isInsideNonSchemaValue(path) {
4
+ for (let i = 0; i < path.length; i += 1) {
5
+ const segment = path[i];
6
+ if (typeof segment !== 'string' || !NON_SCHEMA_CONTAINERS.has(segment)) continue;
7
+ if (i > 0 && path[i - 1] === 'properties') continue;
8
+ return true;
9
+ }
10
+ return false;
11
+ }
12
+
13
+ function isResponseJsonSchema(path) {
14
+ const n = path.length;
15
+ if (path[n - 1] !== 'schema') return false;
16
+ if (n >= 5 && path[n - 2] === 'application/json' && path[n - 3] === 'content' && path[n - 5] === 'responses') {
17
+ return true;
18
+ }
19
+ return n >= 3 && path[n - 3] === 'responses';
20
+ }
21
+
22
+ function isObjectType(type) {
23
+ return type === 'object' || (Array.isArray(type) && type.indexOf('object') > -1);
24
+ }
25
+
1
26
  /**
2
27
  * @param {object} given
3
28
  * @param {object} options
@@ -6,10 +31,10 @@
6
31
  module.exports = (given, options, context) => {
7
32
  const results = [];
8
33
 
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;
34
+ if (!given || typeof given !== 'object') return results;
35
+ if (isInsideNonSchemaValue(context.path)) return results;
36
+
37
+ if (!isObjectType(given.type) && !isResponseJsonSchema(context.path)) return results;
13
38
 
14
39
  const { required, properties } = given;
15
40
  if (!Array.isArray(required)) return results;
@@ -21,7 +46,7 @@ module.exports = (given, options, context) => {
21
46
  if (!propertyNames.has(field)) {
22
47
  results.push({
23
48
  message: `${ruleCode}: This value does not exist, '${field}' must be defined in the schema properties.`,
24
- path: [...context.path, 'required', index]
49
+ path: [...context.path, 'required', index],
25
50
  });
26
51
  }
27
52
  });
@@ -13,7 +13,24 @@ function isValidPattern(pattern) {
13
13
  }
14
14
  }
15
15
 
16
+ const NON_SCHEMA_CONTAINERS = new Set(['example', 'examples', 'default', 'enum']);
17
+
18
+ function isNotASchemaNode(path) {
19
+ for (let i = 0; i < path.length; i += 1) {
20
+ const segment = path[i];
21
+ if (typeof segment !== 'string' || !NON_SCHEMA_CONTAINERS.has(segment)) continue;
22
+ if (i > 0 && path[i - 1] === 'properties') continue;
23
+ return true;
24
+ }
25
+ const last = path.length - 2;
26
+ return last >= 0 && path[last] === 'headers' && (last === 0 || path[last - 1] !== 'properties');
27
+ }
28
+
16
29
  module.exports = (targetVal, _options, context) => {
30
+ if (isNotASchemaNode(context.path)) {
31
+ return [];
32
+ }
33
+
17
34
  const typePath = [...context.path, 'type'];
18
35
 
19
36
  const type = targetVal.type;
@@ -0,0 +1,318 @@
1
+ const DEFAULT_RESPONSE_SCHEMA = '{"type":"object","properties":{"status":{"type":"object","properties":{"code":{"type":"integer"},"description":{"type":"string"},"internal_code":{"type":"string"},"errors":{"type":"array","nullable":true,"items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}}}}},"required":["code"]},"payload":{"type":"any"}},"required":["status","payload"]}';
2
+ const DEFAULT_PATH_EXCLUSIONS = '/status';
3
+
4
+ const TYPE_ANY = '*';
5
+ const OPERATIONS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
6
+
7
+ const MESSAGES = {
8
+ propertyMissing: (name) => `'${name}' property is missing`,
9
+ propertyWrongType: (name, type) => `'${name}' must be of type ${type}`,
10
+ propertyItemsMissing: (name) => `'${name}' items are missing`,
11
+ propertyItemsWrongType: (name, type) => `'${name}' items must be of type ${type}`,
12
+ requiredProperties: (fields) => `The following fields must be required: ${fields}`,
13
+ requiredOneProperty: () => 'At least one property must be defined',
14
+ };
15
+
16
+ function isType(type, name) {
17
+ if (name === TYPE_ANY) return true;
18
+ if (type === undefined || type === null) return false;
19
+ if (Array.isArray(type)) return type.some((element) => String(element) === name);
20
+ return String(type) === name;
21
+ }
22
+
23
+ function typeOf(node) {
24
+ return node && typeof node === 'object' ? node.type : undefined;
25
+ }
26
+
27
+ function getAllProperties(schema, basePath) {
28
+ const properties = new Map();
29
+ if (!schema || typeof schema !== 'object') return properties;
30
+
31
+ if (schema.properties && typeof schema.properties === 'object') {
32
+ Object.keys(schema.properties).forEach((name) => {
33
+ properties.set(name, {
34
+ value: schema.properties[name],
35
+ path: [...basePath, 'properties', name],
36
+ });
37
+ });
38
+ }
39
+
40
+ if (Array.isArray(schema.allOf)) {
41
+ schema.allOf.forEach((member, index) => {
42
+ getAllProperties(member, [...basePath, 'allOf', index]).forEach((entry, name) => {
43
+ properties.set(name, entry);
44
+ });
45
+ });
46
+ }
47
+
48
+ return properties;
49
+ }
50
+
51
+ function matchesTypeViaAllOf(node, expectedType) {
52
+ if (!node || !Array.isArray(node.allOf)) return false;
53
+ return node.allOf.some((member) => member && typeof member === 'object' && isType(member.type, expectedType));
54
+ }
55
+
56
+ function declaredTypeOf(declared) {
57
+ let type = declared && typeof declared === 'object' && typeof declared.type === 'string'
58
+ ? declared.type.trim()
59
+ : '';
60
+ if (type === '' || type === 'any') type = TYPE_ANY;
61
+ if (type === TYPE_ANY && declared && typeof declared === 'object' && declared.properties) type = 'object';
62
+ return type;
63
+ }
64
+
65
+ function createValidator(config, ruleCode) {
66
+ const results = [];
67
+
68
+ const push = (path, text) => results.push({ message: `${ruleCode}: ${text}`, path });
69
+
70
+ function validateProperty(properties, propertyName, propertyType, parentPath) {
71
+ const entry = properties.get(propertyName);
72
+ if (!entry) {
73
+ push(parentPath, MESSAGES.propertyMissing(propertyName));
74
+ return null;
75
+ }
76
+ const { value, path } = entry;
77
+ if (isType(typeOf(value), propertyType)) return entry;
78
+ if (matchesTypeViaAllOf(value, propertyType)) return entry;
79
+
80
+ const hasType = typeOf(value) !== undefined;
81
+ push(hasType ? [...path, 'type'] : path, MESSAGES.propertyWrongType(propertyName, propertyType));
82
+ return null;
83
+ }
84
+
85
+ function validateRequiredProperties(schema, schemaPath, requiredValues) {
86
+ const declared = schema && typeof schema === 'object' ? schema.required : undefined;
87
+ const present = new Set(Array.isArray(declared) ? declared.map(String) : []);
88
+ if (requiredValues.every((name) => present.has(name))) return;
89
+ push(
90
+ Array.isArray(declared) ? [...schemaPath, 'required'] : schemaPath,
91
+ MESSAGES.requiredProperties(requiredValues.join(', ')),
92
+ );
93
+ }
94
+
95
+ function validateItems(entry, propertyName, itemsType) {
96
+ const { value, path } = entry;
97
+ if (!isType(typeOf(value), 'array')) return null;
98
+
99
+ const items = value.items;
100
+ if (items === undefined || items === null) {
101
+ push(path, MESSAGES.propertyItemsMissing(propertyName));
102
+ return null;
103
+ }
104
+ const itemsPath = [...path, 'items'];
105
+ if (!isType(typeOf(items), itemsType)) {
106
+ push(
107
+ typeOf(items) === undefined ? itemsPath : [...itemsPath, 'type'],
108
+ MESSAGES.propertyItemsWrongType(propertyName, itemsType),
109
+ );
110
+ return null;
111
+ }
112
+ return { value: items, path: itemsPath };
113
+ }
114
+
115
+ function validateObject(declaredSchema, entry) {
116
+ const declaredProperties = declaredSchema && typeof declaredSchema === 'object'
117
+ ? declaredSchema.properties
118
+ : null;
119
+ if (declaredProperties && typeof declaredProperties === 'object') {
120
+ Object.keys(declaredProperties).sort().forEach((child) => {
121
+ validateProperties(child, declaredProperties[child], entry);
122
+ });
123
+ }
124
+
125
+ const declaredRequired = declaredSchema && typeof declaredSchema === 'object'
126
+ ? declaredSchema.required
127
+ : null;
128
+ if (Array.isArray(declaredRequired) && declaredRequired.length > 0) {
129
+ const required = [...new Set(declaredRequired.map(String))].sort();
130
+ validateRequiredProperties(entry.value, entry.path, required);
131
+ }
132
+ }
133
+
134
+ function validateArray(declaredSchema, entry, propertyName) {
135
+ const declaredItems = declaredSchema && typeof declaredSchema === 'object' ? declaredSchema.items : null;
136
+ const itemsType = declaredTypeOf(declaredItems);
137
+
138
+ if (itemsType === 'object') {
139
+ const items = validateItems(entry, propertyName, itemsType);
140
+ if (items) validateObject(declaredItems, items);
141
+ return;
142
+ }
143
+ if (itemsType === 'array') {
144
+ const items = validateItems(entry, propertyName, itemsType);
145
+ if (items) validateArray(declaredItems, items, propertyName);
146
+ return;
147
+ }
148
+ validateItems(entry, propertyName, itemsType);
149
+ }
150
+
151
+ function validateProperties(propertyName, declaredSchema, containerEntry) {
152
+ const propertyMap = getAllProperties(containerEntry.value, containerEntry.path);
153
+ const schemaType = declaredTypeOf(declaredSchema);
154
+
155
+ if (schemaType === 'object') {
156
+ const entry = validateProperty(propertyMap, propertyName, 'object', containerEntry.path);
157
+ if (entry) validateObject(declaredSchema, entry);
158
+ return;
159
+ }
160
+ if (schemaType === 'array') {
161
+ const entry = validateProperty(propertyMap, propertyName, 'array', containerEntry.path);
162
+ if (entry) validateArray(declaredSchema, entry, propertyName);
163
+ return;
164
+ }
165
+ validateProperty(propertyMap, propertyName, schemaType, containerEntry.path);
166
+ }
167
+
168
+ function validateDataProperty(name, declaredSchema, properties, parentEntry) {
169
+ let type = declaredSchema && typeof declaredSchema === 'object' && typeof declaredSchema.type === 'string'
170
+ ? declaredSchema.type
171
+ : TYPE_ANY;
172
+ if (type === 'any') type = TYPE_ANY;
173
+
174
+ const entry = validateProperty(properties, name, type, parentEntry.path);
175
+ if (!entry) return;
176
+
177
+ const parentIsArray = isType(typeOf(parentEntry.value), 'array');
178
+ if (getAllProperties(entry.value, entry.path).size === 0 && !parentIsArray) {
179
+ push(entry.path, MESSAGES.requiredOneProperty());
180
+ }
181
+ }
182
+
183
+ function validateRootProperties(requiredNames, properties, parentEntry) {
184
+ if (!Array.isArray(requiredNames) || requiredNames.length === 0) return;
185
+ requiredNames.map(String).forEach((name) => {
186
+ const declaredSchema = config.properties && Object.prototype.hasOwnProperty.call(config.properties, name)
187
+ ? config.properties[name]
188
+ : null;
189
+ if (name === config.dataProperty) {
190
+ validateDataProperty(name, declaredSchema, properties, parentEntry);
191
+ } else {
192
+ validateProperties(name, declaredSchema, parentEntry);
193
+ }
194
+ });
195
+ }
196
+
197
+ function resolveRootNode(properties, schemaEntry) {
198
+ let rootProperty = config.rootProperty;
199
+ if (rootProperty === TYPE_ANY) {
200
+ const first = properties.keys().next();
201
+ if (first.done) return null;
202
+ rootProperty = first.value;
203
+ }
204
+
205
+ const rootEntry = validateProperty(properties, rootProperty, 'object', schemaEntry.path);
206
+ if (rootEntry && getAllProperties(rootEntry.value, rootEntry.path).size === 0) {
207
+ push(rootEntry.path, MESSAGES.requiredOneProperty());
208
+ }
209
+ return rootEntry;
210
+ }
211
+
212
+ function visitSchemaNode(schemaEntry, statusCode) {
213
+ let successCode = false;
214
+ let code = 0;
215
+ if (String(statusCode).toLowerCase() !== 'default') {
216
+ code = Number.parseInt(String(statusCode), 10);
217
+ if (Number.isNaN(code)) return;
218
+ successCode = code >= 200 && code < 300 && code !== 204;
219
+ }
220
+ if (code === 204) return;
221
+
222
+ let entry = schemaEntry;
223
+ let properties = getAllProperties(entry.value, entry.path);
224
+
225
+ if (config.rootProperty !== null) {
226
+ const rootEntry = resolveRootNode(properties, entry);
227
+ if (!rootEntry) return;
228
+ entry = rootEntry;
229
+ properties = getAllProperties(rootEntry.value, rootEntry.path);
230
+ }
231
+
232
+ validateRootProperties(successCode ? config.requiredOnSuccess : config.requiredOnError, properties, entry);
233
+ validateRootProperties(config.requiredAlways, properties, entry);
234
+ }
235
+
236
+ return { results, visitSchemaNode };
237
+ }
238
+
239
+ function parseConfig(options) {
240
+ const raw = options && options['response-schema'] !== undefined && options['response-schema'] !== null
241
+ ? options['response-schema']
242
+ : DEFAULT_RESPONSE_SCHEMA;
243
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
244
+ return {
245
+ requiredOnSuccess: Array.isArray(parsed.requiredOnSuccess) ? parsed.requiredOnSuccess : null,
246
+ requiredOnError: Array.isArray(parsed.requiredOnError) ? parsed.requiredOnError : null,
247
+ requiredAlways: Array.isArray(parsed.requiredAlways) ? parsed.requiredAlways : null,
248
+ properties: parsed.properties && typeof parsed.properties === 'object' ? parsed.properties : null,
249
+ dataProperty: typeof parsed.dataProperty === 'string' ? parsed.dataProperty : null,
250
+ rootProperty: typeof parsed.rootProperty === 'string' ? parsed.rootProperty : null,
251
+ };
252
+ }
253
+
254
+ /**
255
+ * @param {object} targetVal the Paths Object
256
+ * @param {object} options
257
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
258
+ */
259
+ module.exports = (targetVal, options, context) => {
260
+ if (!targetVal || typeof targetVal !== 'object') return [];
261
+
262
+ const ruleCode = context.rule.name.split(':').pop();
263
+
264
+ let config;
265
+ try {
266
+ config = parseConfig(options);
267
+ } catch (err) {
268
+ return [{ message: `${ruleCode}: Error parsing Standard Response Schemas`, path: context.path }];
269
+ }
270
+
271
+ const rawExclusions = options && options['path-exclusions'] !== undefined && options['path-exclusions'] !== null
272
+ ? String(options['path-exclusions'])
273
+ : DEFAULT_PATH_EXCLUSIONS;
274
+ const exclusions = new Set(rawExclusions.split(',').map((item) => item.trim()));
275
+
276
+ const validator = createValidator(config, ruleCode);
277
+
278
+ Object.keys(targetVal).forEach((pathName) => {
279
+ if (exclusions.has(pathName)) return;
280
+ const pathItem = targetVal[pathName];
281
+ if (!pathItem || typeof pathItem !== 'object') return;
282
+
283
+ Object.keys(pathItem).forEach((operationName) => {
284
+ if (!OPERATIONS.has(operationName.toLowerCase())) return;
285
+ const operation = pathItem[operationName];
286
+ if (!operation || typeof operation !== 'object') return;
287
+ const { responses } = operation;
288
+ if (!responses || typeof responses !== 'object') return;
289
+
290
+ Object.keys(responses).forEach((statusCode) => {
291
+ const response = responses[statusCode];
292
+ if (!response || typeof response !== 'object') return;
293
+ const responsePath = [...context.path, pathName, operationName, 'responses', statusCode];
294
+
295
+ if (response.content && typeof response.content === 'object') {
296
+ Object.keys(response.content).forEach((mediaType) => {
297
+ if (!mediaType.toLowerCase().includes('json')) return;
298
+ const mediaTypeNode = response.content[mediaType];
299
+ if (!mediaTypeNode || typeof mediaTypeNode !== 'object') return;
300
+ const { schema } = mediaTypeNode;
301
+ if (schema === undefined || schema === null) return;
302
+ validator.visitSchemaNode(
303
+ { value: schema, path: [...responsePath, 'content', mediaType, 'schema'] },
304
+ statusCode,
305
+ );
306
+ });
307
+ return;
308
+ }
309
+
310
+ const { schema } = response;
311
+ if (schema === undefined || schema === null) return;
312
+ validator.visitSchemaNode({ value: schema, path: [...responsePath, 'schema'] }, statusCode);
313
+ });
314
+ });
315
+ });
316
+
317
+ return validator.results;
318
+ };
@@ -0,0 +1,35 @@
1
+ const DEFAULT_INTEGRITY = 'minLength,maxLength,pattern,enum';
2
+
3
+ function isStringType(type) {
4
+ return type === 'string' || (Array.isArray(type) && type.indexOf('string') > -1);
5
+ }
6
+
7
+ /**
8
+ * @param {object} targetVal a Parameter Object
9
+ * @param {object} options
10
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
11
+ */
12
+ module.exports = (targetVal, options, context) => {
13
+ if (!targetVal || typeof targetVal !== 'object') return [];
14
+
15
+ const hasOwnType = Object.prototype.hasOwnProperty.call(targetVal, 'type');
16
+ const isSwaggerBody = targetVal.in === 'body';
17
+ const container = hasOwnType || isSwaggerBody ? targetVal : targetVal.schema;
18
+ const containerPath = hasOwnType || isSwaggerBody ? context.path : [...context.path, 'schema'];
19
+ if (!container || typeof container !== 'object') return [];
20
+
21
+ if (!isStringType(container.type)) return [];
22
+
23
+ const raw = options && options.parameter_integrity !== undefined && options.parameter_integrity !== null
24
+ ? String(options.parameter_integrity)
25
+ : DEFAULT_INTEGRITY;
26
+ const checks = raw.split(',').map((key) => key.trim());
27
+
28
+ const satisfied = checks.some((key) => {
29
+ if (!Object.prototype.hasOwnProperty.call(container, key)) return false;
30
+ return container[key] !== undefined && container[key] !== null;
31
+ });
32
+ if (satisfied) return [];
33
+
34
+ return [{ message: context.rule.message, path: [...containerPath, 'type'] }];
35
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-quality-spectral-ruleset",
3
- "version": "1.5.0-beta.1",
3
+ "version": "1.5.0-beta.3",
4
4
  "description": "Spectral ruleset by API Quality",
5
5
  "main": "apq-spectral.yaml",
6
6
  "files": [