api-quality-spectral-ruleset 1.0.0 → 1.1.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 +125 -49
- package/functions/apq-check-examples-coverage.js +54 -0
- package/functions/apq-custom-field.js +70 -0
- package/functions/apq-require-response-on-path-params.js +62 -0
- package/functions/apq-required-fields-exist.js +27 -0
- package/functions/apq-response-headers.js +94 -0
- package/functions/apq-security-check.js +35 -0
- package/functions/apq-standard-response-codes.js +17 -12
- package/package.json +1 -1
package/apq-spectral.yaml
CHANGED
|
@@ -7,6 +7,12 @@ functions:
|
|
|
7
7
|
- apq-resources-by-verb
|
|
8
8
|
- apq-at-most-one-body-parameter
|
|
9
9
|
- apq-standard-response-codes
|
|
10
|
+
- apq-response-headers
|
|
11
|
+
- apq-require-response-on-path-params
|
|
12
|
+
- apq-required-fields-exist
|
|
13
|
+
- apq-security-check
|
|
14
|
+
- apq-custom-field
|
|
15
|
+
- apq-check-examples-coverage
|
|
10
16
|
extends:
|
|
11
17
|
- spectral:asyncapi
|
|
12
18
|
- spectral:oas
|
|
@@ -111,12 +117,15 @@ rules:
|
|
|
111
117
|
apiq:OAR011:
|
|
112
118
|
description: "URLs should follow the specified naming convention: all literal path segments must be in kebab-case."
|
|
113
119
|
message: "OAR011: The resource names with more than two words must be compliant with the standard naming convention (kebab-case)."
|
|
120
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR011.md"
|
|
114
121
|
severity: error
|
|
115
|
-
given:
|
|
122
|
+
given:
|
|
123
|
+
- "$.paths[*]~"
|
|
124
|
+
- "$.servers[*].url"
|
|
116
125
|
then:
|
|
117
126
|
function: pattern
|
|
118
127
|
functionOptions:
|
|
119
|
-
match: "^(
|
|
128
|
+
match: "^(https?:\\/\\/[^\\/]+)?(\\/(?:[a-z0-9]+(?:-[a-z0-9]+)*|\\{[^}]+\\}))*\\/?$"
|
|
120
129
|
apiq:OAR012:
|
|
121
130
|
description: "Path params, query params, object names and property names should follow the configured naming convention. You can configure snake_case (default), kebab-case, camelCase or UpperCamelCase"
|
|
122
131
|
message: "OAR012: Path params, query params, object names and property names must follow the configured naming convention."
|
|
@@ -163,24 +172,29 @@ rules:
|
|
|
163
172
|
message: "OAR016: Numeric types must use a valid format for their type."
|
|
164
173
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR016.md"
|
|
165
174
|
severity: error
|
|
166
|
-
given:
|
|
167
|
-
- "$..[?(@.type == 'integer' && @.format)]"
|
|
168
|
-
- "$..[?(@.type == 'number' && @.format)]"
|
|
175
|
+
given: "$..[?(@.type=='number' || @.type=='integer')]"
|
|
169
176
|
then:
|
|
170
177
|
function: schema
|
|
171
178
|
functionOptions:
|
|
172
179
|
schema:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
required: [format]
|
|
181
|
+
allOf:
|
|
182
|
+
- if:
|
|
183
|
+
properties:
|
|
184
|
+
type:
|
|
185
|
+
const: integer
|
|
186
|
+
then:
|
|
187
|
+
properties:
|
|
188
|
+
format:
|
|
189
|
+
enum: [int32, int64]
|
|
190
|
+
- if:
|
|
191
|
+
properties:
|
|
192
|
+
type:
|
|
193
|
+
const: number
|
|
194
|
+
then:
|
|
195
|
+
properties:
|
|
196
|
+
format:
|
|
197
|
+
enum: [float, double]
|
|
184
198
|
apiq:OAR017:
|
|
185
199
|
description: "Resource path should alternate static and parametrized parts."
|
|
186
200
|
message: "OAR017: Resource path should alternate static and parametrized parts."
|
|
@@ -353,14 +367,14 @@ rules:
|
|
|
353
367
|
function: truthy
|
|
354
368
|
apiq:OAR031:
|
|
355
369
|
description: The examples can help developers to understand the response data structure and representation."
|
|
356
|
-
message: "OAR031: Examples -
|
|
370
|
+
message: "OAR031: Examples - Parameters, Request Body and Responses must have an examples defined."
|
|
357
371
|
severity: error
|
|
358
372
|
given:
|
|
359
373
|
- $.paths.*.*.parameters.*
|
|
360
|
-
- $.paths
|
|
374
|
+
- $.paths.*.*.requestBody
|
|
375
|
+
- $.paths.*.*.responses[?(@property !== "204")]
|
|
361
376
|
then:
|
|
362
|
-
|
|
363
|
-
function: truthy
|
|
377
|
+
function: apq-check-examples-coverage
|
|
364
378
|
apiq:OAR032:
|
|
365
379
|
description: "Ambiguous path parts not encouraged."
|
|
366
380
|
message: "OAR032: When reading a URL, it must be possible to interpret which resources it is referring to in a concrete way. Ambiguous names in resources must be avoided."
|
|
@@ -481,6 +495,17 @@ rules:
|
|
|
481
495
|
then:
|
|
482
496
|
field: "x-aut-type"
|
|
483
497
|
function: truthy
|
|
498
|
+
apiq:OAR042:
|
|
499
|
+
description: "Base path must be compliant with the standard."
|
|
500
|
+
message: "OAR042: Base path must follow the '/api-<name>/v<version>' standard."
|
|
501
|
+
severity: "error"
|
|
502
|
+
given:
|
|
503
|
+
- "$.basePath"
|
|
504
|
+
- "$.servers[*].url"
|
|
505
|
+
then:
|
|
506
|
+
function: pattern
|
|
507
|
+
functionOptions:
|
|
508
|
+
match: "^(/api-[^/]+/v[0-9]+|https?:\\/\\/[^/]+\\/api-[^/]+\\/v[0-9]+)$"
|
|
484
509
|
apiq:OAR044:
|
|
485
510
|
description: "Declared media type should conform to RFC6838 and RFC7231."
|
|
486
511
|
message: "OAR044: Declared media type range should conform to RFC7231."
|
|
@@ -590,6 +615,20 @@ rules:
|
|
|
590
615
|
then:
|
|
591
616
|
field: format
|
|
592
617
|
function: truthy
|
|
618
|
+
apiq:OAR053:
|
|
619
|
+
description: "There are mandatory response headers and others that are not allowed."
|
|
620
|
+
message: "{{error}}"
|
|
621
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR053.md"
|
|
622
|
+
severity: error
|
|
623
|
+
given: "$.paths[*][*].responses[*]"
|
|
624
|
+
then:
|
|
625
|
+
- function: apq-response-headers
|
|
626
|
+
functionOptions:
|
|
627
|
+
mandatory-headers: "x-trace-id,x-request-id"
|
|
628
|
+
allowed-headers: "idcorrelacion,x-correlacionid,x-global-trasaction-id,x-power-by,x-trace-id,x-request-id"
|
|
629
|
+
included-response-codes: "*"
|
|
630
|
+
excluded-response-codes: "204"
|
|
631
|
+
path-exclusions: "/status,/prueba"
|
|
593
632
|
apiq:OAR054:
|
|
594
633
|
description: "Ensure the host matches the specified format"
|
|
595
634
|
message: "OAR054: Hostname must be a subdomain of the organization's domain name."
|
|
@@ -790,16 +829,32 @@ rules:
|
|
|
790
829
|
- required: ["enum"]
|
|
791
830
|
apiq:OAR076:
|
|
792
831
|
description: "Schema should use well-defined type and format."
|
|
793
|
-
message: "OAR076:
|
|
794
|
-
|
|
795
|
-
|
|
832
|
+
message: "OAR076: Schema should use well-defined type and format."
|
|
833
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR076.md"
|
|
834
|
+
severity: error
|
|
835
|
+
given: "$..[?(@.type=='number' || @.type=='integer')]"
|
|
796
836
|
then:
|
|
797
|
-
|
|
798
|
-
function: enumeration
|
|
837
|
+
function: schema
|
|
799
838
|
functionOptions:
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
839
|
+
schema:
|
|
840
|
+
required: [format]
|
|
841
|
+
allOf:
|
|
842
|
+
- if:
|
|
843
|
+
properties:
|
|
844
|
+
type:
|
|
845
|
+
const: integer
|
|
846
|
+
then:
|
|
847
|
+
properties:
|
|
848
|
+
format:
|
|
849
|
+
enum: [int32, int64]
|
|
850
|
+
- if:
|
|
851
|
+
properties:
|
|
852
|
+
type:
|
|
853
|
+
const: number
|
|
854
|
+
then:
|
|
855
|
+
properties:
|
|
856
|
+
format:
|
|
857
|
+
enum: [float, double]
|
|
803
858
|
apiq:OAR077:
|
|
804
859
|
description: "All parameters in query must be snake_case."
|
|
805
860
|
message: "OAR077: All parameters in query must be snake_case."
|
|
@@ -812,20 +867,22 @@ rules:
|
|
|
812
867
|
match: ^\$?_?[a-z]+(_[a-z]+)*$
|
|
813
868
|
apiq:OAR078:
|
|
814
869
|
description: "All API methods must have security defined."
|
|
815
|
-
message: "
|
|
870
|
+
message: "{{error}}"
|
|
871
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR078.md"
|
|
816
872
|
severity: error
|
|
817
|
-
given: "
|
|
873
|
+
given: "$"
|
|
818
874
|
then:
|
|
819
|
-
|
|
820
|
-
function: truthy
|
|
875
|
+
function: apq-security-check
|
|
821
876
|
apiq:OAR079:
|
|
822
877
|
description: "Operations with path parameters should include a 404 Not Found response."
|
|
823
878
|
message: "OAR079: Path parameter present, but missing 404 Not Found response."
|
|
879
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR079.md"
|
|
824
880
|
severity: "warn"
|
|
825
|
-
given: "$.paths[*][
|
|
881
|
+
given: "$.paths[*][get,post,put,patch,delete]"
|
|
826
882
|
then:
|
|
827
|
-
|
|
828
|
-
|
|
883
|
+
function: apq-require-response-on-path-params
|
|
884
|
+
functionOptions:
|
|
885
|
+
response: "404"
|
|
829
886
|
apiq:OAR080:
|
|
830
887
|
description: "The security scheme must be among those allowed by the organization and must be complete."
|
|
831
888
|
message: "OAR080: The security scheme '{{property}}' must be among those allowed by the organization and must be complete."
|
|
@@ -934,9 +991,12 @@ rules:
|
|
|
934
991
|
apiq:OAR091:
|
|
935
992
|
description: "Parameters must contain only $ref references."
|
|
936
993
|
message: "OAR091: Parameters must contain only $ref references."
|
|
994
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR091.md"
|
|
937
995
|
severity: error
|
|
938
|
-
|
|
996
|
+
resolved: false
|
|
997
|
+
given: "$.paths[*][get,post,put,patch,delete].parameters[*]"
|
|
939
998
|
then:
|
|
999
|
+
field: $ref
|
|
940
1000
|
function: truthy
|
|
941
1001
|
apiq:OAR092:
|
|
942
1002
|
description: "RequestBody must contain a $ref."
|
|
@@ -1101,19 +1161,35 @@ rules:
|
|
|
1101
1161
|
then:
|
|
1102
1162
|
field: "contact"
|
|
1103
1163
|
function: truthy
|
|
1164
|
+
apiq:OAR113:
|
|
1165
|
+
description: "Field or extension must be at the assigned location"
|
|
1166
|
+
message: "{{error}}"
|
|
1167
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR113.md"
|
|
1168
|
+
severity: warn
|
|
1169
|
+
given: "$"
|
|
1170
|
+
then:
|
|
1171
|
+
function: apq-custom-field
|
|
1172
|
+
functionOptions:
|
|
1173
|
+
fieldName: "x-custom-example"
|
|
1174
|
+
fieldLocation: "path,operation_get,response_200"
|
|
1104
1175
|
apiq:OAR114:
|
|
1105
|
-
description: There are mandatory response headers and others that are not allowed.
|
|
1106
|
-
message: "
|
|
1107
|
-
documentationUrl: https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR114.md
|
|
1108
|
-
severity: error
|
|
1109
|
-
given: "$.paths[*][*].responses[*]
|
|
1176
|
+
description: "There are mandatory response headers and others that are not allowed."
|
|
1177
|
+
message: "{{error}}"
|
|
1178
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR114.md"
|
|
1179
|
+
severity: "error"
|
|
1180
|
+
given: "$.paths[*][*].responses[*]"
|
|
1110
1181
|
then:
|
|
1111
|
-
- function:
|
|
1182
|
+
- function: apq-response-headers
|
|
1112
1183
|
functionOptions:
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1184
|
+
mandatory-headers: "x-api-key,x-trace-id"
|
|
1185
|
+
allowed-headers: "x-api-key,x-trace-id,traceId,dateTime"
|
|
1186
|
+
apiq:OAR115:
|
|
1187
|
+
description: "All fields listed in the required array must be defined in the schema properties."
|
|
1188
|
+
message: "{{error}}"
|
|
1189
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR115.md"
|
|
1190
|
+
severity: warn
|
|
1191
|
+
given:
|
|
1192
|
+
- "$.components.schemas[*]"
|
|
1193
|
+
- "$.definitions[*]"
|
|
1194
|
+
then:
|
|
1195
|
+
function: apq-required-fields-exist
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} given
|
|
3
|
+
* @param {object} options
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = (given, options, context) => {
|
|
7
|
+
if (!given || typeof given !== 'object') return [];
|
|
8
|
+
|
|
9
|
+
const hasExample = (node) => {
|
|
10
|
+
if (!node || typeof node !== 'object') return false;
|
|
11
|
+
|
|
12
|
+
if (node.example !== undefined || node.examples !== undefined) return true;
|
|
13
|
+
|
|
14
|
+
if (node.content && typeof node.content === 'object') {
|
|
15
|
+
return Object.values(node.content).some(mediaType => hasExample(mediaType));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (node.schema) {
|
|
19
|
+
return checkSchema(node.schema);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (node.properties || node.items || node.type) {
|
|
23
|
+
return checkSchema(node);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return false;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const checkSchema = (schema) => {
|
|
30
|
+
if (!schema || typeof schema !== 'object') return false;
|
|
31
|
+
|
|
32
|
+
if (schema.example !== undefined || schema.examples !== undefined) return true;
|
|
33
|
+
|
|
34
|
+
if (schema.properties && typeof schema.properties === 'object') {
|
|
35
|
+
return Object.values(schema.properties).some(prop => checkSchema(prop));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (schema.items) {
|
|
39
|
+
return checkSchema(schema.items);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (!hasExample(given)) {
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
message: context.rule.message
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [];
|
|
54
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} given
|
|
3
|
+
* @param {object} options
|
|
4
|
+
* @param {string} options.fieldName
|
|
5
|
+
* @param {string} options.fieldLocation
|
|
6
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
7
|
+
*/
|
|
8
|
+
module.exports = (given, options, context) => {
|
|
9
|
+
const { fieldName, fieldLocation } = options || {};
|
|
10
|
+
const ruleCode = context.rule.name.split(':').pop();
|
|
11
|
+
|
|
12
|
+
if (!given || !fieldName || !fieldLocation) return [];
|
|
13
|
+
|
|
14
|
+
const locations = fieldLocation.split(',').map(l => l.trim());
|
|
15
|
+
const results = [];
|
|
16
|
+
|
|
17
|
+
const hasField = node =>
|
|
18
|
+
node && typeof node === 'object' && Object.prototype.hasOwnProperty.call(node, fieldName);
|
|
19
|
+
|
|
20
|
+
const addError = path =>
|
|
21
|
+
results.push({
|
|
22
|
+
message: `${ruleCode}: Field or extension ${fieldName} must be at the assigned location`,
|
|
23
|
+
path,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (hasField(given)) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rootLocations = ['path', 'components'];
|
|
31
|
+
|
|
32
|
+
const missingRoot = rootLocations.find(
|
|
33
|
+
loc => locations.includes(loc) && !given[loc === 'path' ? 'paths' : loc]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (missingRoot) {
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
message: `${ruleCode}: The location ${missingRoot} is not present.`,
|
|
40
|
+
path: [],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const opLoc = locations.find(l => l.startsWith('operation_'));
|
|
46
|
+
const respLoc = locations.find(l => l.startsWith('response_'));
|
|
47
|
+
|
|
48
|
+
const verb = opLoc?.split('_')[1];
|
|
49
|
+
const status = respLoc?.split('_')[1];
|
|
50
|
+
|
|
51
|
+
Object.entries(given.paths || {}).forEach(([pathKey, pathItem]) => {
|
|
52
|
+
if (hasField(pathItem)) return;
|
|
53
|
+
|
|
54
|
+
if (!verb || !pathItem[verb]) return;
|
|
55
|
+
|
|
56
|
+
const operation = pathItem[verb];
|
|
57
|
+
|
|
58
|
+
if (hasField(operation)) return;
|
|
59
|
+
|
|
60
|
+
if (!operation.responses || !operation.responses[status]) return;
|
|
61
|
+
|
|
62
|
+
const response = operation.responses[status];
|
|
63
|
+
|
|
64
|
+
if (!hasField(response)) {
|
|
65
|
+
addError(['paths', pathKey, verb, 'responses', status]);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return results;
|
|
70
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requires one or more HTTP response codes when path parameters are present.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} given
|
|
5
|
+
* @param {object} options
|
|
6
|
+
* @param {string} options.response Comma-separated HTTP status codes (e.g. "400,404,200")
|
|
7
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
8
|
+
*/
|
|
9
|
+
module.exports = (given, options, context) => {
|
|
10
|
+
if (!given || typeof given !== 'object') {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ruleCode = context.rule?.name?.split(':').pop();
|
|
15
|
+
const responseOpt = options?.response;
|
|
16
|
+
|
|
17
|
+
if (typeof responseOpt !== 'string') {
|
|
18
|
+
throw new TypeError(`${ruleCode}: "response" option must be a comma-separated string of HTTP status codes`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const requiredResponses = responseOpt
|
|
22
|
+
.split(',')
|
|
23
|
+
.map(r => r.trim())
|
|
24
|
+
.filter(r => /^\d{3}$/.test(r));
|
|
25
|
+
|
|
26
|
+
if (requiredResponses.length === 0) {
|
|
27
|
+
throw new Error(`${ruleCode}: No valid HTTP status codes found in "response" option`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pathItemKeyIndex = context.path?.length - 2;
|
|
31
|
+
const pathItem = typeof pathItemKeyIndex === 'number'
|
|
32
|
+
? context.path?.[pathItemKeyIndex]
|
|
33
|
+
: null;
|
|
34
|
+
|
|
35
|
+
const pathParams = (pathItem?.parameters || []).filter(
|
|
36
|
+
p => p?.in === 'path'
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const operationParams = (given.parameters || []).filter(
|
|
40
|
+
p => p?.in === 'path'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (pathParams.length === 0 && operationParams.length === 0) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const responses = given?.responses || {};
|
|
48
|
+
|
|
49
|
+
const hasRequiredResponse = requiredResponses.some(
|
|
50
|
+
code => responses[code]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (!hasRequiredResponse) {
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
message: context.rule.message,
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [];
|
|
62
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} given
|
|
3
|
+
* @param {object} options
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = (given, options, context) => {
|
|
7
|
+
const results = [];
|
|
8
|
+
|
|
9
|
+
if (!given || given.type !== 'object') return results;
|
|
10
|
+
|
|
11
|
+
const { required, properties } = given;
|
|
12
|
+
if (!Array.isArray(required)) return results;
|
|
13
|
+
|
|
14
|
+
const propertyNames = new Set(Object.keys(properties || {}));
|
|
15
|
+
const ruleCode = context.rule.name.split(':').pop();
|
|
16
|
+
|
|
17
|
+
required.forEach((field, index) => {
|
|
18
|
+
if (!propertyNames.has(field)) {
|
|
19
|
+
results.push({
|
|
20
|
+
message: `${ruleCode}: This value does not exist, '${field}' must be defined in the schema properties.`,
|
|
21
|
+
path: [...context.path, 'required', index]
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return results;
|
|
27
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module.exports = (responseNode, options = {}, context) => {
|
|
2
|
+
if (!responseNode || typeof responseNode !== 'object') {
|
|
3
|
+
return [];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const results = [];
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
'mandatory-headers': mandatoryHeadersOpt = '',
|
|
10
|
+
'allowed-headers': allowedHeadersOpt = '',
|
|
11
|
+
'included-response-codes': includedCodesOpt = '*',
|
|
12
|
+
'excluded-response-codes': excludedCodesOpt = '',
|
|
13
|
+
'path-exclusions': pathExclusionsOpt = '',
|
|
14
|
+
} = options;
|
|
15
|
+
|
|
16
|
+
const mandatoryHeaders = mandatoryHeadersOpt
|
|
17
|
+
.split(',')
|
|
18
|
+
.map(h => h.toLowerCase().trim())
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
|
|
21
|
+
const allowedHeaders = allowedHeadersOpt
|
|
22
|
+
.split(',')
|
|
23
|
+
.map(h => h.toLowerCase().trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
|
|
26
|
+
const includedCodes = includedCodesOpt
|
|
27
|
+
.split(',')
|
|
28
|
+
.map(c => c.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
|
|
31
|
+
const excludedCodes = excludedCodesOpt
|
|
32
|
+
.split(',')
|
|
33
|
+
.map(c => c.trim())
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
|
|
36
|
+
const pathExclusions = pathExclusionsOpt
|
|
37
|
+
.split(',')
|
|
38
|
+
.map(p => p.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
|
|
41
|
+
const path = context.path?.find(p => typeof p === 'string' && p.startsWith('/'));
|
|
42
|
+
const responseCode = context.path?.[context.path.length - 1];
|
|
43
|
+
|
|
44
|
+
if (path && pathExclusions.includes(path)) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (excludedCodes.includes(String(responseCode))) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!includedCodes.includes('*') && !includedCodes.includes(String(responseCode))) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let headers = responseNode.headers;
|
|
57
|
+
|
|
58
|
+
if (!headers && responseNode.$ref && context.document?.resolved) {
|
|
59
|
+
try {
|
|
60
|
+
const ref = responseNode.$ref.replace(/^#\//, '').split('/');
|
|
61
|
+
let resolved = context.document.resolved;
|
|
62
|
+
for (const part of ref) {
|
|
63
|
+
resolved = resolved?.[part];
|
|
64
|
+
}
|
|
65
|
+
headers = resolved?.headers;
|
|
66
|
+
} catch (_) {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const headerNames = headers && typeof headers === 'object'
|
|
71
|
+
? Object.keys(headers).map(h => h.toLowerCase().trim())
|
|
72
|
+
: [];
|
|
73
|
+
const ruleCode = context.rule.name.split(':').pop();
|
|
74
|
+
|
|
75
|
+
if (mandatoryHeaders.length > 0) {
|
|
76
|
+
const missing = mandatoryHeaders.filter(h => !headerNames.includes(h));
|
|
77
|
+
if (missing.length > 0) {
|
|
78
|
+
results.push({
|
|
79
|
+
message: `${ruleCode}: Headers [${missing.join(', ')}] are required`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (allowedHeaders.length > 0 && headerNames.length > 0) {
|
|
85
|
+
const forbidden = headerNames.filter(h => !allowedHeaders.includes(h));
|
|
86
|
+
if (forbidden.length > 0) {
|
|
87
|
+
results.push({
|
|
88
|
+
message: `${ruleCode}: Header not allowed`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return results;
|
|
94
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} given
|
|
3
|
+
* @param {object} options
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = (given, options, context) => {
|
|
7
|
+
if (!given) return [];
|
|
8
|
+
|
|
9
|
+
const results = [];
|
|
10
|
+
const ruleCode = context.rule.name.split(':').pop();
|
|
11
|
+
|
|
12
|
+
const globalSecurity = given.security ?? given.swagger?.security;
|
|
13
|
+
if (globalSecurity) {
|
|
14
|
+
return results;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const methods = ['get', 'post', 'put', 'patch', 'delete'];
|
|
18
|
+
|
|
19
|
+
const pathsObj = given.paths || {};
|
|
20
|
+
for (const [pathName, pathItem] of Object.entries(pathsObj)) {
|
|
21
|
+
for (const method of methods) {
|
|
22
|
+
const operation = pathItem[method];
|
|
23
|
+
if (!operation) continue;
|
|
24
|
+
|
|
25
|
+
if (!operation.security || operation.security.length === 0) {
|
|
26
|
+
results.push({
|
|
27
|
+
message: `${ruleCode}: The operation '${method} ${pathName}' must have security defined.`,
|
|
28
|
+
path: ['paths', pathName, method, 'security']
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return results;
|
|
35
|
+
};
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} given
|
|
3
|
+
* @param {object} options
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = function apqStandardResponseCodes(given, options, context) {
|
|
2
7
|
const results = [];
|
|
3
8
|
|
|
4
|
-
if (!
|
|
9
|
+
if (!given || !context?.path) return results;
|
|
5
10
|
|
|
6
|
-
const responses =
|
|
11
|
+
const responses = given.responses;
|
|
7
12
|
if (!responses || typeof responses !== 'object') return results;
|
|
8
13
|
|
|
9
14
|
const definedCodes = Object.keys(responses);
|
|
@@ -17,15 +22,15 @@ module.exports = function apqStandardResponseCodes(targetVal, options, context)
|
|
|
17
22
|
|
|
18
23
|
const exclusions = options?.['resources-exclusions'] || [];
|
|
19
24
|
if (exclusions.some(ex => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
const [exVerb, exPath] = ex.split(':');
|
|
26
|
+
return exVerb.toLowerCase() === verb.toLowerCase() && new RegExp(`^${exPath}$`).test(resourcePath);
|
|
27
|
+
})) return results;
|
|
23
28
|
|
|
24
29
|
const rulesConfig = options?.['required-codes-by-resources-paths'];
|
|
25
30
|
if (!rulesConfig) return results;
|
|
26
31
|
|
|
27
32
|
const rules = rulesConfig
|
|
28
|
-
.split(/[\n;]/)
|
|
33
|
+
.split(/[\n;]/)
|
|
29
34
|
.map(r => r.trim())
|
|
30
35
|
.filter(Boolean)
|
|
31
36
|
.map(rule => {
|
|
@@ -49,19 +54,19 @@ module.exports = function apqStandardResponseCodes(targetVal, options, context)
|
|
|
49
54
|
|
|
50
55
|
if (!matchedRule) return results;
|
|
51
56
|
|
|
52
|
-
matchedRule.requiredCodes.forEach(
|
|
57
|
+
matchedRule.requiredCodes.forEach(respCode => {
|
|
53
58
|
let missing = false;
|
|
54
59
|
let msg = "";
|
|
55
60
|
|
|
56
|
-
if (
|
|
57
|
-
const alternatives =
|
|
61
|
+
if (respCode.includes('|')) {
|
|
62
|
+
const alternatives = respCode.split('|');
|
|
58
63
|
if (!alternatives.some(c => definedCodes.includes(c))) {
|
|
59
64
|
missing = true;
|
|
60
65
|
msg = `OAR039: Response code ${alternatives.join(' or ')} must be defined.`;
|
|
61
66
|
}
|
|
62
|
-
} else if (!definedCodes.includes(
|
|
67
|
+
} else if (!definedCodes.includes(respCode)) {
|
|
63
68
|
missing = true;
|
|
64
|
-
msg = `OAR039: Response code ${
|
|
69
|
+
msg = `OAR039: Response code ${respCode} must be defined.`;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
if (missing) {
|