api-quality-spectral-ruleset 1.0.0 → 1.1.0
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 +68 -20
- 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,11 @@ 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
|
|
10
15
|
extends:
|
|
11
16
|
- spectral:asyncapi
|
|
12
17
|
- spectral:oas
|
|
@@ -481,6 +486,17 @@ rules:
|
|
|
481
486
|
then:
|
|
482
487
|
field: "x-aut-type"
|
|
483
488
|
function: truthy
|
|
489
|
+
apiq:OAR042:
|
|
490
|
+
description: "Base path must be compliant with the standard."
|
|
491
|
+
message: "OAR042: Base path must follow the '/api-<name>/v<version>' standard."
|
|
492
|
+
severity: "error"
|
|
493
|
+
given:
|
|
494
|
+
- "$.basePath"
|
|
495
|
+
- "$.servers[*].url"
|
|
496
|
+
then:
|
|
497
|
+
function: pattern
|
|
498
|
+
functionOptions:
|
|
499
|
+
match: "^(/api-[^/]+/v[0-9]+|https?:\\/\\/[^/]+\\/api-[^/]+\\/v[0-9]+)$"
|
|
484
500
|
apiq:OAR044:
|
|
485
501
|
description: "Declared media type should conform to RFC6838 and RFC7231."
|
|
486
502
|
message: "OAR044: Declared media type range should conform to RFC7231."
|
|
@@ -590,6 +606,20 @@ rules:
|
|
|
590
606
|
then:
|
|
591
607
|
field: format
|
|
592
608
|
function: truthy
|
|
609
|
+
apiq:OAR053:
|
|
610
|
+
description: "There are mandatory response headers and others that are not allowed."
|
|
611
|
+
message: "{{error}}"
|
|
612
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR053.md"
|
|
613
|
+
severity: error
|
|
614
|
+
given: "$.paths[*][*].responses[*]"
|
|
615
|
+
then:
|
|
616
|
+
- function: apq-response-headers
|
|
617
|
+
functionOptions:
|
|
618
|
+
mandatory-headers: "x-trace-id,x-request-id"
|
|
619
|
+
allowed-headers: "idcorrelacion,x-correlacionid,x-global-trasaction-id,x-power-by,x-trace-id,x-request-id"
|
|
620
|
+
included-response-codes: "*"
|
|
621
|
+
excluded-response-codes: "204"
|
|
622
|
+
path-exclusions: "/status,/prueba"
|
|
593
623
|
apiq:OAR054:
|
|
594
624
|
description: "Ensure the host matches the specified format"
|
|
595
625
|
message: "OAR054: Hostname must be a subdomain of the organization's domain name."
|
|
@@ -812,20 +842,22 @@ rules:
|
|
|
812
842
|
match: ^\$?_?[a-z]+(_[a-z]+)*$
|
|
813
843
|
apiq:OAR078:
|
|
814
844
|
description: "All API methods must have security defined."
|
|
815
|
-
message: "
|
|
845
|
+
message: "{{error}}"
|
|
846
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR078.md"
|
|
816
847
|
severity: error
|
|
817
|
-
given: "
|
|
848
|
+
given: "$"
|
|
818
849
|
then:
|
|
819
|
-
|
|
820
|
-
function: truthy
|
|
850
|
+
function: apq-security-check
|
|
821
851
|
apiq:OAR079:
|
|
822
852
|
description: "Operations with path parameters should include a 404 Not Found response."
|
|
823
853
|
message: "OAR079: Path parameter present, but missing 404 Not Found response."
|
|
854
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR079.md"
|
|
824
855
|
severity: "warn"
|
|
825
|
-
given: "$.paths[*][
|
|
856
|
+
given: "$.paths[*][get,post,put,patch,delete]"
|
|
826
857
|
then:
|
|
827
|
-
|
|
828
|
-
|
|
858
|
+
function: apq-require-response-on-path-params
|
|
859
|
+
functionOptions:
|
|
860
|
+
response: "404"
|
|
829
861
|
apiq:OAR080:
|
|
830
862
|
description: "The security scheme must be among those allowed by the organization and must be complete."
|
|
831
863
|
message: "OAR080: The security scheme '{{property}}' must be among those allowed by the organization and must be complete."
|
|
@@ -1101,19 +1133,35 @@ rules:
|
|
|
1101
1133
|
then:
|
|
1102
1134
|
field: "contact"
|
|
1103
1135
|
function: truthy
|
|
1136
|
+
apiq:OAR113:
|
|
1137
|
+
description: "Field or extension must be at the assigned location"
|
|
1138
|
+
message: "{{error}}"
|
|
1139
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR113.md"
|
|
1140
|
+
severity: warn
|
|
1141
|
+
given: "$"
|
|
1142
|
+
then:
|
|
1143
|
+
function: apq-custom-field
|
|
1144
|
+
functionOptions:
|
|
1145
|
+
fieldName: "x-custom-example"
|
|
1146
|
+
fieldLocation: "path,operation_get,response_200"
|
|
1104
1147
|
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[*]
|
|
1148
|
+
description: "There are mandatory response headers and others that are not allowed."
|
|
1149
|
+
message: "{{error}}"
|
|
1150
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR114.md"
|
|
1151
|
+
severity: "error"
|
|
1152
|
+
given: "$.paths[*][*].responses[*]"
|
|
1110
1153
|
then:
|
|
1111
|
-
- function:
|
|
1154
|
+
- function: apq-response-headers
|
|
1112
1155
|
functionOptions:
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1156
|
+
mandatory-headers: "x-api-key,x-trace-id"
|
|
1157
|
+
allowed-headers: "x-api-key,x-trace-id,traceId,dateTime"
|
|
1158
|
+
apiq:OAR115:
|
|
1159
|
+
description: "All fields listed in the required array must be defined in the schema properties."
|
|
1160
|
+
message: "{{error}}"
|
|
1161
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR115.md"
|
|
1162
|
+
severity: warn
|
|
1163
|
+
given:
|
|
1164
|
+
- "$.components.schemas[*]"
|
|
1165
|
+
- "$.definitions[*]"
|
|
1166
|
+
then:
|
|
1167
|
+
function: apq-required-fields-exist
|
|
@@ -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) {
|