Skip to content

Commit fcbc939

Browse files
authored
feat: add validation of server variable example (#221)
1 parent f9f4a9e commit fcbc939

File tree

5 files changed

+684
-325
lines changed

5 files changed

+684
-325
lines changed

.editorconfig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ insert_final_newline = true
88

99
[*.js]
1010
indent_size = 2
11-
indent_style = space
11+
indent_style = space
12+
quote_type = single

dist/bundle.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/customValidators.js

Lines changed: 160 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,128 @@
11
const ParserError = require('./errors/parser-error');
2-
const { parseUrlVariables, getMissingProps, groupValidationErrors, tilde, parseUrlQueryParameters, setNotProvidedParams } = require('./utils');
2+
const {
3+
parseUrlVariables,
4+
getMissingProps,
5+
groupValidationErrors,
6+
tilde,
7+
parseUrlQueryParameters,
8+
setNotProvidedParams
9+
} = require('./utils');
310
const validationError = 'validation-errors';
411

512
/**
6-
* Validates if variables provided in the url have corresponding variable object defined
13+
* Validates if variables provided in the url have corresponding variable object defined and if example is correct
714
* @private
815
* @param {Object} parsedJSON parsed AsyncAPI document
916
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
1017
* @param {String} initialFormat information of the document was oryginally JSON or YAML
1118
* @returns {Boolean} true in case the document is valid, otherwise throws ParserError
1219
*/
13-
function validateServerVariables(parsedJSON, asyncapiYAMLorJSON, initialFormat) {
20+
function validateServerVariables(
21+
parsedJSON,
22+
asyncapiYAMLorJSON,
23+
initialFormat
24+
) {
1425
const srvs = parsedJSON.servers;
1526
if (!srvs) return true;
1627

1728
const srvsMap = new Map(Object.entries(srvs));
1829
const notProvidedVariables = new Map();
30+
const notProvidedExamplesInEnum = new Map();
1931

20-
srvsMap.forEach((val, key) => {
21-
const variables = parseUrlVariables(val.url);
22-
const notProvidedServerVars = notProvidedVariables.get(tilde(key));
32+
srvsMap.forEach((srvr, srvrName) => {
33+
const variables = parseUrlVariables(srvr.url);
34+
const variablesObj = srvr.variables;
35+
const notProvidedServerVars = notProvidedVariables.get(tilde(srvrName));
2336
if (!variables) return;
2437

25-
const missingServerVariables = getMissingProps(variables, val.variables);
26-
if (!missingServerVariables.length) return;
38+
const missingServerVariables = getMissingProps(variables, variablesObj);
39+
if (missingServerVariables.length) {
40+
notProvidedVariables.set(
41+
tilde(srvrName),
42+
notProvidedServerVars
43+
? notProvidedServerVars.concat(missingServerVariables)
44+
: missingServerVariables
45+
);
46+
}
2747

28-
notProvidedVariables.set(tilde(key),
29-
notProvidedServerVars
30-
? notProvidedServerVars.concat(missingServerVariables)
31-
: missingServerVariables);
48+
if (variablesObj) {
49+
setNotValidExamples(variablesObj, srvrName, notProvidedExamplesInEnum);
50+
}
3251
});
3352

3453
if (notProvidedVariables.size) {
3554
throw new ParserError({
3655
type: validationError,
3756
title: 'Not all server variables are described with variable object',
3857
parsedJSON,
39-
validationErrors: groupValidationErrors('servers', 'server does not have a corresponding variable object for', notProvidedVariables, asyncapiYAMLorJSON, initialFormat)
58+
validationErrors: groupValidationErrors(
59+
'servers',
60+
'server does not have a corresponding variable object for',
61+
notProvidedVariables,
62+
asyncapiYAMLorJSON,
63+
initialFormat
64+
),
65+
});
66+
}
67+
68+
if (notProvidedExamplesInEnum.size) {
69+
throw new ParserError({
70+
type: validationError,
71+
title:
72+
'Check your server variables. The example does not match the enum list',
73+
parsedJSON,
74+
validationErrors: groupValidationErrors(
75+
'servers',
76+
'server variable provides an example that does not match the enum list',
77+
notProvidedExamplesInEnum,
78+
asyncapiYAMLorJSON,
79+
initialFormat
80+
),
4081
});
4182
}
4283

4384
return true;
4485
}
4586

87+
/**
88+
* extend map with info about examples that are not part of the enum
89+
*
90+
* @function setNotValidExamples
91+
* @private
92+
* @param {Array<Object>} variables server variables object
93+
* @param {String} srvrName name of the server where variables object is located
94+
* @param {Map} notProvidedExamplesInEnum result map of all wrong examples and what variable they belong to
95+
*/
96+
function setNotValidExamples(variables, srvrName, notProvidedExamplesInEnum) {
97+
const variablesMap = new Map(Object.entries(variables));
98+
variablesMap.forEach((variable, variableName) => {
99+
if (variable.enum && variable.examples) {
100+
const wrongExamples = variable.examples.filter(r => !variable.enum.includes(r));
101+
if (wrongExamples.length) {
102+
notProvidedExamplesInEnum.set(
103+
`${tilde(srvrName)}/variables/${tilde(variableName)}`,
104+
wrongExamples
105+
);
106+
}
107+
}
108+
});
109+
};
110+
46111
/**
47112
* Validates if operationIds are duplicated in the document
48-
*
113+
*
49114
* @private
50115
* @param {Object} parsedJSON parsed AsyncAPI document
51116
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
52117
* @param {String} initialFormat information of the document was oryginally JSON or YAML
53118
* @returns {Boolean} true in case the document is valid, otherwise throws ParserError
54119
*/
55-
function validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, operations) {
120+
function validateOperationId(
121+
parsedJSON,
122+
asyncapiYAMLorJSON,
123+
initialFormat,
124+
operations
125+
) {
56126
const chnls = parsedJSON.channels;
57127
if (!chnls) return true;
58128
const chnlsMap = new Map(Object.entries(chnls));
@@ -66,15 +136,18 @@ function validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, oper
66136
if (!operationId) return;
67137

68138
const operationPath = `${tilde(channelName)}/${opName}/operationId`;
69-
const isOperationIdDuplicated = allOperations.filter(v => v[0] === operationId);
70-
if (!isOperationIdDuplicated.length) return allOperations.push([operationId, operationPath]);
139+
const isOperationIdDuplicated = allOperations.filter(
140+
(v) => v[0] === operationId
141+
);
142+
if (!isOperationIdDuplicated.length)
143+
return allOperations.push([operationId, operationPath]);
71144

72-
//isOperationIdDuplicated always holds one record and it is an array of paths, the one that is a duplicate and the one that is duplicated
145+
//isOperationIdDuplicated always holds one record and it is an array of paths, the one that is a duplicate and the one that is duplicated
73146
duplicatedOperations.set(operationPath, isOperationIdDuplicated[0][1]);
74147
};
75148

76149
chnlsMap.forEach((chnlObj, chnlName) => {
77-
operations.forEach(opName => {
150+
operations.forEach((opName) => {
78151
const op = chnlObj[String(opName)];
79152
if (op) addDuplicateToMap(op, chnlName, opName);
80153
});
@@ -85,7 +158,13 @@ function validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, oper
85158
type: validationError,
86159
title: 'operationId must be unique across all the operations.',
87160
parsedJSON,
88-
validationErrors: groupValidationErrors('channels', 'is a duplicate of', duplicatedOperations, asyncapiYAMLorJSON, initialFormat)
161+
validationErrors: groupValidationErrors(
162+
'channels',
163+
'is a duplicate of',
164+
duplicatedOperations,
165+
asyncapiYAMLorJSON,
166+
initialFormat
167+
),
89168
});
90169
}
91170

@@ -94,15 +173,20 @@ function validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, oper
94173

95174
/**
96175
* Validates if server security is declared properly and the name has a corresponding security schema definition in components with the same name
97-
*
176+
*
98177
* @private
99178
* @param {Object} parsedJSON parsed AsyncAPI document
100179
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
101180
* @param {String} initialFormat information of the document was oryginally JSON or YAML
102181
* @param {String[]} specialSecTypes list of security types that can have data in array
103182
* @returns {Boolean} true in case the document is valid, otherwise throws ParserError
104183
*/
105-
function validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, specialSecTypes) {
184+
function validateServerSecurity(
185+
parsedJSON,
186+
asyncapiYAMLorJSON,
187+
initialFormat,
188+
specialSecTypes
189+
) {
106190
const srvs = parsedJSON.servers;
107191
if (!srvs) return true;
108192

@@ -119,8 +203,8 @@ function validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, s
119203
if (!serverSecInfo) return true;
120204

121205
//server security info is an array of many possible values
122-
serverSecInfo.forEach(secObj => {
123-
Object.keys(secObj).forEach(secName => {
206+
serverSecInfo.forEach((secObj) => {
207+
Object.keys(secObj).forEach((secName) => {
124208
//security schema is located in components object, we need to find if there is security schema with the same name as the server security info object
125209
const schema = findSecuritySchema(secName, parsedJSON.components);
126210
const srvrSecurityPath = `${serverName}/security/${secName}`;
@@ -129,26 +213,41 @@ function validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, s
129213

130214
//findSecuritySchema returns type always on index 1. Type is needed further to validate if server security info can be or not an empty array
131215
const schemaType = schema[1];
132-
if (!isSrvrSecProperArray(schemaType, specialSecTypes, secObj, secName)) invalidSecurityValues.set(srvrSecurityPath, schemaType);
216+
if (!isSrvrSecProperArray(schemaType, specialSecTypes, secObj, secName))
217+
invalidSecurityValues.set(srvrSecurityPath, schemaType);
133218
});
134219
});
135220
});
136221

137222
if (missingSecSchema.size) {
138223
throw new ParserError({
139224
type: validationError,
140-
title: 'Server security name must correspond to a security scheme which is declared in the security schemes under the components object.',
225+
title:
226+
'Server security name must correspond to a security scheme which is declared in the security schemes under the components object.',
141227
parsedJSON,
142-
validationErrors: groupValidationErrors(root, 'doesn\'t have a corresponding security schema under the components object', missingSecSchema, asyncapiYAMLorJSON, initialFormat)
228+
validationErrors: groupValidationErrors(
229+
root,
230+
'doesn\'t have a corresponding security schema under the components object',
231+
missingSecSchema,
232+
asyncapiYAMLorJSON,
233+
initialFormat
234+
),
143235
});
144236
}
145237

146238
if (invalidSecurityValues.size) {
147239
throw new ParserError({
148240
type: validationError,
149-
title: 'Server security value must be an empty array if corresponding security schema type is not oauth2 or openIdConnect.',
241+
title:
242+
'Server security value must be an empty array if corresponding security schema type is not oauth2 or openIdConnect.',
150243
parsedJSON,
151-
validationErrors: groupValidationErrors(root, 'security info must have an empty array because its corresponding security schema type is', invalidSecurityValues, asyncapiYAMLorJSON, initialFormat)
244+
validationErrors: groupValidationErrors(
245+
root,
246+
'security info must have an empty array because its corresponding security schema type is',
247+
invalidSecurityValues,
248+
asyncapiYAMLorJSON,
249+
initialFormat
250+
),
152251
});
153252
}
154253

@@ -164,7 +263,9 @@ function validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, s
164263
*/
165264
function findSecuritySchema(securityName, components) {
166265
const secSchemes = components && components.securitySchemes;
167-
const secSchemesMap = secSchemes ? new Map(Object.entries(secSchemes)) : new Map();
266+
const secSchemesMap = secSchemes
267+
? new Map(Object.entries(secSchemes))
268+
: new Map();
168269
const schemaInfo = [];
169270

170271
//using for loop here as there is no point to iterate over all entries as it is enough to find first matching element
@@ -198,7 +299,7 @@ function isSrvrSecProperArray(schemaType, specialSecTypes, secObj, secName) {
198299

199300
/**
200301
* Validates if parameters specified in the channel have corresponding parameters object defined and if name does not contain url parameters
201-
*
302+
*
202303
* @private
203304
* @param {Object} parsedJSON parsed AsyncAPI document
204305
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
@@ -217,31 +318,50 @@ function validateChannels(parsedJSON, asyncapiYAMLorJSON, initialFormat) {
217318
const variables = parseUrlVariables(key);
218319
const notProvidedChannelParams = notProvidedParams.get(tilde(key));
219320
const queryParameters = parseUrlQueryParameters(key);
220-
321+
221322
//channel variable validation: fill return obeject with missing parameters
222323
if (variables) {
223-
setNotProvidedParams(variables, val, key, notProvidedChannelParams, notProvidedParams);
324+
setNotProvidedParams(
325+
variables,
326+
val,
327+
key,
328+
notProvidedChannelParams,
329+
notProvidedParams
330+
);
224331
}
225332

226333
//channel name validation: fill return object with channels containing query parameters
227334
if (queryParameters) {
228-
invalidChannelName.set(tilde(key),
229-
queryParameters);
335+
invalidChannelName.set(tilde(key), queryParameters);
230336
}
231337
});
232338

233339
//combine validation errors of both checks and output them as one array
234-
const parameterValidationErrors = groupValidationErrors('channels', 'channel does not have a corresponding parameter object for', notProvidedParams, asyncapiYAMLorJSON, initialFormat);
235-
const nameValidationErrors = groupValidationErrors('channels', 'channel contains invalid name with url query parameters', invalidChannelName, asyncapiYAMLorJSON, initialFormat);
236-
const allValidationErrors = parameterValidationErrors.concat(nameValidationErrors);
340+
const parameterValidationErrors = groupValidationErrors(
341+
'channels',
342+
'channel does not have a corresponding parameter object for',
343+
notProvidedParams,
344+
asyncapiYAMLorJSON,
345+
initialFormat
346+
);
347+
const nameValidationErrors = groupValidationErrors(
348+
'channels',
349+
'channel contains invalid name with url query parameters',
350+
invalidChannelName,
351+
asyncapiYAMLorJSON,
352+
initialFormat
353+
);
354+
const allValidationErrors = parameterValidationErrors.concat(
355+
nameValidationErrors
356+
);
237357

238358
//channel variable validation: throw exception if channel validation failes
239359
if (notProvidedParams.size || invalidChannelName.size) {
240360
throw new ParserError({
241361
type: validationError,
242362
title: 'Channel validation failed',
243363
parsedJSON,
244-
validationErrors: allValidationErrors
364+
validationErrors: allValidationErrors,
245365
});
246366
}
247367

@@ -252,5 +372,5 @@ module.exports = {
252372
validateServerVariables,
253373
validateOperationId,
254374
validateServerSecurity,
255-
validateChannels
375+
validateChannels,
256376
};

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)