api-quality-spectral-ruleset 1.5.0-beta.2 → 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 +33 -102
- package/functions/apq-example-schema-types.js +116 -0
- package/functions/apq-numeric-invalid-format.js +64 -0
- package/functions/apq-numeric-missing-format.js +58 -0
- package/functions/apq-numeric-well-defined-format.js +64 -0
- package/functions/apq-required-fields-exist.js +30 -5
- package/functions/apq-schema-format.js +17 -0
- package/functions/apq-standard-response-schema.js +318 -0
- package/functions/apq-string-parameter-integrity.js +35 -0
- package/package.json +1 -1
package/apq-spectral.yaml
CHANGED
|
@@ -34,6 +34,12 @@ functions:
|
|
|
34
34
|
- apq-query-params-optional
|
|
35
35
|
- apq-wso2-scopes-valid
|
|
36
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
|
|
37
43
|
extends:
|
|
38
44
|
- spectral:asyncapi
|
|
39
45
|
- spectral:oas
|
|
@@ -196,29 +202,10 @@ rules:
|
|
|
196
202
|
message: "OAR016: Numeric types must use a valid format for their type."
|
|
197
203
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR016.md"
|
|
198
204
|
severity: error
|
|
199
|
-
|
|
205
|
+
resolved: false
|
|
206
|
+
given: "$..[?(@ && @.type)]"
|
|
200
207
|
then:
|
|
201
|
-
function:
|
|
202
|
-
functionOptions:
|
|
203
|
-
schema:
|
|
204
|
-
required: [format]
|
|
205
|
-
allOf:
|
|
206
|
-
- if:
|
|
207
|
-
properties:
|
|
208
|
-
type:
|
|
209
|
-
const: integer
|
|
210
|
-
then:
|
|
211
|
-
properties:
|
|
212
|
-
format:
|
|
213
|
-
enum: [int32, int64, uint32, uint64]
|
|
214
|
-
- if:
|
|
215
|
-
properties:
|
|
216
|
-
type:
|
|
217
|
-
const: number
|
|
218
|
-
then:
|
|
219
|
-
properties:
|
|
220
|
-
format:
|
|
221
|
-
enum: [float, double, decimal]
|
|
208
|
+
function: apq-numeric-invalid-format
|
|
222
209
|
apiq:OAR017:
|
|
223
210
|
description: "Resource path should alternate static and parametrized parts."
|
|
224
211
|
message: "OAR017: Resource path should alternate static and parametrized parts"
|
|
@@ -376,38 +363,14 @@ rules:
|
|
|
376
363
|
function: apq-has-filter-query-param
|
|
377
364
|
apiq:OAR029:
|
|
378
365
|
description: "A response not compliant with the standard may cause application issues."
|
|
379
|
-
message: "
|
|
366
|
+
message: "{{error}}"
|
|
380
367
|
severity: error
|
|
381
|
-
given: "$.
|
|
368
|
+
given: "$.paths"
|
|
382
369
|
then:
|
|
383
|
-
function: schema
|
|
370
|
+
function: apq-standard-response-schema
|
|
384
371
|
functionOptions:
|
|
385
|
-
schema:
|
|
386
|
-
|
|
387
|
-
properties:
|
|
388
|
-
properties:
|
|
389
|
-
type: object
|
|
390
|
-
properties:
|
|
391
|
-
error:
|
|
392
|
-
type: object
|
|
393
|
-
properties:
|
|
394
|
-
properties:
|
|
395
|
-
type: object
|
|
396
|
-
properties:
|
|
397
|
-
code:
|
|
398
|
-
type: string
|
|
399
|
-
message:
|
|
400
|
-
type: string
|
|
401
|
-
details:
|
|
402
|
-
type: array
|
|
403
|
-
httpStatus:
|
|
404
|
-
type: integer
|
|
405
|
-
required:
|
|
406
|
-
- code
|
|
407
|
-
- message
|
|
408
|
-
- httpStatus
|
|
409
|
-
required:
|
|
410
|
-
- 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"
|
|
411
374
|
apiq:OAR030:
|
|
412
375
|
description: "The configured status endpoint must be declared with the configured HTTP method."
|
|
413
376
|
message: "OAR030: The required status endpoint is not declared or does not have the required method."
|
|
@@ -688,10 +651,10 @@ rules:
|
|
|
688
651
|
message: "OAR052: Numeric types requires a format"
|
|
689
652
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR052.md"
|
|
690
653
|
severity: warn
|
|
691
|
-
|
|
654
|
+
resolved: false
|
|
655
|
+
given: "$..[?(@ && @.type)]"
|
|
692
656
|
then:
|
|
693
|
-
|
|
694
|
-
function: truthy
|
|
657
|
+
function: apq-numeric-missing-format
|
|
695
658
|
apiq:OAR053:
|
|
696
659
|
description: "Response headers for API observability and tracing must be defined (excluding 204 responses and health endpoints)."
|
|
697
660
|
message: "OAR053: Response must include mandatory headers and exclude forbidden headers."
|
|
@@ -889,47 +852,25 @@ rules:
|
|
|
889
852
|
description: "String parameters should have minLength, maxLength, pattern (regular expression), or enum restriction."
|
|
890
853
|
message: "OAR075: String parameters should have minLength, maxLength, pattern, or enum restriction."
|
|
891
854
|
severity: error
|
|
892
|
-
|
|
855
|
+
resolved: false
|
|
856
|
+
given:
|
|
857
|
+
- "$.paths[*][*].parameters[*]"
|
|
858
|
+
- "$.paths[*].parameters[*]"
|
|
859
|
+
- "$.components.parameters[*]"
|
|
860
|
+
- "$.parameters[*]"
|
|
893
861
|
then:
|
|
894
|
-
function:
|
|
862
|
+
function: apq-string-parameter-integrity
|
|
895
863
|
functionOptions:
|
|
896
|
-
|
|
897
|
-
type: object
|
|
898
|
-
properties:
|
|
899
|
-
schema:
|
|
900
|
-
anyOf:
|
|
901
|
-
- required: ["minLength"]
|
|
902
|
-
- required: ["maxLength"]
|
|
903
|
-
- required: ["pattern"]
|
|
904
|
-
- required: ["enum"]
|
|
864
|
+
parameter_integrity: "minLength,maxLength,pattern,enum"
|
|
905
865
|
apiq:OAR076:
|
|
906
866
|
description: "Schema should use well-defined type and format."
|
|
907
867
|
message: "OAR076: Schema should use well-defined type and format."
|
|
908
868
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR076.md"
|
|
909
869
|
severity: error
|
|
910
|
-
|
|
870
|
+
resolved: false
|
|
871
|
+
given: "$..[?(@ && @.type)]"
|
|
911
872
|
then:
|
|
912
|
-
function:
|
|
913
|
-
functionOptions:
|
|
914
|
-
schema:
|
|
915
|
-
required: [format]
|
|
916
|
-
allOf:
|
|
917
|
-
- if:
|
|
918
|
-
properties:
|
|
919
|
-
type:
|
|
920
|
-
const: integer
|
|
921
|
-
then:
|
|
922
|
-
properties:
|
|
923
|
-
format:
|
|
924
|
-
enum: [int32, int64]
|
|
925
|
-
- if:
|
|
926
|
-
properties:
|
|
927
|
-
type:
|
|
928
|
-
const: number
|
|
929
|
-
then:
|
|
930
|
-
properties:
|
|
931
|
-
format:
|
|
932
|
-
enum: [float, double]
|
|
873
|
+
function: apq-numeric-well-defined-format
|
|
933
874
|
apiq:OAR077:
|
|
934
875
|
description: "All parameters in query must be snake_case."
|
|
935
876
|
message: "OAR077: All parameters in query must be snake_case."
|
|
@@ -1199,19 +1140,10 @@ rules:
|
|
|
1199
1140
|
description: "The schemas should match the provided examples."
|
|
1200
1141
|
message: "OAR108: Schema does not match the provided example."
|
|
1201
1142
|
severity: "error"
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
- "$.paths['/item'].get.responses['200'].examples['application/json']"
|
|
1143
|
+
resolved: false
|
|
1144
|
+
given: "$.paths[*][*].responses[*]"
|
|
1205
1145
|
then:
|
|
1206
|
-
|
|
1207
|
-
functionOptions:
|
|
1208
|
-
schema:
|
|
1209
|
-
type: "object"
|
|
1210
|
-
properties:
|
|
1211
|
-
id:
|
|
1212
|
-
type: "integer"
|
|
1213
|
-
nombre:
|
|
1214
|
-
type: "string"
|
|
1146
|
+
function: apq-example-schema-types
|
|
1215
1147
|
apiq:OAR109:
|
|
1216
1148
|
description: "Use default response instead of directly specifying 5XX codes."
|
|
1217
1149
|
message: "OAR109: Use default response instead of specifying 5XX codes directly."
|
|
@@ -1265,9 +1197,8 @@ rules:
|
|
|
1265
1197
|
message: "OAR115: All fields in the required array must be defined in schema properties."
|
|
1266
1198
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR115.md"
|
|
1267
1199
|
severity: warn
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
- "$.definitions[*]"
|
|
1200
|
+
resolved: false
|
|
1201
|
+
given: "$..[?(@ && @.required)]"
|
|
1271
1202
|
then:
|
|
1272
1203
|
function: apq-required-fields-exist
|
|
1273
1204
|
apiq:OAR116:
|
|
@@ -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
|
+
};
|
|
@@ -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
|
-
|
|
11
|
-
|
|
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
|
+
};
|