@e22m4u/js-openapi 0.0.5
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/.c8rc +9 -0
- package/.commitlintrc +5 -0
- package/.editorconfig +13 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +6 -0
- package/.mocharc.json +4 -0
- package/.prettierrc +7 -0
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/build-cjs.js +16 -0
- package/dist/cjs/index.cjs +2695 -0
- package/eslint.config.js +41 -0
- package/package.json +64 -0
- package/src/data-type/index.d.ts +1 -0
- package/src/data-type/index.js +1 -0
- package/src/data-type/infer-openapi-data-type.d.ts +30 -0
- package/src/data-type/infer-openapi-data-type.js +38 -0
- package/src/data-validation/data-format-validator-map.d.ts +13 -0
- package/src/data-validation/data-format-validator-map.js +36 -0
- package/src/data-validation/data-format-validator-map.spec.js +39 -0
- package/src/data-validation/data-format-validators.d.ts +84 -0
- package/src/data-validation/data-format-validators.js +217 -0
- package/src/data-validation/index.d.ts +3 -0
- package/src/data-validation/index.js +3 -0
- package/src/data-validation/validate-data-with-openapi-schema.d.ts +46 -0
- package/src/data-validation/validate-data-with-openapi-schema.js +1913 -0
- package/src/data-validation/validate-data-with-openapi-schema.spec.js +6953 -0
- package/src/errors/index.d.ts +1 -0
- package/src/errors/index.js +1 -0
- package/src/errors/oa-data-validation-error.d.ts +6 -0
- package/src/errors/oa-data-validation-error.js +6 -0
- package/src/errors/oa-data-validation-error.spec.js +17 -0
- package/src/index.d.ts +9 -0
- package/src/index.js +9 -0
- package/src/json-pointer/escape-json-pointer.d.ts +7 -0
- package/src/json-pointer/escape-json-pointer.js +18 -0
- package/src/json-pointer/escape-json-pointer.spec.js +36 -0
- package/src/json-pointer/index.d.ts +3 -0
- package/src/json-pointer/index.js +3 -0
- package/src/json-pointer/resolve-json-pointer.d.ts +10 -0
- package/src/json-pointer/resolve-json-pointer.js +83 -0
- package/src/json-pointer/resolve-json-pointer.spec.js +103 -0
- package/src/json-pointer/unescape-json-pointer.d.ts +8 -0
- package/src/json-pointer/unescape-json-pointer.js +18 -0
- package/src/json-pointer/unescape-json-pointer.spec.js +32 -0
- package/src/oa-document-builder.d.ts +312 -0
- package/src/oa-document-builder.js +450 -0
- package/src/oa-document-object/index.d.ts +1 -0
- package/src/oa-document-object/index.js +1 -0
- package/src/oa-document-object/validate-shallow-oa-document.d.ts +10 -0
- package/src/oa-document-object/validate-shallow-oa-document.js +209 -0
- package/src/oa-document-object/validate-shallow-oa-document.spec.js +362 -0
- package/src/oa-document-scope.d.ts +52 -0
- package/src/oa-document-scope.js +228 -0
- package/src/oa-reference-object/index.d.ts +3 -0
- package/src/oa-reference-object/index.js +3 -0
- package/src/oa-reference-object/is-oa-reference-object.d.ts +9 -0
- package/src/oa-reference-object/is-oa-reference-object.js +14 -0
- package/src/oa-reference-object/is-oa-reference-object.spec.js +19 -0
- package/src/oa-reference-object/oa-ref.d.ts +11 -0
- package/src/oa-reference-object/oa-ref.js +31 -0
- package/src/oa-reference-object/oa-ref.spec.js +56 -0
- package/src/oa-reference-object/resolve-oa-reference-object.d.ts +18 -0
- package/src/oa-reference-object/resolve-oa-reference-object.js +113 -0
- package/src/oa-reference-object/resolve-oa-reference-object.spec.js +233 -0
- package/src/oa-specification.d.ts +767 -0
- package/src/oa-specification.js +153 -0
- package/src/types.d.ts +4 -0
- package/src/utils/count-unicode.d.ts +11 -0
- package/src/utils/count-unicode.js +15 -0
- package/src/utils/index.d.ts +5 -0
- package/src/utils/index.js +5 -0
- package/src/utils/join-path.d.ts +6 -0
- package/src/utils/join-path.js +36 -0
- package/src/utils/join-path.spec.js +104 -0
- package/src/utils/normalize-path.d.ts +12 -0
- package/src/utils/normalize-path.js +22 -0
- package/src/utils/normalize-path.spec.js +56 -0
- package/src/utils/to-pascal-case.d.ts +6 -0
- package/src/utils/to-pascal-case.js +26 -0
- package/src/utils/to-pascal-case.spec.js +15 -0
- package/src/utils/to-spaced-json.d.ts +17 -0
- package/src/utils/to-spaced-json.js +27 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,1913 @@
|
|
|
1
|
+
import {countUnicode} from '../utils/count-unicode.js';
|
|
2
|
+
import {toSpacedJson} from '../utils/to-spaced-json.js';
|
|
3
|
+
import {toPascalCase} from '../utils/to-pascal-case.js';
|
|
4
|
+
import {OADataValidationError} from '../errors/index.js';
|
|
5
|
+
import {inferOpenApiDataType} from '../data-type/index.js';
|
|
6
|
+
import {format, InvalidArgumentError} from '@e22m4u/js-format';
|
|
7
|
+
import {resolveOAReferenceObject} from '../oa-reference-object/index.js';
|
|
8
|
+
import {OA_BUILT_IN_DATA_FORMAT_VALIDATOR_MAP} from './data-format-validator-map.js';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
OADataType,
|
|
12
|
+
OAAccessMode,
|
|
13
|
+
ACCESS_MODE_LIST,
|
|
14
|
+
} from '../oa-specification.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Регулярное выражение для целых чисел в формате JSON.
|
|
18
|
+
* Согласно RFC 8259, не допускает дробной части,
|
|
19
|
+
* экспоненты и явного знака "+".
|
|
20
|
+
*/
|
|
21
|
+
export const JSON_INTEGER_REGEXP = /^-?\d+$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Регулярное выражение для чисел в формате JSON.
|
|
25
|
+
* Поддерживает дробную часть и научную нотацию
|
|
26
|
+
* (например, -1.5e+10).
|
|
27
|
+
*/
|
|
28
|
+
export const JSON_NUMBER_REGEXP = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Unsupported schema properties.
|
|
32
|
+
*/
|
|
33
|
+
const UNSUPPORTED_SCHEMA_KEYWORDS = [
|
|
34
|
+
'nullable', // удалено в 3.1.0
|
|
35
|
+
'contentEncoding',
|
|
36
|
+
'contentMediaType',
|
|
37
|
+
'contentSchema',
|
|
38
|
+
'discriminator',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate data with OpenAPI Schema.
|
|
43
|
+
*
|
|
44
|
+
* @param {*} data
|
|
45
|
+
* @param {boolean|object} schema
|
|
46
|
+
* @param {object} [options]
|
|
47
|
+
* @returns {object}
|
|
48
|
+
*/
|
|
49
|
+
export function validateDataWithOpenApiSchema(data, schema, options = {}) {
|
|
50
|
+
if (
|
|
51
|
+
schema === null ||
|
|
52
|
+
(typeof schema !== 'boolean' && typeof schema !== 'object') ||
|
|
53
|
+
Array.isArray(schema)
|
|
54
|
+
) {
|
|
55
|
+
throw new InvalidArgumentError(
|
|
56
|
+
'Parameter "schema" must be a Boolean or an Object, but %v was given.',
|
|
57
|
+
schema,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
61
|
+
throw new InvalidArgumentError(
|
|
62
|
+
'Parameter "options" must be an Object, but %v was given.',
|
|
63
|
+
options,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (options.rootDocument !== undefined) {
|
|
67
|
+
if (
|
|
68
|
+
!options.rootDocument ||
|
|
69
|
+
typeof options.rootDocument !== 'object' ||
|
|
70
|
+
Array.isArray(options.rootDocument)
|
|
71
|
+
) {
|
|
72
|
+
throw new InvalidArgumentError(
|
|
73
|
+
'Option "rootDocument" must be an Object, but %v was given.',
|
|
74
|
+
options.rootDocument,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (options.required !== undefined) {
|
|
79
|
+
if (typeof options.required !== 'boolean') {
|
|
80
|
+
throw new InvalidArgumentError(
|
|
81
|
+
'Option "required" must be a Boolean, but %v was given.',
|
|
82
|
+
options.required,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (options.coerceTypes !== undefined) {
|
|
87
|
+
if (typeof options.coerceTypes !== 'boolean') {
|
|
88
|
+
throw new InvalidArgumentError(
|
|
89
|
+
'Option "coerceTypes" must be a Boolean, but %v was given.',
|
|
90
|
+
options.coerceTypes,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (options.applyDefaults !== undefined) {
|
|
95
|
+
if (typeof options.applyDefaults !== 'boolean') {
|
|
96
|
+
throw new InvalidArgumentError(
|
|
97
|
+
'Option "applyDefaults" must be a Boolean, but %v was given.',
|
|
98
|
+
options.applyDefaults,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (options.stripUnknown !== undefined) {
|
|
103
|
+
if (typeof options.stripUnknown !== 'boolean') {
|
|
104
|
+
throw new InvalidArgumentError(
|
|
105
|
+
'Option "stripUnknown" must be a Boolean, but %v was given.',
|
|
106
|
+
options.stripUnknown,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (options.dataSourceUri !== undefined) {
|
|
111
|
+
if (!options.dataSourceUri || typeof options.dataSourceUri !== 'string') {
|
|
112
|
+
throw new InvalidArgumentError(
|
|
113
|
+
'Option "dataSourceUri" must be a non-empty String, but %v was given.',
|
|
114
|
+
options.dataSourceUri,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (options.schemaSourceUri !== undefined) {
|
|
119
|
+
if (
|
|
120
|
+
!options.schemaSourceUri ||
|
|
121
|
+
typeof options.schemaSourceUri !== 'string'
|
|
122
|
+
) {
|
|
123
|
+
throw new InvalidArgumentError(
|
|
124
|
+
'Option "schemaSourceUri" must be a non-empty String, ' +
|
|
125
|
+
'but %v was given.',
|
|
126
|
+
options.schemaSourceUri,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (options.silent !== undefined) {
|
|
131
|
+
if (typeof options.silent !== 'boolean') {
|
|
132
|
+
throw new InvalidArgumentError(
|
|
133
|
+
'Option "silent" must be a Boolean, but %v was given.',
|
|
134
|
+
options.silent,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (options.accessMode !== undefined) {
|
|
139
|
+
if (!ACCESS_MODE_LIST.includes(options.accessMode)) {
|
|
140
|
+
throw new InvalidArgumentError(
|
|
141
|
+
'Access mode %v is not supported.',
|
|
142
|
+
options.accessMode,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (options.formatValidators !== undefined) {
|
|
147
|
+
if (
|
|
148
|
+
!options.formatValidators ||
|
|
149
|
+
typeof options.formatValidators !== 'object' ||
|
|
150
|
+
Array.isArray(options.formatValidators)
|
|
151
|
+
) {
|
|
152
|
+
throw new InvalidArgumentError(
|
|
153
|
+
'Option "formatValidators" must be an Object, but %v was given.',
|
|
154
|
+
options.formatValidators,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
Object.keys(options.formatValidators).forEach(formatName => {
|
|
158
|
+
const formatValidator = options.formatValidators[formatName];
|
|
159
|
+
if (
|
|
160
|
+
formatValidator !== undefined &&
|
|
161
|
+
typeof formatValidator !== 'function'
|
|
162
|
+
) {
|
|
163
|
+
throw new InvalidArgumentError(
|
|
164
|
+
'Format validator %v must be a Function, but %v was given.',
|
|
165
|
+
formatName,
|
|
166
|
+
formatValidator,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (options.parseJson !== undefined) {
|
|
172
|
+
if (typeof options.parseJson !== 'boolean') {
|
|
173
|
+
throw new InvalidArgumentError(
|
|
174
|
+
'Option "parseJson" must be a Boolean, but %v was given.',
|
|
175
|
+
options.parseJson,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// если обязательное значение является
|
|
180
|
+
// undefined, то выбрасывается ошибка
|
|
181
|
+
if (options.required === true && data === undefined) {
|
|
182
|
+
const result = {
|
|
183
|
+
isValid: false,
|
|
184
|
+
value: data,
|
|
185
|
+
valueUri: options.dataSourceUri || '',
|
|
186
|
+
schemaUri: options.schemaSourceUri || '',
|
|
187
|
+
reason: format(
|
|
188
|
+
'Value at %v is required, but %v was given.',
|
|
189
|
+
options.dataSourceUri || '',
|
|
190
|
+
data,
|
|
191
|
+
),
|
|
192
|
+
};
|
|
193
|
+
if (!options.silent) {
|
|
194
|
+
throw new OADataValidationError(result.reason);
|
|
195
|
+
} else {
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const subResult = _validate(data, schema, {
|
|
200
|
+
...options,
|
|
201
|
+
dataSourceUri: options.dataSourceUri || '',
|
|
202
|
+
schemaSourceUri: options.schemaSourceUri || '',
|
|
203
|
+
});
|
|
204
|
+
// если данные не прошли проверку, а тихий
|
|
205
|
+
// режим отключен, то выбрасывается ошибка
|
|
206
|
+
if (!subResult.isValid && !options.silent) {
|
|
207
|
+
throw new OADataValidationError(subResult.reason);
|
|
208
|
+
}
|
|
209
|
+
const result = {
|
|
210
|
+
isValid: subResult.isValid,
|
|
211
|
+
reason: subResult.reason,
|
|
212
|
+
value: subResult.value,
|
|
213
|
+
valueUri: subResult.valueUri,
|
|
214
|
+
schemaUri: subResult.schemaUri,
|
|
215
|
+
};
|
|
216
|
+
// если проверка выполнена успешно,
|
|
217
|
+
// то из результата исключается "reason"
|
|
218
|
+
if (result.isValid) {
|
|
219
|
+
delete result.reason;
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Validate (internal).
|
|
226
|
+
*
|
|
227
|
+
* @param {*} data
|
|
228
|
+
* @param {boolean|object} schema
|
|
229
|
+
* @param {object} options
|
|
230
|
+
* @returns {object}
|
|
231
|
+
*/
|
|
232
|
+
function _validate(data, schema, options) {
|
|
233
|
+
if (
|
|
234
|
+
schema === null ||
|
|
235
|
+
(typeof schema !== 'boolean' && typeof schema !== 'object') ||
|
|
236
|
+
Array.isArray(schema)
|
|
237
|
+
) {
|
|
238
|
+
throw new InvalidArgumentError(
|
|
239
|
+
'Schema at %v must be a Boolean or an Object, but %v was given.',
|
|
240
|
+
options.schemaSourceUri,
|
|
241
|
+
schema,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
// если схема является логическим значением,
|
|
245
|
+
// то значение используется как правило допуска
|
|
246
|
+
if (typeof schema === 'boolean') {
|
|
247
|
+
if (schema === true) {
|
|
248
|
+
return {
|
|
249
|
+
isValid: true,
|
|
250
|
+
value: data,
|
|
251
|
+
valueUri: options.dataSourceUri,
|
|
252
|
+
schemaUri: options.schemaSourceUri,
|
|
253
|
+
};
|
|
254
|
+
} else if (data !== undefined) {
|
|
255
|
+
return {
|
|
256
|
+
isValid: false,
|
|
257
|
+
value: data,
|
|
258
|
+
valueUri: options.dataSourceUri,
|
|
259
|
+
schemaUri: options.schemaSourceUri,
|
|
260
|
+
reason: format(
|
|
261
|
+
'Value at %v is rejected by the boolean schema at %v.',
|
|
262
|
+
options.dataSourceUri,
|
|
263
|
+
options.schemaSourceUri,
|
|
264
|
+
),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// сеты для сбора проверенных индексов массива
|
|
269
|
+
// и свойств объекта на текущем уровне вложенности
|
|
270
|
+
let evaluatedIndexes = new Set();
|
|
271
|
+
let evaluatedProperties = new Set();
|
|
272
|
+
// если параметр "applyDefaults" имеет значение true,
|
|
273
|
+
// а исходным значением является undefined, то исходное
|
|
274
|
+
// значение подменяется значением по умолчанию
|
|
275
|
+
if (
|
|
276
|
+
schema.default !== undefined &&
|
|
277
|
+
data === undefined &&
|
|
278
|
+
options.applyDefaults === true
|
|
279
|
+
) {
|
|
280
|
+
data = schema.default;
|
|
281
|
+
}
|
|
282
|
+
// parse json
|
|
283
|
+
if (
|
|
284
|
+
options.parseJson === true &&
|
|
285
|
+
schema.type !== undefined &&
|
|
286
|
+
typeof data === 'string'
|
|
287
|
+
) {
|
|
288
|
+
data = _parseJson(data, schema.type);
|
|
289
|
+
}
|
|
290
|
+
// coerce type
|
|
291
|
+
if (
|
|
292
|
+
options.coerceTypes === true &&
|
|
293
|
+
schema.type !== undefined &&
|
|
294
|
+
data !== undefined
|
|
295
|
+
) {
|
|
296
|
+
data = _coerceType(data, schema.type);
|
|
297
|
+
}
|
|
298
|
+
// если текущая схема содержит свойство "$ref" или "$dynamicRef",
|
|
299
|
+
// то выполняется извлечение целевой схемы и проверка данных
|
|
300
|
+
// согласно извлеченной схемы
|
|
301
|
+
if (schema.$ref !== undefined || schema.$dynamicRef !== undefined) {
|
|
302
|
+
const resSchema = resolveOAReferenceObject(schema, {
|
|
303
|
+
rootDocument: options.rootDocument,
|
|
304
|
+
});
|
|
305
|
+
const result = _validate(data, resSchema, {
|
|
306
|
+
...options,
|
|
307
|
+
schemaSourceUri: schema.$ref || schema.$dynamicRef,
|
|
308
|
+
});
|
|
309
|
+
if (!result.isValid) {
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
// данные могли измениться во время рекурсии,
|
|
313
|
+
// выполняется обновление исходного значения
|
|
314
|
+
data = result.value;
|
|
315
|
+
// внедрение аннотаций о проверенных индексах
|
|
316
|
+
// из результата успешной проверки
|
|
317
|
+
if (result.evaluatedIndexes) {
|
|
318
|
+
evaluatedIndexes = new Set([
|
|
319
|
+
...evaluatedIndexes,
|
|
320
|
+
...result.evaluatedIndexes,
|
|
321
|
+
]);
|
|
322
|
+
}
|
|
323
|
+
// внедрение аннотаций о проверенных свойствах
|
|
324
|
+
// из результата успешной проверки
|
|
325
|
+
if (result.evaluatedProperties) {
|
|
326
|
+
evaluatedProperties = new Set([
|
|
327
|
+
...evaluatedProperties,
|
|
328
|
+
...result.evaluatedProperties,
|
|
329
|
+
]);
|
|
330
|
+
}
|
|
331
|
+
// аннотации могут всплывать из "$ref" и "$dynamicRef"
|
|
332
|
+
// аппликаторов, но не передаются во вложенные схемы,
|
|
333
|
+
// на которые ссылаются данные ключевые слова
|
|
334
|
+
}
|
|
335
|
+
// при наличии неподдерживаемых ключевых
|
|
336
|
+
// слов схемы выбрасывается ошибка
|
|
337
|
+
UNSUPPORTED_SCHEMA_KEYWORDS.forEach(keyword => {
|
|
338
|
+
if (schema[keyword] !== undefined) {
|
|
339
|
+
throw new InvalidArgumentError(
|
|
340
|
+
'Schema keyword %v is not supported.',
|
|
341
|
+
keyword,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
// readOnly
|
|
346
|
+
if (
|
|
347
|
+
options.accessMode === OAAccessMode.WRITE &&
|
|
348
|
+
schema.readOnly === true &&
|
|
349
|
+
data !== undefined
|
|
350
|
+
) {
|
|
351
|
+
return {
|
|
352
|
+
isValid: false,
|
|
353
|
+
value: data,
|
|
354
|
+
valueUri: options.dataSourceUri,
|
|
355
|
+
schemaUri: options.schemaSourceUri,
|
|
356
|
+
reason: format(
|
|
357
|
+
'Value at %v is read-only and cannot be sent in a write context.',
|
|
358
|
+
options.dataSourceUri,
|
|
359
|
+
),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
// writeOnly
|
|
363
|
+
if (
|
|
364
|
+
options.accessMode === OAAccessMode.READ &&
|
|
365
|
+
schema.writeOnly === true &&
|
|
366
|
+
data !== undefined
|
|
367
|
+
) {
|
|
368
|
+
return {
|
|
369
|
+
isValid: false,
|
|
370
|
+
value: data,
|
|
371
|
+
valueUri: options.dataSourceUri,
|
|
372
|
+
schemaUri: options.schemaSourceUri,
|
|
373
|
+
reason: format(
|
|
374
|
+
'Value at %v is write-only and cannot be present in a read context.',
|
|
375
|
+
options.dataSourceUri,
|
|
376
|
+
),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
// прежде чем перейти к проверке данных "in-place" аппликаторами,
|
|
380
|
+
// нужно учитывать, что согласно спецификации JSON Schema 2020-12,
|
|
381
|
+
// аннотации должны всплывать снизу вверх или действовать на текущем
|
|
382
|
+
// уровне, а не погружаться в подсхемы аппликаторов
|
|
383
|
+
// allOf
|
|
384
|
+
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
385
|
+
for (const index in schema.allOf) {
|
|
386
|
+
const subSchema = schema.allOf[index];
|
|
387
|
+
const result = _validate(data, subSchema, {
|
|
388
|
+
...options,
|
|
389
|
+
schemaSourceUri: options.schemaSourceUri + '/allOf/' + index,
|
|
390
|
+
});
|
|
391
|
+
if (!result.isValid) {
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
// данные могли измениться во время рекурсии,
|
|
395
|
+
// выполняется обновление исходного значения
|
|
396
|
+
data = result.value;
|
|
397
|
+
// внедрение аннотаций о проверенных индексах
|
|
398
|
+
// из результата успешной проверки
|
|
399
|
+
if (result.evaluatedIndexes) {
|
|
400
|
+
evaluatedIndexes = new Set([
|
|
401
|
+
...evaluatedIndexes,
|
|
402
|
+
...result.evaluatedIndexes,
|
|
403
|
+
]);
|
|
404
|
+
}
|
|
405
|
+
// внедрение аннотаций о проверенных свойствах
|
|
406
|
+
// из результата успешной проверки
|
|
407
|
+
if (result.evaluatedProperties) {
|
|
408
|
+
evaluatedProperties = new Set([
|
|
409
|
+
...evaluatedProperties,
|
|
410
|
+
...result.evaluatedProperties,
|
|
411
|
+
]);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// oneOf
|
|
416
|
+
if (schema.oneOf && Array.isArray(schema.oneOf)) {
|
|
417
|
+
let matches = 0;
|
|
418
|
+
let matchedResultValue = data;
|
|
419
|
+
let matchedEvaluatedIndexes = undefined;
|
|
420
|
+
let matchedEvaluatedProperties = undefined;
|
|
421
|
+
for (const index in schema.oneOf) {
|
|
422
|
+
const subSchema = schema.oneOf[index];
|
|
423
|
+
const result = _validate(data, subSchema, {
|
|
424
|
+
...options,
|
|
425
|
+
schemaSourceUri: options.schemaSourceUri + '/oneOf/' + index,
|
|
426
|
+
});
|
|
427
|
+
if (result.isValid) {
|
|
428
|
+
matches++;
|
|
429
|
+
// сохранение результата приведения типов
|
|
430
|
+
// из последней успешной проверки
|
|
431
|
+
matchedResultValue = result.value;
|
|
432
|
+
// сохранение аннотаций о проверенных
|
|
433
|
+
// свойствах из успешной проверки
|
|
434
|
+
matchedEvaluatedIndexes = result.evaluatedIndexes;
|
|
435
|
+
// сохранение аннотаций о проверенных
|
|
436
|
+
// свойствах из успешной проверки
|
|
437
|
+
matchedEvaluatedProperties = result.evaluatedProperties;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (matches !== 1) {
|
|
441
|
+
const schemaUri = options.schemaSourceUri + '/oneOf';
|
|
442
|
+
return {
|
|
443
|
+
isValid: false,
|
|
444
|
+
value: data,
|
|
445
|
+
valueUri: options.dataSourceUri,
|
|
446
|
+
schemaUri,
|
|
447
|
+
reason: format(
|
|
448
|
+
'Value at %v must match exactly one of schemas at %v, ' +
|
|
449
|
+
'but %d matched.',
|
|
450
|
+
options.dataSourceUri,
|
|
451
|
+
schemaUri,
|
|
452
|
+
matches,
|
|
453
|
+
),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
// обновление исходных данных
|
|
457
|
+
// значением успешной проверки
|
|
458
|
+
data = matchedResultValue;
|
|
459
|
+
// внедрение аннотаций о проверенных индексах
|
|
460
|
+
// из результата успешной проверки
|
|
461
|
+
if (matchedEvaluatedIndexes) {
|
|
462
|
+
evaluatedIndexes = new Set([
|
|
463
|
+
...evaluatedIndexes,
|
|
464
|
+
...matchedEvaluatedIndexes,
|
|
465
|
+
]);
|
|
466
|
+
}
|
|
467
|
+
// внедрение аннотаций о проверенных свойствах
|
|
468
|
+
// из результата успешной проверки
|
|
469
|
+
if (matchedEvaluatedProperties) {
|
|
470
|
+
evaluatedProperties = new Set([
|
|
471
|
+
...evaluatedProperties,
|
|
472
|
+
...matchedEvaluatedProperties,
|
|
473
|
+
]);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// anyOf
|
|
477
|
+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
|
478
|
+
let matchFound = false;
|
|
479
|
+
for (const index in schema.anyOf) {
|
|
480
|
+
const subSchema = schema.anyOf[index];
|
|
481
|
+
const result = _validate(data, subSchema, {
|
|
482
|
+
...options,
|
|
483
|
+
schemaSourceUri: options.schemaSourceUri + '/anyOf/' + index,
|
|
484
|
+
});
|
|
485
|
+
if (result.isValid) {
|
|
486
|
+
// обновление исходных данных значением
|
|
487
|
+
// из первой успешной проверки
|
|
488
|
+
if (!matchFound) {
|
|
489
|
+
data = result.value;
|
|
490
|
+
}
|
|
491
|
+
// внедрение аннотаций о проверенных индексах
|
|
492
|
+
// из результата успешной проверки
|
|
493
|
+
if (result.evaluatedIndexes) {
|
|
494
|
+
evaluatedIndexes = new Set([
|
|
495
|
+
...evaluatedIndexes,
|
|
496
|
+
...result.evaluatedIndexes,
|
|
497
|
+
]);
|
|
498
|
+
}
|
|
499
|
+
// внедрение аннотаций о проверенных свойствах
|
|
500
|
+
// из результата успешной проверки
|
|
501
|
+
if (result.evaluatedProperties) {
|
|
502
|
+
evaluatedProperties = new Set([
|
|
503
|
+
...evaluatedProperties,
|
|
504
|
+
...result.evaluatedProperties,
|
|
505
|
+
]);
|
|
506
|
+
}
|
|
507
|
+
matchFound = true;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (!matchFound) {
|
|
511
|
+
const schemaUri = options.schemaSourceUri + '/anyOf';
|
|
512
|
+
return {
|
|
513
|
+
isValid: false,
|
|
514
|
+
value: data,
|
|
515
|
+
valueUri: options.dataSourceUri,
|
|
516
|
+
schemaUri,
|
|
517
|
+
reason: format(
|
|
518
|
+
'Value at %v must match at least one of schemas at %v, ' +
|
|
519
|
+
'but 0 matched.',
|
|
520
|
+
options.dataSourceUri,
|
|
521
|
+
schemaUri,
|
|
522
|
+
),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// not
|
|
527
|
+
if (schema.not) {
|
|
528
|
+
const {isValid} = _validate(data, schema.not, {
|
|
529
|
+
...options,
|
|
530
|
+
schemaSourceUri: options.schemaSourceUri + '/not',
|
|
531
|
+
});
|
|
532
|
+
if (isValid) {
|
|
533
|
+
return {
|
|
534
|
+
isValid: false,
|
|
535
|
+
value: data,
|
|
536
|
+
valueUri: options.dataSourceUri,
|
|
537
|
+
schemaUri: options.schemaSourceUri + '/not',
|
|
538
|
+
reason: format(
|
|
539
|
+
'Value at %v must not match the "not" schema.',
|
|
540
|
+
options.dataSourceUri,
|
|
541
|
+
),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
// @todo должны ли мы учитывать свойства, которые
|
|
545
|
+
// прошли проверку в аннотациях проверенных свойств?
|
|
546
|
+
}
|
|
547
|
+
// if / then / else
|
|
548
|
+
if (schema.if) {
|
|
549
|
+
// если данные не соответствуют схеме "if",
|
|
550
|
+
// то результат проверки будет проигнорирован
|
|
551
|
+
// без ошибки для всего документа
|
|
552
|
+
const ifResult = _validate(data, schema.if, {
|
|
553
|
+
...options,
|
|
554
|
+
schemaSourceUri: options.schemaSourceUri + '/if',
|
|
555
|
+
});
|
|
556
|
+
if (ifResult.isValid) {
|
|
557
|
+
// так как при проверке по схеме "if" данные
|
|
558
|
+
// могли измениться, выполняется обновление
|
|
559
|
+
// исходного значения
|
|
560
|
+
data = ifResult.value;
|
|
561
|
+
// внедрение аннотаций о проверенных индексах
|
|
562
|
+
// из результата успешной проверки схемой "if"
|
|
563
|
+
if (ifResult.evaluatedIndexes) {
|
|
564
|
+
evaluatedIndexes = new Set([
|
|
565
|
+
...evaluatedIndexes,
|
|
566
|
+
...ifResult.evaluatedIndexes,
|
|
567
|
+
]);
|
|
568
|
+
}
|
|
569
|
+
// внедрение аннотаций о проверенных свойствах
|
|
570
|
+
// из результата успешной проверки схемой "if"
|
|
571
|
+
if (ifResult.evaluatedProperties) {
|
|
572
|
+
evaluatedProperties = new Set([
|
|
573
|
+
...evaluatedProperties,
|
|
574
|
+
...ifResult.evaluatedProperties,
|
|
575
|
+
]);
|
|
576
|
+
}
|
|
577
|
+
// если определена схема "then", то данные
|
|
578
|
+
// обязаны соответствовать указанной схеме
|
|
579
|
+
if (schema.then) {
|
|
580
|
+
const result = _validate(data, schema.then, {
|
|
581
|
+
...options,
|
|
582
|
+
schemaSourceUri: options.schemaSourceUri + '/then',
|
|
583
|
+
});
|
|
584
|
+
if (!result.isValid) {
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
// так как при проверке по схеме "then" данные
|
|
588
|
+
// могли измениться, выполняется обновление
|
|
589
|
+
// исходного значения
|
|
590
|
+
data = result.value;
|
|
591
|
+
// внедрение аннотаций о проверенных индексах
|
|
592
|
+
// из результата успешной проверки схемой "then"
|
|
593
|
+
if (result.evaluatedIndexes) {
|
|
594
|
+
evaluatedIndexes = new Set([
|
|
595
|
+
...evaluatedIndexes,
|
|
596
|
+
...result.evaluatedIndexes,
|
|
597
|
+
]);
|
|
598
|
+
}
|
|
599
|
+
// внедрение аннотаций о проверенных свойствах
|
|
600
|
+
// из результата успешной проверки схемой "then"
|
|
601
|
+
if (result.evaluatedProperties) {
|
|
602
|
+
evaluatedProperties = new Set([
|
|
603
|
+
...evaluatedProperties,
|
|
604
|
+
...result.evaluatedProperties,
|
|
605
|
+
]);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
// если определена схема "else", то данные
|
|
610
|
+
// должны соответствовать указанной схеме
|
|
611
|
+
if (schema.else) {
|
|
612
|
+
const result = _validate(data, schema.else, {
|
|
613
|
+
...options,
|
|
614
|
+
schemaSourceUri: options.schemaSourceUri + '/else',
|
|
615
|
+
});
|
|
616
|
+
if (!result.isValid) {
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
// так как при проверке по схеме "else" данные
|
|
620
|
+
// могли измениться, выполняется обновление
|
|
621
|
+
// исходного значения
|
|
622
|
+
data = result.value;
|
|
623
|
+
// внедрение аннотаций о проверенных индексах
|
|
624
|
+
// из результата успешной проверки схемой "else"
|
|
625
|
+
if (result.evaluatedIndexes) {
|
|
626
|
+
evaluatedIndexes = new Set([
|
|
627
|
+
...evaluatedIndexes,
|
|
628
|
+
...result.evaluatedIndexes,
|
|
629
|
+
]);
|
|
630
|
+
}
|
|
631
|
+
// внедрение аннотаций о проверенных свойствах
|
|
632
|
+
// из результата успешной проверки схемой "else"
|
|
633
|
+
if (result.evaluatedProperties) {
|
|
634
|
+
evaluatedProperties = new Set([
|
|
635
|
+
...evaluatedProperties,
|
|
636
|
+
...result.evaluatedProperties,
|
|
637
|
+
]);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// type
|
|
643
|
+
if (schema.type !== undefined && data !== undefined) {
|
|
644
|
+
const result = _validateType(data, schema.type, options);
|
|
645
|
+
if (!result.isValid) {
|
|
646
|
+
return result;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// format
|
|
650
|
+
if (schema.format) {
|
|
651
|
+
const result = _validateFormat(
|
|
652
|
+
data,
|
|
653
|
+
schema.format,
|
|
654
|
+
{...OA_BUILT_IN_DATA_FORMAT_VALIDATOR_MAP, ...options.formatValidators},
|
|
655
|
+
options,
|
|
656
|
+
);
|
|
657
|
+
if (!result.isValid) {
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// const
|
|
662
|
+
if (schema.const !== undefined) {
|
|
663
|
+
if (!_areValuesEqual(schema.const, data)) {
|
|
664
|
+
return {
|
|
665
|
+
isValid: false,
|
|
666
|
+
value: data,
|
|
667
|
+
valueUri: options.dataSourceUri,
|
|
668
|
+
schemaUri: options.schemaSourceUri,
|
|
669
|
+
reason: format(
|
|
670
|
+
'Value at %v must be equal to constant %s, but %s was given.',
|
|
671
|
+
options.dataSourceUri,
|
|
672
|
+
toSpacedJson(schema.const, {truncate: 50}),
|
|
673
|
+
toSpacedJson(data, {truncate: 50}),
|
|
674
|
+
),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// enum
|
|
679
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
680
|
+
const isFoundInEnum = schema.enum.some(el => _areValuesEqual(el, data));
|
|
681
|
+
if (!isFoundInEnum) {
|
|
682
|
+
return {
|
|
683
|
+
isValid: false,
|
|
684
|
+
value: data,
|
|
685
|
+
valueUri: options.dataSourceUri,
|
|
686
|
+
schemaUri: options.schemaSourceUri,
|
|
687
|
+
reason: format(
|
|
688
|
+
'Value at %v must be one of %s, but %s was given.',
|
|
689
|
+
options.dataSourceUri,
|
|
690
|
+
toSpacedJson(schema.enum, {truncate: 50}),
|
|
691
|
+
toSpacedJson(data, {truncate: 50}),
|
|
692
|
+
),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// определение типа и проверка
|
|
697
|
+
// значения согласно данному типу
|
|
698
|
+
const currentType = inferOpenApiDataType(data, true);
|
|
699
|
+
if (currentType === OADataType.STRING) {
|
|
700
|
+
const result = _validateString(data, schema, options);
|
|
701
|
+
if (!result.isValid) {
|
|
702
|
+
return result;
|
|
703
|
+
}
|
|
704
|
+
} else if (
|
|
705
|
+
currentType === OADataType.NUMBER ||
|
|
706
|
+
currentType === OADataType.INTEGER
|
|
707
|
+
) {
|
|
708
|
+
const result = _validateNumber(data, schema, options);
|
|
709
|
+
if (!result.isValid) {
|
|
710
|
+
return result;
|
|
711
|
+
}
|
|
712
|
+
} else if (currentType === OADataType.ARRAY) {
|
|
713
|
+
const result = _validateArray(data, schema, evaluatedIndexes, options);
|
|
714
|
+
if (!result.isValid) {
|
|
715
|
+
return result;
|
|
716
|
+
}
|
|
717
|
+
// данные могли измениться во время проверки,
|
|
718
|
+
// выполняется обновление исходного значения
|
|
719
|
+
data = result.value;
|
|
720
|
+
// внедрение аннотаций о проверенных
|
|
721
|
+
// индексах из результата проверки
|
|
722
|
+
if (result.evaluatedIndexes) {
|
|
723
|
+
evaluatedIndexes = new Set([
|
|
724
|
+
...evaluatedIndexes,
|
|
725
|
+
...result.evaluatedIndexes,
|
|
726
|
+
]);
|
|
727
|
+
}
|
|
728
|
+
} else if (currentType === OADataType.OBJECT) {
|
|
729
|
+
const result = _validateObject(data, schema, evaluatedProperties, options);
|
|
730
|
+
if (!result.isValid) {
|
|
731
|
+
return result;
|
|
732
|
+
}
|
|
733
|
+
// данные могли измениться во время проверки,
|
|
734
|
+
// выполняется обновление исходного значения
|
|
735
|
+
data = result.value;
|
|
736
|
+
// внедрение аннотаций о проверенных
|
|
737
|
+
// свойствах из результата проверки
|
|
738
|
+
if (result.evaluatedProperties) {
|
|
739
|
+
evaluatedProperties = new Set([
|
|
740
|
+
...evaluatedProperties,
|
|
741
|
+
...result.evaluatedProperties,
|
|
742
|
+
]);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
isValid: true,
|
|
748
|
+
value: data,
|
|
749
|
+
valueUri: options.dataSourceUri,
|
|
750
|
+
schemaUri: options.schemaSourceUri,
|
|
751
|
+
evaluatedIndexes,
|
|
752
|
+
evaluatedProperties,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Parse JSON.
|
|
758
|
+
*
|
|
759
|
+
* @param {*} data
|
|
760
|
+
* @param {string|string[]} expectedType
|
|
761
|
+
* @returns {*}
|
|
762
|
+
*/
|
|
763
|
+
function _parseJson(data, expectedType) {
|
|
764
|
+
if (typeof data !== 'string') {
|
|
765
|
+
return data;
|
|
766
|
+
}
|
|
767
|
+
const types = Array.isArray(expectedType) ? expectedType : [expectedType];
|
|
768
|
+
// json спецификация RFC 8259 разрешает
|
|
769
|
+
// пробельные символы перед началом значения
|
|
770
|
+
const trimmedData = data.trim();
|
|
771
|
+
// разбор json строки должен выполняться только
|
|
772
|
+
// при наличии совместимого содержания с указанными
|
|
773
|
+
// в схеме типами данных
|
|
774
|
+
/* prettier-ignore */
|
|
775
|
+
if (
|
|
776
|
+
(types.includes(OADataType.ARRAY) && trimmedData[0] === '[') ||
|
|
777
|
+
(types.includes(OADataType.OBJECT) && trimmedData[0] === '{') ||
|
|
778
|
+
(types.includes(OADataType.INTEGER) && JSON_INTEGER_REGEXP.test(trimmedData)) ||
|
|
779
|
+
(types.includes(OADataType.NUMBER) && JSON_NUMBER_REGEXP.test(trimmedData)) ||
|
|
780
|
+
(types.includes(OADataType.BOOLEAN) && (trimmedData === 'true' || trimmedData === 'false')) ||
|
|
781
|
+
(types.includes(OADataType.NULL) && trimmedData === 'null')
|
|
782
|
+
) {
|
|
783
|
+
try {
|
|
784
|
+
return JSON.parse(trimmedData);
|
|
785
|
+
} catch {
|
|
786
|
+
return data;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return data;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Coerce type.
|
|
794
|
+
*
|
|
795
|
+
* @param {*} data
|
|
796
|
+
* @param {string|string[]} expectedType
|
|
797
|
+
* @returns {*}
|
|
798
|
+
*/
|
|
799
|
+
function _coerceType(data, expectedType) {
|
|
800
|
+
const types = Array.isArray(expectedType) ? expectedType : [expectedType];
|
|
801
|
+
const actualType = inferOpenApiDataType(data, true);
|
|
802
|
+
// если тип уже соответствует ожиданию,
|
|
803
|
+
// то значение возвращается без изменений
|
|
804
|
+
if (types.includes(actualType)) {
|
|
805
|
+
return data;
|
|
806
|
+
}
|
|
807
|
+
// если текущий тип является "integer",
|
|
808
|
+
// а ожидаемый "number" то значение
|
|
809
|
+
// возвращается без изменений
|
|
810
|
+
if (actualType === OADataType.INTEGER && types.includes(OADataType.NUMBER)) {
|
|
811
|
+
return data;
|
|
812
|
+
}
|
|
813
|
+
for (const type of types) {
|
|
814
|
+
switch (type) {
|
|
815
|
+
case OADataType.STRING: {
|
|
816
|
+
if (typeof data === 'number' || typeof data === 'boolean') {
|
|
817
|
+
return String(data);
|
|
818
|
+
}
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
case OADataType.NUMBER:
|
|
822
|
+
case OADataType.INTEGER: {
|
|
823
|
+
if (typeof data === 'string' && data.trim() !== '') {
|
|
824
|
+
const num = Number(data);
|
|
825
|
+
if (!Number.isNaN(num)) {
|
|
826
|
+
if (type === OADataType.INTEGER && Number.isInteger(num)) {
|
|
827
|
+
return num;
|
|
828
|
+
}
|
|
829
|
+
if (type === OADataType.NUMBER) {
|
|
830
|
+
return num;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
case OADataType.BOOLEAN: {
|
|
837
|
+
if (data === 'true') {
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
if (data === 'false') {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return data;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Validate type.
|
|
852
|
+
*
|
|
853
|
+
* @param {unknown} data
|
|
854
|
+
* @param {string|string[]} expectedType
|
|
855
|
+
* @param {object} options
|
|
856
|
+
* @returns {object}
|
|
857
|
+
*/
|
|
858
|
+
function _validateType(data, expectedType, options) {
|
|
859
|
+
const allowedTypes = [expectedType].flat();
|
|
860
|
+
const actualType = inferOpenApiDataType(data, true);
|
|
861
|
+
const isMatch = allowedTypes.some(allowedType => {
|
|
862
|
+
if (allowedType === actualType) {
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
865
|
+
if (
|
|
866
|
+
allowedType === OADataType.NUMBER &&
|
|
867
|
+
actualType === OADataType.INTEGER
|
|
868
|
+
) {
|
|
869
|
+
return true;
|
|
870
|
+
}
|
|
871
|
+
return false;
|
|
872
|
+
});
|
|
873
|
+
if (allowedTypes.length && !isMatch) {
|
|
874
|
+
return {
|
|
875
|
+
isValid: false,
|
|
876
|
+
value: data,
|
|
877
|
+
valueUri: options.dataSourceUri,
|
|
878
|
+
schemaUri: options.schemaSourceUri,
|
|
879
|
+
reason: format(
|
|
880
|
+
'Value at %v must be %s, but %s was given.',
|
|
881
|
+
options.dataSourceUri,
|
|
882
|
+
allowedTypes.map(v => toPascalCase(v)).join(' or '),
|
|
883
|
+
toPascalCase(actualType),
|
|
884
|
+
),
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
isValid: true,
|
|
889
|
+
value: data,
|
|
890
|
+
valueUri: options.dataSourceUri,
|
|
891
|
+
schemaUri: options.schemaSourceUri,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Validate format.
|
|
897
|
+
*
|
|
898
|
+
* @param {*} data
|
|
899
|
+
* @param {string} expectedFormat
|
|
900
|
+
* @param {object} validatorsMap
|
|
901
|
+
* @param {object} options
|
|
902
|
+
* @returns {object}
|
|
903
|
+
*/
|
|
904
|
+
function _validateFormat(data, expectedFormat, validatorsMap, options) {
|
|
905
|
+
const validator = validatorsMap[expectedFormat];
|
|
906
|
+
if (validator) {
|
|
907
|
+
const isValid = validator(data);
|
|
908
|
+
if (!isValid) {
|
|
909
|
+
return {
|
|
910
|
+
isValid: false,
|
|
911
|
+
value: data,
|
|
912
|
+
valueUri: options.dataSourceUri,
|
|
913
|
+
schemaUri: options.schemaSourceUri,
|
|
914
|
+
reason: format(
|
|
915
|
+
'Value at %v must match format %v, but %s was given.',
|
|
916
|
+
options.dataSourceUri,
|
|
917
|
+
expectedFormat,
|
|
918
|
+
toSpacedJson(data, {truncate: 50}),
|
|
919
|
+
),
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
// если валидатора нет, спецификация OpenAPI
|
|
924
|
+
// рекомендует игнорировать неизвестные форматы
|
|
925
|
+
return {
|
|
926
|
+
isValid: true,
|
|
927
|
+
value: data,
|
|
928
|
+
valueUri: options.dataSourceUri,
|
|
929
|
+
schemaUri: options.schemaSourceUri,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Deep equality check for "enum" and "uniqueItems" option.
|
|
935
|
+
*
|
|
936
|
+
* @param {unknown} a
|
|
937
|
+
* @param {unknown} b
|
|
938
|
+
* @returns {boolean}
|
|
939
|
+
*/
|
|
940
|
+
function _areValuesEqual(a, b) {
|
|
941
|
+
if (a === b) {
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
if (
|
|
945
|
+
typeof a !== 'object' ||
|
|
946
|
+
a === null ||
|
|
947
|
+
typeof b !== 'object' ||
|
|
948
|
+
b === null
|
|
949
|
+
) {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
// Date
|
|
953
|
+
if (a instanceof Date && b instanceof Date) {
|
|
954
|
+
return a.getTime() === b.getTime();
|
|
955
|
+
}
|
|
956
|
+
// RegExp
|
|
957
|
+
if (a instanceof RegExp && b instanceof RegExp) {
|
|
958
|
+
return a.toString() === b.toString();
|
|
959
|
+
}
|
|
960
|
+
// Array
|
|
961
|
+
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
if (Array.isArray(a)) {
|
|
965
|
+
if (a.length !== b.length) {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
for (let i = 0; i < a.length; i++) {
|
|
969
|
+
if (!_areValuesEqual(a[i], b[i])) {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
// Object
|
|
976
|
+
const keysA = Object.keys(a);
|
|
977
|
+
const keysB = Object.keys(b);
|
|
978
|
+
if (keysA.length !== keysB.length) {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
for (const key of keysA) {
|
|
982
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
if (!_areValuesEqual(a[key], b[key])) {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Validate string.
|
|
994
|
+
*
|
|
995
|
+
* @param {string} data
|
|
996
|
+
* @param {object} schema
|
|
997
|
+
* @param {object} options
|
|
998
|
+
* @returns {object}
|
|
999
|
+
*/
|
|
1000
|
+
function _validateString(data, schema, options) {
|
|
1001
|
+
// minLength
|
|
1002
|
+
if (typeof schema.minLength === 'number') {
|
|
1003
|
+
const dataLength = countUnicode(data);
|
|
1004
|
+
if (dataLength < schema.minLength) {
|
|
1005
|
+
return {
|
|
1006
|
+
isValid: false,
|
|
1007
|
+
value: data,
|
|
1008
|
+
valueUri: options.dataSourceUri,
|
|
1009
|
+
schemaUri: options.schemaSourceUri,
|
|
1010
|
+
reason: format(
|
|
1011
|
+
'String at %v must be at least %d characters long, but %d was given.',
|
|
1012
|
+
options.dataSourceUri,
|
|
1013
|
+
schema.minLength,
|
|
1014
|
+
dataLength,
|
|
1015
|
+
),
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
// maxLength
|
|
1020
|
+
if (typeof schema.maxLength === 'number') {
|
|
1021
|
+
const dataLength = countUnicode(data);
|
|
1022
|
+
if (dataLength > schema.maxLength) {
|
|
1023
|
+
return {
|
|
1024
|
+
isValid: false,
|
|
1025
|
+
value: data,
|
|
1026
|
+
valueUri: options.dataSourceUri,
|
|
1027
|
+
schemaUri: options.schemaSourceUri,
|
|
1028
|
+
reason: format(
|
|
1029
|
+
'String at %v must be at most %d characters long, but %d was given.',
|
|
1030
|
+
options.dataSourceUri,
|
|
1031
|
+
schema.maxLength,
|
|
1032
|
+
dataLength,
|
|
1033
|
+
),
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
// pattern
|
|
1038
|
+
if (schema.pattern) {
|
|
1039
|
+
let regex;
|
|
1040
|
+
try {
|
|
1041
|
+
regex = new RegExp(schema.pattern, 'u');
|
|
1042
|
+
} catch {
|
|
1043
|
+
throw new InvalidArgumentError(
|
|
1044
|
+
'Schema pattern %v at %v has an invalid expression.',
|
|
1045
|
+
schema.pattern,
|
|
1046
|
+
options.schemaSourceUri,
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
if (!regex.test(data)) {
|
|
1050
|
+
const reString =
|
|
1051
|
+
schema.pattern instanceof RegExp
|
|
1052
|
+
? String(schema.pattern)
|
|
1053
|
+
: schema.pattern;
|
|
1054
|
+
return {
|
|
1055
|
+
isValid: false,
|
|
1056
|
+
value: data,
|
|
1057
|
+
valueUri: options.dataSourceUri,
|
|
1058
|
+
schemaUri: options.schemaSourceUri,
|
|
1059
|
+
reason: format(
|
|
1060
|
+
'String at %v must match pattern %v, but %s was given.',
|
|
1061
|
+
options.dataSourceUri,
|
|
1062
|
+
reString,
|
|
1063
|
+
toSpacedJson(data, {truncate: 50}),
|
|
1064
|
+
),
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
isValid: true,
|
|
1070
|
+
value: data,
|
|
1071
|
+
valueUri: options.dataSourceUri,
|
|
1072
|
+
schemaUri: options.schemaSourceUri,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Validate number/integer.
|
|
1078
|
+
*
|
|
1079
|
+
* @param {number} data
|
|
1080
|
+
* @param {object} schema
|
|
1081
|
+
* @param {object} options
|
|
1082
|
+
* @returns {object}
|
|
1083
|
+
*/
|
|
1084
|
+
function _validateNumber(data, schema, options) {
|
|
1085
|
+
// minimum
|
|
1086
|
+
if (typeof schema.minimum === 'number') {
|
|
1087
|
+
if (data < schema.minimum) {
|
|
1088
|
+
return {
|
|
1089
|
+
isValid: false,
|
|
1090
|
+
value: data,
|
|
1091
|
+
valueUri: options.dataSourceUri,
|
|
1092
|
+
schemaUri: options.schemaSourceUri,
|
|
1093
|
+
reason: format(
|
|
1094
|
+
'Value at %v must be greater than or equal to %d, but %v was given.',
|
|
1095
|
+
options.dataSourceUri,
|
|
1096
|
+
schema.minimum,
|
|
1097
|
+
data,
|
|
1098
|
+
),
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
// maximum
|
|
1103
|
+
if (typeof schema.maximum === 'number') {
|
|
1104
|
+
if (data > schema.maximum) {
|
|
1105
|
+
return {
|
|
1106
|
+
isValid: false,
|
|
1107
|
+
value: data,
|
|
1108
|
+
valueUri: options.dataSourceUri,
|
|
1109
|
+
schemaUri: options.schemaSourceUri,
|
|
1110
|
+
reason: format(
|
|
1111
|
+
'Value at %v must be less than or equal to %d, but %v was given.',
|
|
1112
|
+
options.dataSourceUri,
|
|
1113
|
+
schema.maximum,
|
|
1114
|
+
data,
|
|
1115
|
+
),
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
// exclusiveMinimum
|
|
1120
|
+
if (typeof schema.exclusiveMinimum === 'number') {
|
|
1121
|
+
if (data <= schema.exclusiveMinimum) {
|
|
1122
|
+
return {
|
|
1123
|
+
isValid: false,
|
|
1124
|
+
value: data,
|
|
1125
|
+
valueUri: options.dataSourceUri,
|
|
1126
|
+
schemaUri: options.schemaSourceUri,
|
|
1127
|
+
reason: format(
|
|
1128
|
+
'Value at %v must be greater than %d, but %v was given.',
|
|
1129
|
+
options.dataSourceUri,
|
|
1130
|
+
schema.exclusiveMinimum,
|
|
1131
|
+
data,
|
|
1132
|
+
),
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
// exclusiveMaximum
|
|
1137
|
+
if (typeof schema.exclusiveMaximum === 'number') {
|
|
1138
|
+
if (data >= schema.exclusiveMaximum) {
|
|
1139
|
+
return {
|
|
1140
|
+
isValid: false,
|
|
1141
|
+
value: data,
|
|
1142
|
+
valueUri: options.dataSourceUri,
|
|
1143
|
+
schemaUri: options.schemaSourceUri,
|
|
1144
|
+
reason: format(
|
|
1145
|
+
'Value at %v must be less than %d, but %v was given.',
|
|
1146
|
+
options.dataSourceUri,
|
|
1147
|
+
schema.exclusiveMaximum,
|
|
1148
|
+
data,
|
|
1149
|
+
),
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// multipleOf
|
|
1154
|
+
if (typeof schema.multipleOf === 'number') {
|
|
1155
|
+
// если "multipleOf" равен или меньше 0,
|
|
1156
|
+
// то выбрасывается ошибка
|
|
1157
|
+
if (schema.multipleOf <= 0) {
|
|
1158
|
+
throw new InvalidArgumentError(
|
|
1159
|
+
'Schema keyword "multipleOf" must be a Number strictly ' +
|
|
1160
|
+
'greater than 0, but %v was given.',
|
|
1161
|
+
schema.multipleOf,
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
const quotient = data / schema.multipleOf;
|
|
1165
|
+
// EPSILON (1e-9 достаточно для большинства API задач)
|
|
1166
|
+
// используется для компенсации ошибок округления IEEE 754,
|
|
1167
|
+
// например, чтобы 0.3 считалось кратным 0.1, хотя в бинарном
|
|
1168
|
+
// виде 0.3 / 0.1 дает 2.9999999999999996
|
|
1169
|
+
const isInteger = Math.abs(quotient - Math.round(quotient)) < 1e-9;
|
|
1170
|
+
if (!isInteger) {
|
|
1171
|
+
return {
|
|
1172
|
+
isValid: false,
|
|
1173
|
+
value: data,
|
|
1174
|
+
valueUri: options.dataSourceUri,
|
|
1175
|
+
schemaUri: options.schemaSourceUri,
|
|
1176
|
+
reason: format(
|
|
1177
|
+
'Value at %v must be a multiple of %v, but %v was given.',
|
|
1178
|
+
options.dataSourceUri,
|
|
1179
|
+
schema.multipleOf,
|
|
1180
|
+
data,
|
|
1181
|
+
),
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return {
|
|
1186
|
+
isValid: true,
|
|
1187
|
+
value: data,
|
|
1188
|
+
valueUri: options.dataSourceUri,
|
|
1189
|
+
schemaUri: options.schemaSourceUri,
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Validate array.
|
|
1195
|
+
*
|
|
1196
|
+
* @param {Array} data
|
|
1197
|
+
* @param {object} schema
|
|
1198
|
+
* @param {Set} evaluatedIndexes
|
|
1199
|
+
* @param {object} options
|
|
1200
|
+
* @returns {object}
|
|
1201
|
+
*/
|
|
1202
|
+
function _validateArray(data, schema, evaluatedIndexes, options) {
|
|
1203
|
+
// чтобы избежать мутации набора проверенных
|
|
1204
|
+
// индексов, выполняется копирование набора
|
|
1205
|
+
evaluatedIndexes = new Set(evaluatedIndexes);
|
|
1206
|
+
// чтобы избежать проблем с производительностью,
|
|
1207
|
+
// копирование данных будет выполнено только в момент
|
|
1208
|
+
// изменения элементов массива
|
|
1209
|
+
const originalData = data;
|
|
1210
|
+
// если определен "prefixItems", то "items" проверяет
|
|
1211
|
+
// только хвост массива, по этой причине, необходимо
|
|
1212
|
+
// отслеживать последний проверенный элемент
|
|
1213
|
+
let validationStartIndex = 0;
|
|
1214
|
+
// prefixItems
|
|
1215
|
+
if (Array.isArray(schema.prefixItems)) {
|
|
1216
|
+
// при проверке кортежа, элементов может быть меньше,
|
|
1217
|
+
// чем схем в "prefixItems", это является допустимым
|
|
1218
|
+
// (проверка длины выполняется через minItems).
|
|
1219
|
+
const loopLimit = Math.min(schema.prefixItems.length, data.length);
|
|
1220
|
+
for (let i = 0; i < loopLimit; i++) {
|
|
1221
|
+
const item = data[i];
|
|
1222
|
+
const subSchema = schema.prefixItems[i];
|
|
1223
|
+
const result = _validate(item, subSchema, {
|
|
1224
|
+
...options,
|
|
1225
|
+
dataSourceUri: `${options.dataSourceUri}/${i}`,
|
|
1226
|
+
schemaSourceUri: `${options.schemaSourceUri}/prefixItems/${i}`,
|
|
1227
|
+
});
|
|
1228
|
+
if (!result.isValid) {
|
|
1229
|
+
return result;
|
|
1230
|
+
}
|
|
1231
|
+
// если элемент массива изменился, то выполняется
|
|
1232
|
+
// обновление исходных данных без мутации оригинала
|
|
1233
|
+
// (поверхностное копирование массива)
|
|
1234
|
+
if (item !== result.value) {
|
|
1235
|
+
if (data === originalData) {
|
|
1236
|
+
data = [...originalData];
|
|
1237
|
+
}
|
|
1238
|
+
data[i] = result.value;
|
|
1239
|
+
}
|
|
1240
|
+
// если проверка элемента выполнена успешно,
|
|
1241
|
+
// то элемент помечается как проверенный
|
|
1242
|
+
evaluatedIndexes.add(i);
|
|
1243
|
+
}
|
|
1244
|
+
// свойство схемы "items" должно применяться
|
|
1245
|
+
// только к элементам после проверки "prefixItems"
|
|
1246
|
+
validationStartIndex = schema.prefixItems.length;
|
|
1247
|
+
}
|
|
1248
|
+
// items
|
|
1249
|
+
if (schema.items !== undefined) {
|
|
1250
|
+
// схема "items" проверяет только элементы,
|
|
1251
|
+
// выходящие за пределы длины "prefixItems"
|
|
1252
|
+
for (let i = validationStartIndex; i < data.length; i++) {
|
|
1253
|
+
const item = data[i];
|
|
1254
|
+
const result = _validate(item, schema.items, {
|
|
1255
|
+
...options,
|
|
1256
|
+
dataSourceUri: `${options.dataSourceUri}/${i}`,
|
|
1257
|
+
schemaSourceUri: `${options.schemaSourceUri}/items`,
|
|
1258
|
+
});
|
|
1259
|
+
if (!result.isValid) {
|
|
1260
|
+
return result;
|
|
1261
|
+
}
|
|
1262
|
+
// если элемент массива изменился, то выполняется
|
|
1263
|
+
// обновление исходных данных без мутации оригинала
|
|
1264
|
+
// (поверхностное копирование массива)
|
|
1265
|
+
if (item !== result.value) {
|
|
1266
|
+
if (data === originalData) {
|
|
1267
|
+
data = [...originalData];
|
|
1268
|
+
}
|
|
1269
|
+
data[i] = result.value;
|
|
1270
|
+
}
|
|
1271
|
+
// если проверка элемента выполнена успешно,
|
|
1272
|
+
// то элемент помечается как проверенный
|
|
1273
|
+
evaluatedIndexes.add(i);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
// minItems
|
|
1277
|
+
if (typeof schema.minItems === 'number') {
|
|
1278
|
+
if (data.length < schema.minItems) {
|
|
1279
|
+
return {
|
|
1280
|
+
isValid: false,
|
|
1281
|
+
value: data,
|
|
1282
|
+
valueUri: options.dataSourceUri,
|
|
1283
|
+
schemaUri: options.schemaSourceUri,
|
|
1284
|
+
reason: format(
|
|
1285
|
+
'Array at %v must contain at least %d items, but %d was given.',
|
|
1286
|
+
options.dataSourceUri,
|
|
1287
|
+
schema.minItems,
|
|
1288
|
+
data.length,
|
|
1289
|
+
),
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
// maxItems
|
|
1294
|
+
if (typeof schema.maxItems === 'number') {
|
|
1295
|
+
if (data.length > schema.maxItems) {
|
|
1296
|
+
return {
|
|
1297
|
+
isValid: false,
|
|
1298
|
+
value: data,
|
|
1299
|
+
valueUri: options.dataSourceUri,
|
|
1300
|
+
schemaUri: options.schemaSourceUri,
|
|
1301
|
+
reason: format(
|
|
1302
|
+
'Array at %v must contain at most %d items, but %d was given.',
|
|
1303
|
+
options.dataSourceUri,
|
|
1304
|
+
schema.maxItems,
|
|
1305
|
+
data.length,
|
|
1306
|
+
),
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
// uniqueItems
|
|
1311
|
+
if (schema.uniqueItems === true) {
|
|
1312
|
+
// для проверки уникальности используется массив
|
|
1313
|
+
// уже просмотренных элементов и глубокое сравнение
|
|
1314
|
+
// для каждого нового элемента
|
|
1315
|
+
for (let i = 0; i < data.length; i++) {
|
|
1316
|
+
// алгоритм O(N^2) является приемлемым для проверки
|
|
1317
|
+
// конфигураций или запросов типичного размера
|
|
1318
|
+
for (let j = i + 1; j < data.length; j++) {
|
|
1319
|
+
if (_areValuesEqual(data[i], data[j])) {
|
|
1320
|
+
return {
|
|
1321
|
+
isValid: false,
|
|
1322
|
+
value: data,
|
|
1323
|
+
valueUri: options.dataSourceUri,
|
|
1324
|
+
schemaUri: options.schemaSourceUri,
|
|
1325
|
+
reason: format(
|
|
1326
|
+
'Array at %v must contain unique items, ' +
|
|
1327
|
+
'but items at index %d and %d are identical.',
|
|
1328
|
+
options.dataSourceUri,
|
|
1329
|
+
i,
|
|
1330
|
+
j,
|
|
1331
|
+
),
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
// contains / minContains / maxContains
|
|
1338
|
+
if (schema.contains) {
|
|
1339
|
+
let matches = 0;
|
|
1340
|
+
for (let i = 0, l = data.length; i < l; i++) {
|
|
1341
|
+
const item = data[i];
|
|
1342
|
+
// проверка каждого элемента на соответствие схеме "contains",
|
|
1343
|
+
// используется опция "silent", чтобы не выбрасывать ошибку,
|
|
1344
|
+
// а только проверить факт соответствия
|
|
1345
|
+
const {isValid} = _validate(item, schema.contains, {
|
|
1346
|
+
...options,
|
|
1347
|
+
silent: true,
|
|
1348
|
+
schemaSourceUri: options.schemaSourceUri + '/contains',
|
|
1349
|
+
});
|
|
1350
|
+
if (isValid) {
|
|
1351
|
+
matches++;
|
|
1352
|
+
// если проверка элемента выполнена успешно,
|
|
1353
|
+
// то элемент помечается как проверенный
|
|
1354
|
+
evaluatedIndexes.add(i);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
// minContains
|
|
1358
|
+
// если "minContains" не указан, но есть "contains", то по умолчанию
|
|
1359
|
+
// он равен 1, но если "minContains" явно равен 0, то проверка
|
|
1360
|
+
// на соответствие "хотя бы один" отключается
|
|
1361
|
+
const minContains =
|
|
1362
|
+
typeof schema.minContains === 'number' ? schema.minContains : 1;
|
|
1363
|
+
if (matches < minContains) {
|
|
1364
|
+
return {
|
|
1365
|
+
isValid: false,
|
|
1366
|
+
value: data,
|
|
1367
|
+
valueUri: options.dataSourceUri,
|
|
1368
|
+
schemaUri: options.schemaSourceUri + '/contains',
|
|
1369
|
+
reason: format(
|
|
1370
|
+
'Array at %v must contain at least %d item(s) matching ' +
|
|
1371
|
+
'the "contains" schema, but %d was found.',
|
|
1372
|
+
options.dataSourceUri,
|
|
1373
|
+
minContains,
|
|
1374
|
+
matches,
|
|
1375
|
+
),
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
// maxContains
|
|
1379
|
+
if (typeof schema.maxContains === 'number') {
|
|
1380
|
+
if (matches > schema.maxContains) {
|
|
1381
|
+
return {
|
|
1382
|
+
isValid: false,
|
|
1383
|
+
value: data,
|
|
1384
|
+
valueUri: options.dataSourceUri,
|
|
1385
|
+
schemaUri: options.schemaSourceUri + '/contains',
|
|
1386
|
+
reason: format(
|
|
1387
|
+
'Array at %v must contain at most %d item(s) matching ' +
|
|
1388
|
+
'the "contains" schema, but %d was found.',
|
|
1389
|
+
options.dataSourceUri,
|
|
1390
|
+
schema.maxContains,
|
|
1391
|
+
matches,
|
|
1392
|
+
),
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
// unevaluatedItems
|
|
1398
|
+
if (schema.unevaluatedItems !== undefined) {
|
|
1399
|
+
// ключевое слово применяется к элементам, которые еще не были
|
|
1400
|
+
// проверены ключевыми словами "prefixItems", "items", "contains"
|
|
1401
|
+
// или другими аппликаторами
|
|
1402
|
+
for (let i = 0, l = data.length; i < l; i++) {
|
|
1403
|
+
// если индекс уже есть в сете, значит данный
|
|
1404
|
+
// элемент массива известен и был проверен ранее
|
|
1405
|
+
if (evaluatedIndexes.has(i)) {
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
const result = _validate(data[i], schema.unevaluatedItems, {
|
|
1409
|
+
...options,
|
|
1410
|
+
dataSourceUri: `${options.dataSourceUri}/${i}`,
|
|
1411
|
+
schemaSourceUri: `${options.schemaSourceUri}/unevaluatedItems`,
|
|
1412
|
+
});
|
|
1413
|
+
if (!result.isValid) {
|
|
1414
|
+
// так как логическое значение false для данной опции
|
|
1415
|
+
// является наиболее частым случаем, то возвращается
|
|
1416
|
+
// особое сообщение
|
|
1417
|
+
if (schema.unevaluatedItems === false) {
|
|
1418
|
+
return {
|
|
1419
|
+
isValid: false,
|
|
1420
|
+
value: data[i],
|
|
1421
|
+
valueUri: `${options.dataSourceUri}/${i}`,
|
|
1422
|
+
schemaUri: `${options.schemaSourceUri}/unevaluatedItems`,
|
|
1423
|
+
reason: format(
|
|
1424
|
+
'Array at %v has unevaluated item at index %d, ' +
|
|
1425
|
+
'which is not allowed.',
|
|
1426
|
+
options.dataSourceUri,
|
|
1427
|
+
i,
|
|
1428
|
+
),
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
return result;
|
|
1432
|
+
}
|
|
1433
|
+
// если элемент массива изменился, то выполняется
|
|
1434
|
+
// обновление исходных данных без мутации оригинала
|
|
1435
|
+
// (поверхностное копирование массива)
|
|
1436
|
+
if (data[i] !== result.value) {
|
|
1437
|
+
if (data === originalData) {
|
|
1438
|
+
data = [...originalData];
|
|
1439
|
+
}
|
|
1440
|
+
data[i] = result.value;
|
|
1441
|
+
}
|
|
1442
|
+
// если проверка элемента выполнена успешно,
|
|
1443
|
+
// то элемент помечается как проверенный,
|
|
1444
|
+
// хотя на этом уровне это уже последний шаг
|
|
1445
|
+
evaluatedIndexes.add(i);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
// stripUnknown
|
|
1449
|
+
if (options.stripUnknown === true) {
|
|
1450
|
+
// если на данном этапе есть хотя бы один проверенный
|
|
1451
|
+
// индекс элемента, то это значит, что структура массива
|
|
1452
|
+
// определена схемой, и можно выполнить удаление
|
|
1453
|
+
// неизвестных элементов
|
|
1454
|
+
if (evaluatedIndexes.size > 0) {
|
|
1455
|
+
for (let i = 0, l = data.length; i < l; i++) {
|
|
1456
|
+
if (evaluatedIndexes.has(i)) {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
// чтобы избежать мутации исходных данных,
|
|
1460
|
+
// выполняется копирование оригинала
|
|
1461
|
+
if (data === originalData) {
|
|
1462
|
+
data = [...originalData];
|
|
1463
|
+
}
|
|
1464
|
+
data.splice(i, 1);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
isValid: true,
|
|
1470
|
+
value: data,
|
|
1471
|
+
valueUri: options.dataSourceUri,
|
|
1472
|
+
schemaUri: options.schemaSourceUri,
|
|
1473
|
+
evaluatedIndexes,
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Validate object.
|
|
1479
|
+
*
|
|
1480
|
+
* @param {object} data
|
|
1481
|
+
* @param {object} schema
|
|
1482
|
+
* @param {Set} evaluatedProperties
|
|
1483
|
+
* @param {object} options
|
|
1484
|
+
* @returns {object}
|
|
1485
|
+
*/
|
|
1486
|
+
function _validateObject(data, schema, evaluatedProperties, options) {
|
|
1487
|
+
// чтобы избежать мутации набора проверенных
|
|
1488
|
+
// свойств, выполняется копирование набора
|
|
1489
|
+
evaluatedProperties = new Set(evaluatedProperties);
|
|
1490
|
+
// чтобы избежать проблем с производительностью,
|
|
1491
|
+
// копирование данных будет выполнено только в момент
|
|
1492
|
+
// изменения элементов массива
|
|
1493
|
+
const originalData = data;
|
|
1494
|
+
// maxProperties
|
|
1495
|
+
if (typeof schema.maxProperties === 'number') {
|
|
1496
|
+
const propsNumber = Object.keys(data).length;
|
|
1497
|
+
if (propsNumber > schema.maxProperties) {
|
|
1498
|
+
return {
|
|
1499
|
+
isValid: false,
|
|
1500
|
+
value: data,
|
|
1501
|
+
valueUri: options.dataSourceUri,
|
|
1502
|
+
schemaUri: options.schemaSourceUri,
|
|
1503
|
+
reason: format(
|
|
1504
|
+
'Object at %v must contain at most %d properties, but %d was given.',
|
|
1505
|
+
options.dataSourceUri,
|
|
1506
|
+
schema.maxProperties,
|
|
1507
|
+
propsNumber,
|
|
1508
|
+
),
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
// minProperties
|
|
1513
|
+
if (typeof schema.minProperties === 'number') {
|
|
1514
|
+
const propsNumber = Object.keys(data).length;
|
|
1515
|
+
if (propsNumber < schema.minProperties) {
|
|
1516
|
+
return {
|
|
1517
|
+
isValid: false,
|
|
1518
|
+
value: data,
|
|
1519
|
+
valueUri: options.dataSourceUri,
|
|
1520
|
+
schemaUri: options.schemaSourceUri,
|
|
1521
|
+
reason: format(
|
|
1522
|
+
'Object at %v must contain at least %d properties, but %d was given.',
|
|
1523
|
+
options.dataSourceUri,
|
|
1524
|
+
schema.minProperties,
|
|
1525
|
+
propsNumber,
|
|
1526
|
+
),
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
// required
|
|
1531
|
+
if (Array.isArray(schema.required)) {
|
|
1532
|
+
for (const propName of schema.required) {
|
|
1533
|
+
if (data[propName] === undefined) {
|
|
1534
|
+
// прежде чем выбросить ошибку, что обязательное свойство
|
|
1535
|
+
// не определено, требуется проверить режим доступа, так
|
|
1536
|
+
// как свойство может быть исключено в данном режиме
|
|
1537
|
+
if (options.accessMode) {
|
|
1538
|
+
const propSchema =
|
|
1539
|
+
schema.properties !== null &&
|
|
1540
|
+
typeof schema.properties === 'object' &&
|
|
1541
|
+
schema.properties[propName] !== null &&
|
|
1542
|
+
typeof schema.properties[propName] === 'object' &&
|
|
1543
|
+
schema.properties[propName];
|
|
1544
|
+
// проверка на наличие исключающего ключевого
|
|
1545
|
+
// слова "readOnly" или "writeOnly" выполняется,
|
|
1546
|
+
// только если определена схема свойства
|
|
1547
|
+
if (propSchema) {
|
|
1548
|
+
if (
|
|
1549
|
+
(options.accessMode === OAAccessMode.WRITE &&
|
|
1550
|
+
propSchema.readOnly === true) ||
|
|
1551
|
+
(options.accessMode === OAAccessMode.READ &&
|
|
1552
|
+
propSchema.writeOnly === true)
|
|
1553
|
+
) {
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
const propValue = data[propName];
|
|
1559
|
+
return {
|
|
1560
|
+
isValid: false,
|
|
1561
|
+
value: propValue,
|
|
1562
|
+
valueUri: `${options.dataSourceUri}/${propName}`,
|
|
1563
|
+
schemaUri: options.schemaSourceUri,
|
|
1564
|
+
reason: format(
|
|
1565
|
+
'Property %v at %v is required, but %v was given.',
|
|
1566
|
+
propName,
|
|
1567
|
+
options.dataSourceUri,
|
|
1568
|
+
propValue,
|
|
1569
|
+
),
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
// dependentRequired
|
|
1575
|
+
if (
|
|
1576
|
+
schema.dependentRequired &&
|
|
1577
|
+
typeof schema.dependentRequired === 'object' &&
|
|
1578
|
+
!Array.isArray(schema.dependentRequired)
|
|
1579
|
+
) {
|
|
1580
|
+
// формат конфигурации {"propertyA": ["propertyB", "propertyC"]}
|
|
1581
|
+
// если есть "propertyA", то "propertyB" и "propertyC" тоже обязательны
|
|
1582
|
+
for (const triggerProp of Object.keys(schema.dependentRequired)) {
|
|
1583
|
+
const requiredProps = schema.dependentRequired[triggerProp];
|
|
1584
|
+
// если свойство-триггер присутствует в данных
|
|
1585
|
+
if (data[triggerProp] !== undefined) {
|
|
1586
|
+
if (Array.isArray(requiredProps)) {
|
|
1587
|
+
for (const requiredProp of requiredProps) {
|
|
1588
|
+
if (data[requiredProp] === undefined) {
|
|
1589
|
+
// прежде чем выбросить ошибку, что обязательное свойство
|
|
1590
|
+
// не определено, требуется проверить режим доступа, так
|
|
1591
|
+
// как свойство может быть исключено в данном режиме
|
|
1592
|
+
if (options.accessMode) {
|
|
1593
|
+
const requiredPropSchema =
|
|
1594
|
+
schema.properties !== null &&
|
|
1595
|
+
typeof schema.properties === 'object' &&
|
|
1596
|
+
schema.properties[requiredProp] !== null &&
|
|
1597
|
+
typeof schema.properties[requiredProp] === 'object' &&
|
|
1598
|
+
schema.properties[requiredProp];
|
|
1599
|
+
// проверка на наличие исключающего ключевого
|
|
1600
|
+
// слова "readOnly" или "writeOnly" выполняется,
|
|
1601
|
+
// только если определена схема обязательного
|
|
1602
|
+
// свойства
|
|
1603
|
+
if (requiredPropSchema) {
|
|
1604
|
+
if (
|
|
1605
|
+
(options.accessMode === OAAccessMode.WRITE &&
|
|
1606
|
+
requiredPropSchema.readOnly === true) ||
|
|
1607
|
+
(options.accessMode === OAAccessMode.READ &&
|
|
1608
|
+
requiredPropSchema.writeOnly === true)
|
|
1609
|
+
) {
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
return {
|
|
1615
|
+
isValid: false,
|
|
1616
|
+
value: data[requiredProp],
|
|
1617
|
+
valueUri: `${options.dataSourceUri}/${requiredProp}`,
|
|
1618
|
+
schemaUri: options.schemaSourceUri,
|
|
1619
|
+
reason: format(
|
|
1620
|
+
'Property %v at %v is required because ' +
|
|
1621
|
+
'the property %v is present.',
|
|
1622
|
+
requiredProp,
|
|
1623
|
+
options.dataSourceUri,
|
|
1624
|
+
triggerProp,
|
|
1625
|
+
),
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
// dependentSchemas
|
|
1634
|
+
if (
|
|
1635
|
+
schema.dependentSchemas &&
|
|
1636
|
+
typeof schema.dependentSchemas === 'object' &&
|
|
1637
|
+
!Array.isArray(schema.dependentSchemas)
|
|
1638
|
+
) {
|
|
1639
|
+
for (const triggerProp of Object.keys(schema.dependentSchemas)) {
|
|
1640
|
+
const subSchema = schema.dependentSchemas[triggerProp];
|
|
1641
|
+
if (data[triggerProp] !== undefined) {
|
|
1642
|
+
const result = _validate(data, subSchema, {
|
|
1643
|
+
...options,
|
|
1644
|
+
schemaSourceUri: `${options.schemaSourceUri}/dependentSchemas/${triggerProp}`,
|
|
1645
|
+
});
|
|
1646
|
+
if (!result.isValid) {
|
|
1647
|
+
return result;
|
|
1648
|
+
}
|
|
1649
|
+
// данные могли измениться во время проверки,
|
|
1650
|
+
// выполняется обновление исходного значения
|
|
1651
|
+
data = result.value;
|
|
1652
|
+
// внедрение аннотаций о проверенных свойствах
|
|
1653
|
+
// из результата успешной проверки
|
|
1654
|
+
if (result.evaluatedProperties) {
|
|
1655
|
+
evaluatedProperties = new Set([
|
|
1656
|
+
...evaluatedProperties,
|
|
1657
|
+
...result.evaluatedProperties,
|
|
1658
|
+
]);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
// propertyNames
|
|
1664
|
+
if (
|
|
1665
|
+
schema.propertyNames &&
|
|
1666
|
+
typeof schema.propertyNames === 'object' &&
|
|
1667
|
+
!Array.isArray(schema.propertyNames)
|
|
1668
|
+
) {
|
|
1669
|
+
for (const propName of Object.keys(data)) {
|
|
1670
|
+
const result = _validate(propName, schema.propertyNames, {
|
|
1671
|
+
...options,
|
|
1672
|
+
dataSourceUri: `${options.dataSourceUri}`,
|
|
1673
|
+
schemaSourceUri: `${options.schemaSourceUri}/propertyNames`,
|
|
1674
|
+
});
|
|
1675
|
+
if (!result.isValid) {
|
|
1676
|
+
return result;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
// properties
|
|
1681
|
+
if (schema.properties && typeof schema.properties === 'object') {
|
|
1682
|
+
for (const propName of Object.keys(schema.properties)) {
|
|
1683
|
+
const propSchema = schema.properties[propName];
|
|
1684
|
+
// перед началом проверки свойства, требуется
|
|
1685
|
+
// определить его наличие и необходимость
|
|
1686
|
+
// установки значения по умолчанию
|
|
1687
|
+
const isPropValuePresent = data[propName] !== undefined;
|
|
1688
|
+
const shouldApplyDefault =
|
|
1689
|
+
options.applyDefaults === true &&
|
|
1690
|
+
propSchema !== null &&
|
|
1691
|
+
typeof propSchema === 'object' &&
|
|
1692
|
+
propSchema.default !== undefined;
|
|
1693
|
+
// проверка свойства выполняется только если
|
|
1694
|
+
// свойство присутствует в данных, либо ожидается
|
|
1695
|
+
// установка значения по умолчанию
|
|
1696
|
+
if (isPropValuePresent || shouldApplyDefault) {
|
|
1697
|
+
const propValue = data[propName];
|
|
1698
|
+
const result = _validate(propValue, propSchema, {
|
|
1699
|
+
...options,
|
|
1700
|
+
dataSourceUri: `${options.dataSourceUri}/${propName}`,
|
|
1701
|
+
schemaSourceUri: `${options.schemaSourceUri}/properties/${propName}`,
|
|
1702
|
+
});
|
|
1703
|
+
if (!result.isValid) {
|
|
1704
|
+
return result;
|
|
1705
|
+
}
|
|
1706
|
+
// если значение свойства изменилось, то выполняется
|
|
1707
|
+
// обновление исходных данных без мутации оригинала
|
|
1708
|
+
// (поверхностное копирование объекта)
|
|
1709
|
+
if (data[propName] !== result.value) {
|
|
1710
|
+
if (data === originalData) {
|
|
1711
|
+
data = {...originalData};
|
|
1712
|
+
}
|
|
1713
|
+
data[propName] = result.value;
|
|
1714
|
+
}
|
|
1715
|
+
// если проверка свойства прошла успешно,
|
|
1716
|
+
// то свойство помечается как проверенное
|
|
1717
|
+
evaluatedProperties.add(propName);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
// patternProperties
|
|
1722
|
+
const passedPatternProperties = [];
|
|
1723
|
+
if (
|
|
1724
|
+
schema.patternProperties &&
|
|
1725
|
+
typeof schema.patternProperties === 'object'
|
|
1726
|
+
) {
|
|
1727
|
+
// проверка каждого имени свойств объекта
|
|
1728
|
+
// на соответствие регулярным выражениям
|
|
1729
|
+
for (const pattern of Object.keys(schema.patternProperties)) {
|
|
1730
|
+
const patternSchema = schema.patternProperties[pattern];
|
|
1731
|
+
let regex;
|
|
1732
|
+
try {
|
|
1733
|
+
regex = new RegExp(pattern, 'u');
|
|
1734
|
+
} catch {
|
|
1735
|
+
throw new InvalidArgumentError(
|
|
1736
|
+
'Property pattern %v at %v has an invalid expression.',
|
|
1737
|
+
pattern,
|
|
1738
|
+
options.schemaSourceUri,
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
for (const propName of Object.keys(data)) {
|
|
1742
|
+
if (regex.test(propName)) {
|
|
1743
|
+
const propValue = data[propName];
|
|
1744
|
+
const result = _validate(propValue, patternSchema, {
|
|
1745
|
+
...options,
|
|
1746
|
+
dataSourceUri: `${options.dataSourceUri}/${propName}`,
|
|
1747
|
+
schemaSourceUri: `${options.schemaSourceUri}/patternProperties/${pattern}`,
|
|
1748
|
+
});
|
|
1749
|
+
if (!result.isValid) {
|
|
1750
|
+
return result;
|
|
1751
|
+
}
|
|
1752
|
+
// если значение свойства изменилось, то выполняется
|
|
1753
|
+
// обновление исходных данных без мутации оригинала
|
|
1754
|
+
// (поверхностное копирование объекта)
|
|
1755
|
+
if (data[propName] !== result.value) {
|
|
1756
|
+
if (data === originalData) {
|
|
1757
|
+
data = {...originalData};
|
|
1758
|
+
}
|
|
1759
|
+
data[propName] = result.value;
|
|
1760
|
+
}
|
|
1761
|
+
// если проверка свойства прошла успешно,
|
|
1762
|
+
// то свойство помечается как проверенное
|
|
1763
|
+
evaluatedProperties.add(propName);
|
|
1764
|
+
// добавление проверенного свойства
|
|
1765
|
+
// в массив проверенных по шаблону
|
|
1766
|
+
passedPatternProperties.push(propName);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
// additionalProperties
|
|
1772
|
+
if (schema.additionalProperties !== undefined) {
|
|
1773
|
+
const definedProps = schema.properties
|
|
1774
|
+
? Object.keys(schema.properties)
|
|
1775
|
+
: [];
|
|
1776
|
+
for (const propName of Object.keys(data)) {
|
|
1777
|
+
// если свойство определено в "properties",
|
|
1778
|
+
// то свойство не является дополнительным
|
|
1779
|
+
if (definedProps.includes(propName)) {
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
// если свойство обработано "patternProperties",
|
|
1783
|
+
// то такое свойство не является дополнительным
|
|
1784
|
+
if (passedPatternProperties.includes(propName)) {
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
// если значение параметра "additionalProperties" определено
|
|
1788
|
+
// как false, то любые неизвестные свойства вызывают ошибку
|
|
1789
|
+
if (schema.additionalProperties === false) {
|
|
1790
|
+
return {
|
|
1791
|
+
isValid: false,
|
|
1792
|
+
value: data[propName],
|
|
1793
|
+
valueUri: `${options.dataSourceUri}/${propName}`,
|
|
1794
|
+
schemaUri: `${options.schemaSourceUri}/additionalProperties`,
|
|
1795
|
+
reason: format(
|
|
1796
|
+
'Property %v at %v is not allowed.',
|
|
1797
|
+
propName,
|
|
1798
|
+
options.dataSourceUri,
|
|
1799
|
+
),
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
// если значение параметра "additionalProperties" является
|
|
1803
|
+
// схемой, то проверяется значение неизвестного свойства
|
|
1804
|
+
if (
|
|
1805
|
+
schema.additionalProperties !== null &&
|
|
1806
|
+
typeof schema.additionalProperties === 'object' &&
|
|
1807
|
+
!Array.isArray(schema.additionalProperties)
|
|
1808
|
+
) {
|
|
1809
|
+
const propValue = data[propName];
|
|
1810
|
+
const result = _validate(propValue, schema.additionalProperties, {
|
|
1811
|
+
...options,
|
|
1812
|
+
dataSourceUri: `${options.dataSourceUri}/${propName}`,
|
|
1813
|
+
schemaSourceUri: `${options.schemaSourceUri}/additionalProperties`,
|
|
1814
|
+
});
|
|
1815
|
+
if (!result.isValid) {
|
|
1816
|
+
return result;
|
|
1817
|
+
}
|
|
1818
|
+
// если значение свойства изменилось, то выполняется
|
|
1819
|
+
// обновление исходных данных без мутации оригинала
|
|
1820
|
+
// (поверхностное копирование объекта)
|
|
1821
|
+
if (data[propName] !== result.value) {
|
|
1822
|
+
if (data === originalData) {
|
|
1823
|
+
data = {...originalData};
|
|
1824
|
+
}
|
|
1825
|
+
data[propName] = result.value;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
// если проверка свойства прошла успешно,
|
|
1829
|
+
// то свойство помечается как проверенное
|
|
1830
|
+
evaluatedProperties.add(propName);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
// unevaluatedProperties
|
|
1834
|
+
if (schema.unevaluatedProperties !== undefined) {
|
|
1835
|
+
// проход по всем ключам объекта для обработки
|
|
1836
|
+
// незатронутых свойств
|
|
1837
|
+
for (const propName of Object.keys(data)) {
|
|
1838
|
+
// если свойство было проверено ранее,
|
|
1839
|
+
// то свойство пропускается
|
|
1840
|
+
if (evaluatedProperties.has(propName)) {
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
// если свойство не было проверено,
|
|
1844
|
+
// применяется схема "unevaluatedProperties"
|
|
1845
|
+
if (schema.unevaluatedProperties === false) {
|
|
1846
|
+
return {
|
|
1847
|
+
isValid: false,
|
|
1848
|
+
value: data[propName],
|
|
1849
|
+
valueUri: `${options.dataSourceUri}/${propName}`,
|
|
1850
|
+
schemaUri: `${options.schemaSourceUri}/unevaluatedProperties`,
|
|
1851
|
+
reason: format(
|
|
1852
|
+
'Property %v at %v is not evaluated and not allowed.',
|
|
1853
|
+
propName,
|
|
1854
|
+
options.dataSourceUri,
|
|
1855
|
+
),
|
|
1856
|
+
};
|
|
1857
|
+
} else if (
|
|
1858
|
+
schema.unevaluatedProperties &&
|
|
1859
|
+
typeof schema.unevaluatedProperties === 'object' &&
|
|
1860
|
+
!Array.isArray(schema.unevaluatedProperties)
|
|
1861
|
+
) {
|
|
1862
|
+
const propValue = data[propName];
|
|
1863
|
+
const result = _validate(propValue, schema.unevaluatedProperties, {
|
|
1864
|
+
...options,
|
|
1865
|
+
dataSourceUri: `${options.dataSourceUri}/${propName}`,
|
|
1866
|
+
schemaSourceUri: `${options.schemaSourceUri}/unevaluatedProperties`,
|
|
1867
|
+
});
|
|
1868
|
+
if (!result.isValid) {
|
|
1869
|
+
return result;
|
|
1870
|
+
}
|
|
1871
|
+
// если значение свойства изменилось, то выполняется
|
|
1872
|
+
// обновление исходных данных без мутации оригинала
|
|
1873
|
+
// (поверхностное копирование объекта)
|
|
1874
|
+
if (data[propName] !== result.value) {
|
|
1875
|
+
if (data === originalData) {
|
|
1876
|
+
data = {...originalData};
|
|
1877
|
+
}
|
|
1878
|
+
data[propName] = result.value;
|
|
1879
|
+
}
|
|
1880
|
+
// если проверка свойства прошла успешно,
|
|
1881
|
+
// то свойство помечается как проверенное
|
|
1882
|
+
evaluatedProperties.add(propName);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
// stripUnknown
|
|
1887
|
+
if (options.stripUnknown === true) {
|
|
1888
|
+
// если на данном этапе есть хотя бы одно проверенное
|
|
1889
|
+
// свойство, то это значит, что структура объекта
|
|
1890
|
+
// определена схемой, и можно выполнить удаление
|
|
1891
|
+
// неизвестных свойств
|
|
1892
|
+
if (evaluatedProperties.size > 0) {
|
|
1893
|
+
for (const propName of Object.keys(data)) {
|
|
1894
|
+
if (evaluatedProperties.has(propName)) {
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
// чтобы избежать мутации исходных данных,
|
|
1898
|
+
// выполняется копирование оригинала
|
|
1899
|
+
if (data === originalData) {
|
|
1900
|
+
data = {...originalData};
|
|
1901
|
+
}
|
|
1902
|
+
delete data[propName];
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
return {
|
|
1907
|
+
isValid: true,
|
|
1908
|
+
value: data,
|
|
1909
|
+
valueUri: options.dataSourceUri,
|
|
1910
|
+
schemaUri: options.schemaSourceUri,
|
|
1911
|
+
evaluatedProperties,
|
|
1912
|
+
};
|
|
1913
|
+
}
|